Gemini Backend: LowCode REST API - parte 2

8 minuti di lettura

Gemini Rest - part 2

Nel primo articolo delle serie Gemini Backend: LowCode REST API abbiamo visto come è semplice creare delle API REST CRUD partendo da un modello dati tipizzato. Si può dire che con Gemini la parte più difficile (e che ha più valore) diventa quella di definizione del dominio dei dati. Ma ovviamente questo dipende da voi e dal vostro caso d'uso, Gemini vi spinge a pensare a quello che conta ( Dominio ) piuttosto che concentrarsi da subito sulla tecnologia o soluzioni a contorno (che è un errore che vedo fare di frequente).

In questo articolo vedremo come usare l'API GET per filtrare i record delle varie entità/collezioni del nostro dominio ed anche come usare l'API di count che diventa molto utile quando non vi servono subito i record ma si ha necessità di sapere se esistono.

Due parole su Gemini

Gemini è un framework che sto sviluppando per velocizzare la parte di sviluppo/utilizzo di API REST che mi capita di dover scrivere nel mio lavoro.

Il realtà è composto da una serie di moduli che backend e frontend che consentono di creare in poco tempo applicazioni web complete (fullstack) di stile gestionale, senza togliere al programmatore la possibilità di customizzare ed inserire il proprio codice sia lato backend che frontend.

In questo articolo ci concentriamo sulla parte backend, con il generatore di API REST, ma potete trovare tutti i dettagli ed i casi d'uso principali del progetto sul repository github ufficiale.

Step 0 - Schema e Run dell'immagine

Nel primo articolo viene spiegato in modo più approfondito come avviare l'immagine Docker del backend. Dateci un occhiata se vi è sfuggito, in ogno caso riporto anche qua lo schema che abbiamo utilizzato ed il comando di avvio. L'immagine è pubblica, non avete bisogno di installare nulla (a parte Docker se non lo avete).

type: ENTITY
entity:
  name: CATEGORY
  lk: [id]
  fields:
    - name: id
      type: STRING
      required: true
    - name: description
      type: STRING

---

type: ENTITY
entity:
  name: PRODUCT
  lk: [id]
  fields:
    - name: id
      type: STRING
      required: true
    - name: name
      type: STRING
    - name: description
      type: STRING
    - name: available
      type: BOOL
    - name: status
      type: ENUM
      enums: [DRAFT, PENDING, PRIVATE, PUBLISH]
    - name: regular_price
      type: DOUBLE
    - name: sale_price
      type: DOUBLE
    - name: categories
      type: ARRAY
      array:
        type: ENTITY_REF
        entityRef:
          entity: CATEGORY
docker run -p 8080:8080 \
-e GEMINI_SCHEMAS=/schemas/product_schema.yaml \
-e GEMINI_MONGODB_URL="mongodb+srv://_mongo_user:_mongo_pwd_@__path__.mongodb.net/db?retryWrites=true&w=majority" \
-e GEMINI_MONGODB_DB=starter \
-v $(pwd)/product_schema.yaml:/schemas/product_schema.yaml:ro \
aat7/gemini-micronaut-mongodb-restapi

Bene con lo schema fornito e l'avvio del servizio avrete in ascolto sulla porta 8080 il modulo backend di Gemini.

Step 1 - Inserimento dati

Nel primo articolo abbiamo inserito solo le categorie, ora invece conviene popolare il database con un pò di prodotti diversi, in modo da essere in grado di filtrare in base ai vari campi dell'entità Prodotti e soprattutto in base al tipo dei vari campi.

Inseriamo un pò di prodotti tecnologici, in particolare Iphone 13 con differenti colori e parametri.

curl --request POST "http://localhost:8080/data/product" \
--header "Content-Type: application/json" \
-d '{"data": [{
                  "id": "iphone-13-128-blue",
                  "name": "Iphone 13 128 GB Blue",
                  "description": "Apple Iphone 13 - Memory: 128 GB - Color: Blue",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 799,
                  "sale_price": 699,
                  "categories": ["tech-1", "smartphone-1"]
              },
              {
                  "id": "iphone-13-128-starlight",
                  "name": "Iphone 13 128 GB Starlight",
                  "description": "Apple Iphone 13 - Memory: 128 GB - Color: Starlight",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 799,
                  "sale_price": 699,
                  "categories": ["tech-1", "smartphone-1"]
              },
              {
                  "id": "iphone-13-128-midnight",
                  "name": "Iphone 13 128 GB Midnight",
                  "description": "Apple Iphone 13 - Memory: 128 GB - Color: Midnight",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 799,
                  "sale_price": 699,
                  "categories": ["tech-1", "smartphone-1"]
              },
                            {
                  "id": "iphone-13-128-pink",
                  "name": "Iphone 13 128 GB Pink",
                  "description": "Apple Iphone 13 - Memory: 128 GB - Color: Pink",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 799,
                  "sale_price": 699,
                  "categories": ["tech-1", "smartphone-1"]
              },
                            {
                  "id": "iphone-13-128-red",
                  "name": "Iphone 13 128 GB Red",
                  "description": "Apple Iphone 13 - Memory: 128 GB - Color: Product RED",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 799,
                  "sale_price": 699,
                  "categories": ["tech-1", "smartphone-1"]
              },
              {
                  "id": "iphone-13-256-blue",
                  "name": "Iphone 13 256 GB Blue",
                  "description": "Apple Iphone 13 - Memory: 256 GB - Color: Blue",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 899,
                  "sale_price": 799,
                  "categories": ["tech-1", "smartphone-1"]
              },
              {
                  "id": "iphone-13-256-starlight",
                  "name": "Iphone 13 256 GB Starlight",
                  "description": "Apple Iphone 13 - Memory: 256 GB - Color: Starlight",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 899,
                  "sale_price": 799,
                  "categories": ["tech-1", "smartphone-1"]
              },
              {
                  "id": "iphone-13-256-midnight",
                  "name": "Iphone 13 256 GB Midnight",
                  "description": "Apple Iphone 13 - Memory: 256 GB - Color: Midnight",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 899,
                  "sale_price": 799,
                  "categories": ["tech-1", "smartphone-1"]
              },
                            {
                  "id": "iphone-13-256-pink",
                  "name": "Iphone 13 256 GB Pink",
                  "description": "Apple Iphone 13 - Memory: 256 GB - Color: Pink",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 899,
                  "sale_price": 799,
                  "categories": ["tech-1", "smartphone-1"]
              },
                            {
                  "id": "iphone-13-256-red",
                  "name": "Iphone 13 256 GB Red",
                  "description": "Apple Iphone 13 - Memory: 256 GB - Color: Product RED",
                  "available": true,
                  "status": "PUBLISH",
                  "regular_price": 899,
                  "sale_price": 799,
                  "categories": ["tech-1", "smartphone-1"]
              }]
  }'
{
    "status": "success",
    "data": [
        {
            "regular_price": 799.0,
            "name": "Iphone 13 128 GB Blue",
            "available": true,
            "description": "Apple Iphone 13 - Memory: 128 GB - Color: Blue",
            "id": "iphone-13-128-blue",
            "categories": [
                "tech-1",
                "smartphone-1"
            ],
            "sale_price": 699.0,
            "status": "PUBLISH"
        },
        // ....
        // ....
        // ....
        {
            "regular_price": 899.0,
            "name": "Iphone 13 256 GB Red",
            "available": true,
            "description": "Apple Iphone 13 - Memory: 256 GB - Color: Product RED",
            "id": "iphone-13-256-red",
            "categories": [
                "tech-1",
                "smartphone-1"
            ],
            "sale_price": 799.0,
            "status": "PUBLISH"
        }
    ],
    "meta": {
        "lastUpdateTimeUnix": 1640703311085,
        "lastUpdateTimeISO": "2021-12-28T14:55:11.085Z",
        "elapsedTime": "479ms"
    }
}

Step 2 - Filtri e Count

Se con una GET alla root dell'entità /data/{entityName} possiamo recuperare tutti i suoi record, con una GET a /entity/{entityName}/recordCounts è possibile contarli.

Count prodotti

curl "http://localhost:8080/entity/product/recordCounts"
{
    "status": "success",
    "data": {
        "count": 10
    },
    "meta": {
        "elapsedTime": "48ms"
    }
}

Filtri

Veniamo ora alla parte interessante, ovvero filtrare le entità in base ai valori dei campi contenuti nei record.

Stringhe

Il campo name dell'entità Product è una stringa. Il driver mongoDb attualmente supporta il CONTAINS e ovviamente l' EQUALS.

# Count - Iphone 13 128 GB Blue - NB: the space is %20 in curl url
curl "http://localhost:8080/entity/product/recordCounts?name=Iphone%2013%20128%20GB%20Blue"
{"status":"success","data":{"count":1},"meta":{"elapsedTime":"38ms"}}

# Get Record - Iphone 13 128 GB Blue
curl "http://localhost:8080/data/product?name=Iphone%2013%20128%20GB%20Blue"
{"status":"success","data":[{"regular_price":799.0,"name":"Iphone 13 128 GB Blue","available":true,"description":"Apple Iphone 13 - Memory: 128 GB - Color: Blue","id":"iphone-13-128-blue","categories":["tech-1","smartphone-1"],"sale_price":699.0,"status":"PUBLISH"}],"meta":{"elapsedTime":"49ms"}}

Per usare CONTAINS la sintassi diventa /data/{entityName}?fieldName[CONTAINS]=value. Ad esempio contiamo tutti i record che contengono 256 nel campo name e recuperiamo tutti i record che contengono Blue.

# Count - Name CONTAINS 256 - result is 5
curl "http://localhost:8080/entity/product/recordCounts?name[CONTAINS]=256"
{"status":"success","data":{"count":5},"meta":{"elapsedTime":"40ms"}}

# Get Records - Name CONTAINS 256 - result is 2 records
curl "http://localhost:8080/data/product?name[CONTAINS]=Blue"
{"status":"success","data":[{"regular_price":799.0,"name":"Iphone 13 128 GB Blue","available":true,"description":"Apple Iphone 13 - Memory: 128 GB - Color: Blue","id":"iphone-13-128-blue","categories":["tech-1","smartphone-1"],"sale_price":699.0,"status":"PUBLISH"},{"regular_price":899.0,"name":"Iphone 13 256 GB Blue","available":true,"description":"Apple Iphone 13 - Memory: 256 GB - Color: Blue","id":"iphone-13-256-blue","categories":["tech-1","smartphone-1"],"sale_price":799.0,"status":"PUBLISH"}],"meta":{"elapsedTime":"41ms"}}

Campi numerici

Il campo regular_price è un DOUBLE. Per semplicità riporto solo esempi con l'API di COUNT, ma il recupero dei record segue lo stesso principio.

Dato che si tratta di un campo numerico abbiamo a disposizione, la solita uguaglianza, ma anche operatori comuni come > < >= <= rispettivamente GT LT GTE LTE. Abbiamo inserito 5 prodotti con regular_price di 799 ed altri 5 con regular_price di 899.

# 10 products > 700
curl "http://localhost:8080/entity/product/recordCounts?regular_price[GT]=700"
{"status":"success","data":{"count":10},"meta":{"elapsedTime":"40ms"}}

# 5 products > 799 
curl "http://localhost:8080/entity/product/recordCounts?regular_price[GT]=799"
{"status":"success","data":{"count":5},"meta":{"elapsedTime":"40ms"}}

# 10 production >= 799 
curl "http://localhost:8080/entity/product/recordCounts?regular_price[GTE]=799"
{"status":"success","data":{"count":10},"meta":{"elapsedTime":"39ms"}}

Una feature interessante è la possibilità di avere più logiche su uno stesso campo in modo da poter esprimere anche intervalli in cui fare la ricerca. Ed ovviamente nessuno vieta di fare ricerche su campi diversi con la medesima logica implicita di AND.

# 5 products with 750 < regular_price < 850
curl "http://localhost:8080/entity/product/recordCounts?regular_price[GT]=750&regular_price[LT]=850"
{"status":"success","data":{"count":5},"meta":{"elapsedTime":"44ms"}}

# 1 products with 750 < regular_price < 850 and name containing 
curl "http://localhost:8080/entity/product/recordCounts?regular_price[GT]=750&regular_price[LT]=850&name[CONTAINS]=Blue"
{"status":"success","data":{"count":1},"meta":{"elapsedTime":"39ms"}}

Campi booleani

Il campo available è un BOOL. Dato che abbiamo inserito solo record con available a true il count a false darà 0 mentre true sarà 10.

# 10 products available
curl "http://localhost:8080/entity/product/recordCounts?available=true"
{"status":"success","data":{"count":10},"meta":{"elapsedTime":"39ms"}}

# 0 products not available
curl "http://localhost:8080/entity/product/recordCounts?available=false"
{"status":"success","data":{"count":0},"meta":{"elapsedTime":"42ms"}}

Conclusioni

In questo articolo abbiamo fatto un piccolo passo avanti nell'esplorazione delle feature del modulo backend di Gemini. Abbiamo visto in particolare come usare i filtri (anche complessi) sia per contare i record che per recuperarne il contenuto.

Tuttavia l'esempio in generale è molto basilare. In reali contesti di produzione uso feature più avanzate sia di Gemini che di Micronaut, come l'autenticazione, la paginazione sulle entità corpose e tipi di dato più complessi da gestire come ARRAY ed OGGETTI annidati.

Tenete in considerazione che Gemini è si in fase di sviluppo, ma è anche attualmente in produzione in aziende con cui collaboro. Se siete curiosi e volete provare a vedere se Gemini può essere utile per il vostro caso d'uso concreto contattatemi pure, sarò felice di aiutare e magari estendere la piattaforma.