A story about developing a soft type with Firestore + Python + OpenAPI + Typescript

Overview

If you define a type with Python's self-made ORM (fsglue) in a schemaless Firestore and combine it with OpenAPI or Typescript, it is a flexible but comfortable development environment. I made it, so I tried to summarize it easily

background

As a hobby, I am developing Low Code Business App Platform Bizglue, and this is a behind-the-scenes story. For the motives for developing the service, see note, so if you are interested, please check it out.

Rough flow until the final configuration

--The server side wants to use Python, which has been used for a long time --I don't want to manage the server and I can't spend money, so use App Engine with a free tier. ――AppEngine is Datastore, but the new Firestore seems to be more convenient, so let's use it. --There is ndb for Datastore, but isn't Firestore an ORM? -(Proceed with development little by little) ――The amount of code is increasing and it is hard to crush bugs. Yeah, let's put in Typescript --It's hard to define a type twice on the server side and the front side. Yes, should I use OpenAPI? --Firestore ORM It looks good, so let's publish it as open source → fsglue ――Because it is a good time, let's write an introductory article ★ Imakoko

overall structure

Server side

--App Engine (standard environment)

Client side

Soft flow

  1. Create Firestore model definition with fsglue
  2. Register the model definition in flasgger
  3. Create OpenAPI definition
  4. Generate API client with openapi-generater
  5. API client can be used with Typescript type
  6. (゚ д ゚) Ummer

What is "soft" is that Firestore itself is basically schemaless, so adding fields and backward compatible changes only requires changing the model definition (no troublesome migration process is required). .. It is a little troublesome to generate the API client manually, but I think it is quite convenient to be able to check the type with Typescript based on the model definition defined on the server side.

Specific code sample

I will explain it a little more concretely with sample code.

Firestore model definition

If you define a model like this

import fsglue

TAGS_SCHEMA = {
    "type": "array",
    "items": {
        "type": "string",
    },
}

class User(fsglue.BaseModel):
    COLLECTION_PATH = "users"
    COLLECTION_PATH_PARAMS = []

    name = fsglue.StringProperty(required=True)
    tags = fsglue.JsonProperty(schema=TAGS_SCHEMA, default=[])
    created_at = fsglue.TimestampProperty(auto_now=True)
    updated_at = fsglue.TimestampProperty(auto_now_add=True)

You can generate the following JsonSchema definition with ʻUser.to_schema () `.

{
  "type": "object",
  "required": [
    "name",
    "owner"
  ],
  "properties": {
    "id": {
      "type": "string"
    },
    "name": {
      "type": "string"
    },
    "tags": {
      "type": "array",
      "items": {
        "type": "string"
      }
    },
    "created_at": {
      "type": "number"
    },
    "updated_at": {
      "type": "number"
    }
  }
}

Register model definition in flasgger

ʻPass the execution result of User.to_schema ()` to flasgger so that it can be referenced in the definition for each endpoint.

from flasgger import Swagger
from xxx import models  #Model definition
from xxx import app  #flask app

template = {
  "swagger": "2.0",
  ...Omission...
  "definitions": {
      "User": models.User.to_schema(),  #Define model
      ...Omission...
  },
  ...Omission...
}

swagger = Swagger(app, template=template)

OpenAPI definition

For example, implement an API to get a list of users belonging to a certain organization with the following image.

from flask import Blueprint
from flasgger.utils import swag_from

app = Blueprint("user", __name__, url_prefix="/api/v1")

@app.route('/organization/<org_id>/user/', methods=['GET'])
@swag_from({
    "operationId": "getUserList",
    "summary": "getUserList",
    "parameters": [
        {"name": "org_id", "in": "path", "type": "string", "required": "true"},
    ],
    "responses": {
        "200": {
            "description": "users",
            "schema": {
                "type": "array",
                "items": {
                    "$ref": "#/definitions/User",  #See models registered in flasgger
                }
            },
        }
    },
    "tags": ["user"],
})
@auth.xxx_required  #Authority check decorator
def list_user(org_id, **kwargs):
    users = User.all(org_id, to_dict=True)
    return jsonify(users)

API client generation

You can install openapi-generator and generate an API client in the development environment with the following command (this time, typescript-fetch I'm using a client)

#Get json of OpenAPI definition
curl http://localhost:8080/apispec_1.json > ./xxx/apispec.json 

# openapi-Generate API client with generator
./node_modules/.bin/openapi-generator \
    generate \
    -g typescript-fetch \
    -o ./yyy/api/ \
    -i ./xxx/apispec.json \
    --additional-properties modelPropertyNaming=snake_case \  #Options are your choice
    --additional-properties supportsES6=true \
    --additional-properties typescriptThreePlus=true \
    --additional-properties disallowAdditionalPropertiesIfNotPresent=false

Use of API client

With the image below, you can use the API client while benefiting from the Typescript type. (When actually using it, in order to give authentication information and implement common processing, the generated API is called via a wrapper instead of directly calling it)

import { UserApi } from "xxx/UserApi";

const api = new OrganizationApi();
//Below, the API argument of getUserList and the return type work with Typescript
const users = await api.getUserList({ orgId: "test" });

Impressions

Where it was subtle

Detailed behavior of type conversion from Json Schema to Typescript

The openapi-generator automatically converts the Json Schema definition to a TypeScript type, but there were some places where I couldn't reach the subtle itching in the details. It can't be helped because it is not completely compatible in terms of specifications, but specifically [dependencies](https://json-schema.org/understanding-json-schema/reference/object.html (#dependencies) is not converted to a type nicely, or enum cannot be a Union type of Typescript. This time, I made it OpenAPI in the middle of development, so if you want to incorporate it from the beginning, it may be better to make it while adjusting the type on the Json Schema side so that the type on the Typescript side feels good.

API client specifications generated by openapi-geneator (typescript-fetch)

It was a little difficult to incorporate common processing (passing a token when hitting the API, incorporating common error handling, etc.) in all APIs. It seems convenient to use the generated API client as it is, but I felt that there were not many interfaces for extending it later, so I'm looking forward to future development.

Convenient place

You can use one definition from server to front

When adding or modifying the schema on the server side, the Typescript type check shows some influence on the front end side, so I feel that this has led to an improvement in development speed. Checking the consistency of the implementation on the server side and the front side is a common and tedious task, so it's nice to be able to reduce that.

There is a mold but it is flexible

Since Firestore itself is schemaless, changes that do not conflict with existing data and modifications that do not affect the index surroundings can be developed simply by modifying the model definition, so while benefiting from Firestore's schemaless, type checking I feel that it is a good mechanism that you can also benefit from the early detection of bugs by. (I don't think it's suitable for large-scale development, but in that case I don't feel like using Firestore in the first place.)

OpenAPI ecosystem available

I haven't used it much this time, but since it will be possible to use the ecosystem maintained by OpenAPI, if it is incorporated well, it seems that there are also the following usage methods.

--Automatic API documentation generation --Validation of requests and responses --Generate Mock / Stub for testing

finally

I personally develop a service called Low Code Development Platform Bizglue. Please use it!

Recommended Posts

A story about developing a soft type with Firestore + Python + OpenAPI + Typescript
A story about making 3D space recognition with Python
A story about making Hanon-like sheet music with Python
A story about an amateur making a breakout with python (kivy) ②
A story about an amateur making a breakout with python (kivy) ①
A story about a python beginner stuck with No module named'http.server'
A story about adding a REST API to a daemon made with Python
A Java programmer studied Python. (About type)
A story about machine learning with Kyasuket
A story about Python pop and append
[Python3] A story stuck with time zone conversion
A story stuck with handling Python binary data
A story about implementing a login screen with django
A story about running Python on PHP on Heroku
A story about modifying Python and adding functions
A story about using Resona's software token with 1Password
A story about predicting exchange rates with Deep Learning
A story about how Windows 10 users created an environment to use OpenCV3 with Python 3.5
A story about how theano was moved with TSUBAME 2.0
A memo about building a Django (Python) application with Docker
[Note] A story about trying to override a class method with two underscores in Python 3 series.
Machine learning A story about people who are not familiar with GBDT using GBDT in Python
A story about developing a machine learning model while managing experiments and models with Azure Machine Learning + MLflow
A note about hitting the Facebook API with the Python SDK
A story about how to specify a relative path in python.
A story about competing with a friend in Othello AI Preparation
A story about installing matplotlib using pip with an error
A story about how to deal with the CORS problem
A story about making a tanka by chance with Sudachi Py
A story about trying to implement a private variable in Python.
Stumble story with Python array
A memorandum about correlation [Python]
Make a fortune with Python
A memorandum about Python mock
Create a directory with python
A note about [python] __debug__
[Python, Selenium, PhantomJS] A story when scraping a website with lazy load
The story of making a standard driver for db with python.
A story about trying to run multiple python versions (Mac edition)
The story of making a module that skips mail with python
[Python] What is a with statement?
Solve ABC163 A ~ C with Python
A python graphing manual with Matplotlib.
A refreshing story about Python's Slice
Let's make a GUI with python.
Python: A Note About Classes 1 "Abstract"
A sloppy story about Python's Slice
Create a virtual environment with Python!
I made a fortune with Python.
Building a virtual environment with Python 3
Solve ABC168 A ~ C with Python
Make a recommender system with python
[Small story] Get timestamp with Python
[Python] Generate a password with Slackbot
Solve ABC162 A ~ C with Python
Solve ABC167 A ~ C with Python
Solve ABC158 A ~ C with Python
Master the type with Python [Python 3.9 compatible]
Let's make a graph with python! !!
A story about using Python's reduce
[Python] Inherit a class with class variables