Repository: cuducos/minha-receita

Issue:
Busca de CNPJ por filtro
A ideia é possibilitar buscas filtradas, como por exemplo `https://minhareceita.org/q?uf=df` listar todos os CNPJs do Distrito Federal.

### Escopo

* Por enquanto, apenas busca por UF (no futuro expandimos para outros filtros)
* Por enquanto, apenas buscas por uma UF por vez (não permitir `uf=df,go` por exemplo, ou `uf=df&uf=go`)
* Por enquanto, implementar apenas em um banco de dados, Postgres ou MongoDB (o outro retorna um erro de funcionalidade não implementada)

### Limitações

1. Não utilizar `OFFSET` e `LIMIT` pois isso é inviável em termos de performance. A ideia é usar um `cursor` que vai ser o último CNPJ visto. Assim, se o último resultado de `https://minhareceita.org/q?uf=df` for o CNPJ `12.345.678/9012-3`, a próxima “página” vai ser `https://minhareceita.org/q?uf=df&cursor=1234567890123`.
2. Devido a isso, ordenação customizada _não_ é uma opção por enquanto, vamos confiar apenas no `id` (número do CNPJ) como ordenação.

### Sugestão de implementação

Cada etapa dessa pode ser um PR:

- [x] ~~Método na interface de banco de dados como `db.Search(q Query)` onde `Query` é um `struct` com os parâmetros da busca — essa busca deve ter um limite máximo (começamos com 1000) e um “cursor” como explicado anteriormente~~ #346
- [x] ~~Criação automática do índice em UF antes do final do comando `transform`~~ 1279fcc
- [x] Criar na API o ponto de entrada `/q` para receber essas requisições e repassar para o banco. A resposta deve ser algo como `{"dados": […]", "cursor": …}`<br>#347
- [x] Documentar em `docs/` (que gera [docs.minhareceita.org](https://docs.minhareceita.org))<br>#347
- [x] Implementar para Postgres (já que é o usado em produção)<br>#347


Discussion:

Solution patch:
diff --git a/db/mongodb.go b/db/mongodb.go
index 08a4365..174dd63 100644
--- a/db/mongodb.go
+++ b/db/mongodb.go
@@ -25,6 +25,26 @@ type MongoDB struct {
 	ctx    context.Context
 }
 
+type Query struct {
+	Uf     string
+	Cursor string
+	Limit  int64
+}
+
+type page struct {
+	Data   []transform.Company `json:"data"`
+	Cursor *string             `json:"cursor"`
+}
+
+func newPage(cs []transform.Company) page {
+	var p page
+	if len(cs) > 0 {
+		p.Data = cs
+		p.Cursor = &cs[len(cs)-1].CNPJ
+	}
+	return p
+}
+
 // NewMongoDB initializes a new MongoDB connection wrapped in a structure.
 func NewMongoDB(uri string) (MongoDB, error) {
 	opts := options.Client().ApplyURI(uri)
@@ -251,3 +271,35 @@ func (m *MongoDB) CreateExtraIndexes(idxs []string) error {
 	slog.Info(fmt.Sprintf("%d %s successfully created in the collection %s", len(r), l, companyTableName))
 	return nil
 }
+
+func (m *MongoDB) Search(q Query) (string, error) {
+	ctx, cancel := context.WithCancel(context.Background())
+	defer cancel()
+	coll := m.db.Collection(companyTableName)
+	f := bson.M{"json.uf": q.Uf}
+	if q.Cursor != "" {
+		f["json.cnpj"] = bson.M{"$gt": q.Cursor}
+	}
+	opts := options.Find().
+		SetSort(bson.D{{Key: "json.cnpj", Value: 1}}).
+		SetLimit(q.Limit)
+	c, err := coll.Find(ctx, f, opts)
+	if err != nil {
+		return "", fmt.Errorf("error running query %#v: %w", q, err)
+	}
+	defer c.Close(ctx)
+	var r []record
+	if err := c.All(ctx, &r); err != nil {
+		return "", fmt.Errorf("error decoding results: %w", err)
+	}
+	var cs []transform.Company
+	for _, c := range r {
+		cs = append(cs, c.Json)
+	}
+	p := newPage(cs)
+	b, err := json.Marshal(p)
+	if err != nil {
+		return "", fmt.Errorf("error serializing the query result: %w", err)
+	}
+	return string(b), nil
+}
diff --git a/db/postgres.go b/db/postgres.go
index 21df784..f9ede55 100644
--- a/db/postgres.go
+++ b/db/postgres.go
@@ -265,6 +265,10 @@ func (p *PostgreSQL) CreateExtraIndexes(idxs []string) error {
 	return nil
 }
 
+func (p *PostgreSQL) Search(q Query) (string, error) {
+	return "", fmt.Errorf("not implemented")
+}
+
 // NewPostgreSQL creates a new PostgreSQL connection and ping it to make sure it works.
 func NewPostgreSQL(uri, schema string) (PostgreSQL, error) {
 	cfg, err := pgxpool.ParseConfig(uri)



Test patch:
diff --git a/db/mongodb_test.go b/db/mongodb_test.go
index 53fba3e..af21032 100644
--- a/db/mongodb_test.go
+++ b/db/mongodb_test.go
@@ -1,6 +1,7 @@
 package db
 
 import (
+	"encoding/json"
 	"os"
 	"path/filepath"
 	"slices"
@@ -106,6 +107,32 @@ func TestMongoDB(t *testing.T) {
 	if err := db.CreateExtraIndexes([]string{"teste.index1"}); err == nil {
 		t.Error("expected errors running extra indexes, got nil")
 	}
+	q := Query{Uf: "RJ", Limit: 1}
+	sr, err := db.Search(q)
+	if err != nil {
+		t.Errorf("expected no error querying %#v, got %s", q, err)
+	}
+	var r page
+	if err := json.Unmarshal([]byte(sr), &r); err != nil {
+		t.Errorf("expected error deserializing JSON, got %s", err)
+	}
+	if r.Data != nil {
+		t.Errorf("expected error no result, got %#v", r)
+	}
+	q = Query{Uf: "SP", Limit: 1}
+	sr, err = db.Search(q)
+	if err != nil {
+		t.Errorf("expected no error querying %#v, got %s", q, err)
+	}
+	if err := json.Unmarshal([]byte(sr), &r); err != nil {
+		t.Errorf("expected error deserializing JSON, got %s", err)
+	}
+	if len(r.Data) != 1 {
+		t.Errorf("expected one result, got %d", len(r.Data))
+	}
+	if r.Data[0].UF != q.Uf {
+		t.Errorf("expected query result to be from SP, got %s", r.Data[0].UF)
+	}
 	i := []string{"qsa.nome_socio"}
 	if err := db.CreateExtraIndexes(i); err != nil {
 		t.Errorf("expected no errors running extra indexes, got %s", err)