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
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
--App Engine (standard environment)
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.
I will explain it a little more concretely with sample code.
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"
}
}
}
ʻ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)
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)
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
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" });
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.
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.
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.
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.)
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
I personally develop a service called Low Code Development Platform Bizglue. Please use it!
Recommended Posts