feat(crud): tipos y generador de DDL para recursos CRUD
Anade los tipos CRUDResource, CRUDField, CRUDListParams y CRUDListResult que modelan un recurso CRUD sobre SQLite, junto con dos funciones puras: - crud_define_resource valida nombre, tabla y campos (tipos SQLite validos, nombres reservados, duplicados) antes de retornar el CRUDResource. - crud_generate_table_sql genera el DDL CREATE TABLE IF NOT EXISTS con id TEXT PRIMARY KEY, timestamps estandar y, si aplica, deleted_at para soft delete. Primera capa de 0021 — el resto (handlers + registro de rutas) se apoya sobre estas estructuras.
This commit is contained in:
@@ -0,0 +1,40 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// CRUDDefineResource construye un CRUDResource validando que el nombre no este vacio,
|
||||||
|
// que haya al menos un campo y que todos los tipos de los campos sean validos
|
||||||
|
// (TEXT, INTEGER, REAL, BLOB). Es pura — solo valida y devuelve la estructura.
|
||||||
|
func CRUDDefineResource(name string, table string, fields []CRUDField, softDelete bool) (CRUDResource, error) {
|
||||||
|
if name == "" {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: name must not be empty")
|
||||||
|
}
|
||||||
|
if table == "" {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: table must not be empty")
|
||||||
|
}
|
||||||
|
if len(fields) == 0 {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: must have at least one field")
|
||||||
|
}
|
||||||
|
seen := make(map[string]bool, len(fields))
|
||||||
|
for _, f := range fields {
|
||||||
|
if f.Name == "" {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: field name must not be empty")
|
||||||
|
}
|
||||||
|
if f.Name == "id" || f.Name == "created_at" || f.Name == "updated_at" || f.Name == "deleted_at" {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: field name %q is reserved", f.Name)
|
||||||
|
}
|
||||||
|
if seen[f.Name] {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: duplicate field name %q", f.Name)
|
||||||
|
}
|
||||||
|
seen[f.Name] = true
|
||||||
|
if !isValidCRUDType(f.Type) {
|
||||||
|
return CRUDResource{}, fmt.Errorf("crud_define_resource: invalid type %q for field %q (must be TEXT, INTEGER, REAL or BLOB)", f.Type, f.Name)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return CRUDResource{
|
||||||
|
Name: name,
|
||||||
|
Table: table,
|
||||||
|
Fields: fields,
|
||||||
|
SoftDelete: softDelete,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
---
|
||||||
|
name: crud_define_resource
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "func CRUDDefineResource(name string, table string, fields []CRUDField, softDelete bool) (CRUDResource, error)"
|
||||||
|
description: "Construye un CRUDResource validando nombre, tabla y campos. Rechaza nombres de campo reservados (id, created_at, updated_at, deleted_at), duplicados y tipos distintos de TEXT, INTEGER, REAL, BLOB."
|
||||||
|
tags: [crud, resource, define, validation, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [CRUDResource_go_infra, CRUDField_go_infra]
|
||||||
|
returns: [CRUDResource_go_infra]
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: [fmt]
|
||||||
|
params:
|
||||||
|
- name: name
|
||||||
|
desc: "nombre singular del recurso en snake_case (ej: 'project')"
|
||||||
|
- name: table
|
||||||
|
desc: "nombre de la tabla SQLite asociada (ej: 'projects')"
|
||||||
|
- name: fields
|
||||||
|
desc: "lista de CRUDField con los campos del recurso (sin id ni timestamps)"
|
||||||
|
- name: softDelete
|
||||||
|
desc: "si true, el recurso usa deleted_at en vez de borrado fisico"
|
||||||
|
output: "CRUDResource validado listo para pasar a crud_generate_table_sql y crud_generate_handlers"
|
||||||
|
tested: true
|
||||||
|
tests: ["construye un recurso valido", "rechaza nombre vacio", "rechaza tabla vacia", "rechaza lista de campos vacia", "rechaza tipos invalidos", "rechaza nombres reservados", "rechaza duplicados"]
|
||||||
|
test_file_path: "functions/infra/crud_test.go"
|
||||||
|
file_path: "functions/infra/crud_define_resource.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, err := CRUDDefineResource("project", "projects", []CRUDField{
|
||||||
|
{Name: "name", Type: "TEXT", Required: true, Unique: true},
|
||||||
|
{Name: "priority", Type: "INTEGER", Default: "0"},
|
||||||
|
}, false)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Funcion pura — no hace I/O. Valida antes de devolver. Los campos id, created_at, updated_at y deleted_at son gestionados por el generador de tabla y los handlers, por eso estan reservados. Los tipos aceptados son los tipos de almacenamiento nativos de SQLite.
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CRUDGenerateTableSQL genera el DDL CREATE TABLE IF NOT EXISTS correspondiente a un CRUDResource.
|
||||||
|
// Incluye siempre: id TEXT PRIMARY KEY, created_at TEXT NOT NULL, updated_at TEXT NOT NULL.
|
||||||
|
// Si el recurso es SoftDelete, agrega una columna deleted_at TEXT (nullable).
|
||||||
|
// Cada CRUDField se mapea a su tipo SQLite y aplica NOT NULL, UNIQUE y DEFAULT segun corresponda.
|
||||||
|
func CRUDGenerateTableSQL(res CRUDResource) string {
|
||||||
|
var sb strings.Builder
|
||||||
|
sb.WriteString(fmt.Sprintf("CREATE TABLE IF NOT EXISTS %s (\n", res.Table))
|
||||||
|
sb.WriteString(" id TEXT PRIMARY KEY")
|
||||||
|
for _, f := range res.Fields {
|
||||||
|
sb.WriteString(",\n ")
|
||||||
|
sb.WriteString(f.Name)
|
||||||
|
sb.WriteString(" ")
|
||||||
|
sb.WriteString(strings.ToUpper(f.Type))
|
||||||
|
if f.Required {
|
||||||
|
sb.WriteString(" NOT NULL")
|
||||||
|
}
|
||||||
|
if f.Unique {
|
||||||
|
sb.WriteString(" UNIQUE")
|
||||||
|
}
|
||||||
|
if f.Default != "" {
|
||||||
|
sb.WriteString(" DEFAULT ")
|
||||||
|
sb.WriteString(f.Default)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sb.WriteString(",\n created_at TEXT NOT NULL")
|
||||||
|
sb.WriteString(",\n updated_at TEXT NOT NULL")
|
||||||
|
if res.SoftDelete {
|
||||||
|
sb.WriteString(",\n deleted_at TEXT")
|
||||||
|
}
|
||||||
|
sb.WriteString("\n);\n")
|
||||||
|
return sb.String()
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
---
|
||||||
|
name: crud_generate_table_sql
|
||||||
|
kind: function
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
purity: pure
|
||||||
|
signature: "func CRUDGenerateTableSQL(res CRUDResource) string"
|
||||||
|
description: "Genera el DDL CREATE TABLE IF NOT EXISTS de un CRUDResource. Incluye id como PRIMARY KEY, timestamps created_at/updated_at y deleted_at si soft_delete. Cada campo aplica su tipo SQLite y constraints NOT NULL/UNIQUE/DEFAULT."
|
||||||
|
tags: [crud, sql, ddl, generate, sqlite, infra]
|
||||||
|
uses_functions: []
|
||||||
|
uses_types: [CRUDResource_go_infra]
|
||||||
|
returns: []
|
||||||
|
returns_optional: false
|
||||||
|
error_type: ""
|
||||||
|
imports: [fmt, strings]
|
||||||
|
params:
|
||||||
|
- name: res
|
||||||
|
desc: "CRUDResource con la definicion completa del recurso"
|
||||||
|
output: "string con el statement CREATE TABLE listo para ejecutar"
|
||||||
|
tested: true
|
||||||
|
tests: ["genera tabla basica con timestamps", "aplica NOT NULL y UNIQUE", "aplica DEFAULT", "anade deleted_at si soft_delete"]
|
||||||
|
test_file_path: "functions/infra/crud_test.go"
|
||||||
|
file_path: "functions/infra/crud_generate_table_sql.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
res, _ := CRUDDefineResource("project", "projects", []CRUDField{
|
||||||
|
{Name: "name", Type: "TEXT", Required: true, Unique: true},
|
||||||
|
{Name: "priority", Type: "INTEGER", Default: "0"},
|
||||||
|
}, false)
|
||||||
|
ddl := CRUDGenerateTableSQL(res)
|
||||||
|
// CREATE TABLE IF NOT EXISTS projects (
|
||||||
|
// id TEXT PRIMARY KEY,
|
||||||
|
// name TEXT NOT NULL UNIQUE,
|
||||||
|
// priority INTEGER DEFAULT 0,
|
||||||
|
// created_at TEXT NOT NULL,
|
||||||
|
// updated_at TEXT NOT NULL
|
||||||
|
// );
|
||||||
|
db.Exec(ddl)
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Funcion pura — solo manipula strings. Usa CREATE TABLE IF NOT EXISTS para ser idempotente. Las columnas id, created_at y updated_at siempre se generan. Si el CRUDResource es SoftDelete, se anade deleted_at TEXT nullable. El resultado se puede ejecutar directamente con db.Exec o envolver como migracion.
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
import (
|
||||||
|
"database/sql"
|
||||||
|
"fmt"
|
||||||
|
"regexp"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
// validCRUDTypes enumera los tipos SQLite aceptados por las funciones CRUD.
|
||||||
|
var validCRUDTypes = map[string]bool{
|
||||||
|
"TEXT": true,
|
||||||
|
"INTEGER": true,
|
||||||
|
"REAL": true,
|
||||||
|
"BLOB": true,
|
||||||
|
}
|
||||||
|
|
||||||
|
// isValidCRUDType indica si el tipo string corresponde a uno soportado.
|
||||||
|
func isValidCRUDType(t string) bool {
|
||||||
|
return validCRUDTypes[strings.ToUpper(t)]
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudFieldByName busca un campo por nombre. Retorna nil si no existe.
|
||||||
|
func crudFieldByName(res CRUDResource, name string) *CRUDField {
|
||||||
|
for i := range res.Fields {
|
||||||
|
if res.Fields[i].Name == name {
|
||||||
|
return &res.Fields[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudColumnNames retorna la lista de nombres de columnas de una tabla sqlite.
|
||||||
|
// Usa PRAGMA table_info — unica forma portable en SQLite.
|
||||||
|
func crudColumnNames(db *sql.DB, table string) ([]string, error) {
|
||||||
|
rows, err := db.Query(fmt.Sprintf("PRAGMA table_info(%q)", table))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("crud column names: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var cols []string
|
||||||
|
for rows.Next() {
|
||||||
|
var cid int
|
||||||
|
var name, ctype string
|
||||||
|
var notnull, pk int
|
||||||
|
var dflt sql.NullString
|
||||||
|
if err := rows.Scan(&cid, &name, &ctype, ¬null, &dflt, &pk); err != nil {
|
||||||
|
return nil, fmt.Errorf("crud column names: %w", err)
|
||||||
|
}
|
||||||
|
cols = append(cols, name)
|
||||||
|
}
|
||||||
|
return cols, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudScanRow escanea una fila generica a map[string]any usando las columnas proporcionadas.
|
||||||
|
// Usa []any con apuntadores para que database/sql decida el tipo Go.
|
||||||
|
func crudScanRow(rows *sql.Rows, cols []string) (map[string]any, error) {
|
||||||
|
values := make([]any, len(cols))
|
||||||
|
scanArgs := make([]any, len(cols))
|
||||||
|
for i := range values {
|
||||||
|
scanArgs[i] = &values[i]
|
||||||
|
}
|
||||||
|
if err := rows.Scan(scanArgs...); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
row := make(map[string]any, len(cols))
|
||||||
|
for i, col := range cols {
|
||||||
|
v := values[i]
|
||||||
|
// Normalizar bytes a string (SQLite TEXT llega como []byte cuando no se tipa)
|
||||||
|
if b, ok := v.([]byte); ok {
|
||||||
|
row[col] = string(b)
|
||||||
|
} else {
|
||||||
|
row[col] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return row, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudValidateField valida un valor contra las reglas de un campo.
|
||||||
|
// Retorna nil si todo ok, error con mensaje descriptivo si falla alguna regla.
|
||||||
|
func crudValidateField(field CRUDField, value any) error {
|
||||||
|
if value == nil {
|
||||||
|
if field.Required {
|
||||||
|
return fmt.Errorf("field %q is required", field.Name)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
switch strings.ToUpper(field.Type) {
|
||||||
|
case "TEXT":
|
||||||
|
s, ok := value.(string)
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("field %q must be a string", field.Name)
|
||||||
|
}
|
||||||
|
return crudValidateText(field, s)
|
||||||
|
case "INTEGER":
|
||||||
|
n, err := crudCoerceInt(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field %q must be an integer", field.Name)
|
||||||
|
}
|
||||||
|
return crudValidateNumber(field, float64(n))
|
||||||
|
case "REAL":
|
||||||
|
f, err := crudCoerceFloat(value)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("field %q must be a number", field.Name)
|
||||||
|
}
|
||||||
|
return crudValidateNumber(field, f)
|
||||||
|
case "BLOB":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudValidateText aplica min_length, max_length, pattern, enum a un string.
|
||||||
|
func crudValidateText(field CRUDField, s string) error {
|
||||||
|
if v, ok := field.Validations["min_length"]; ok {
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err == nil && len(s) < n {
|
||||||
|
return fmt.Errorf("field %q must have at least %d characters", field.Name, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := field.Validations["max_length"]; ok {
|
||||||
|
n, err := strconv.Atoi(v)
|
||||||
|
if err == nil && len(s) > n {
|
||||||
|
return fmt.Errorf("field %q must have at most %d characters", field.Name, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := field.Validations["pattern"]; ok {
|
||||||
|
re, err := regexp.Compile(v)
|
||||||
|
if err == nil && !re.MatchString(s) {
|
||||||
|
return fmt.Errorf("field %q does not match pattern %q", field.Name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := field.Validations["enum"]; ok {
|
||||||
|
options := strings.Split(v, ",")
|
||||||
|
matched := false
|
||||||
|
for _, opt := range options {
|
||||||
|
if strings.TrimSpace(opt) == s {
|
||||||
|
matched = true
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !matched {
|
||||||
|
return fmt.Errorf("field %q must be one of: %s", field.Name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudValidateNumber aplica min y max a un valor numerico.
|
||||||
|
func crudValidateNumber(field CRUDField, f float64) error {
|
||||||
|
if v, ok := field.Validations["min"]; ok {
|
||||||
|
min, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err == nil && f < min {
|
||||||
|
return fmt.Errorf("field %q must be >= %s", field.Name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if v, ok := field.Validations["max"]; ok {
|
||||||
|
max, err := strconv.ParseFloat(v, 64)
|
||||||
|
if err == nil && f > max {
|
||||||
|
return fmt.Errorf("field %q must be <= %s", field.Name, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudCoerceInt intenta convertir un valor a int64.
|
||||||
|
func crudCoerceInt(v any) (int64, error) {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case int:
|
||||||
|
return int64(n), nil
|
||||||
|
case int64:
|
||||||
|
return n, nil
|
||||||
|
case float64:
|
||||||
|
if n != float64(int64(n)) {
|
||||||
|
return 0, fmt.Errorf("not an integer")
|
||||||
|
}
|
||||||
|
return int64(n), nil
|
||||||
|
case string:
|
||||||
|
return strconv.ParseInt(n, 10, 64)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("not an integer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// crudCoerceFloat intenta convertir un valor a float64.
|
||||||
|
func crudCoerceFloat(v any) (float64, error) {
|
||||||
|
switch n := v.(type) {
|
||||||
|
case int:
|
||||||
|
return float64(n), nil
|
||||||
|
case int64:
|
||||||
|
return float64(n), nil
|
||||||
|
case float64:
|
||||||
|
return n, nil
|
||||||
|
case string:
|
||||||
|
return strconv.ParseFloat(n, 64)
|
||||||
|
}
|
||||||
|
return 0, fmt.Errorf("not a number")
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
package infra
|
||||||
|
|
||||||
|
// CRUDResource define un recurso CRUD completo para generar handlers HTTP.
|
||||||
|
// Name es el nombre singular del recurso en snake_case (ej: "project").
|
||||||
|
// Table es el nombre de la tabla SQLite asociada (ej: "projects").
|
||||||
|
// Fields son las columnas del recurso sin contar id, created_at, updated_at y deleted_at.
|
||||||
|
// SoftDelete si es true, el handler delete hace UPDATE deleted_at en vez de DELETE real.
|
||||||
|
type CRUDResource struct {
|
||||||
|
Name string // nombre del recurso (singular, snake_case)
|
||||||
|
Table string // nombre de la tabla SQLite
|
||||||
|
Fields []CRUDField // campos del recurso (sin id ni timestamps)
|
||||||
|
SoftDelete bool // si true, usa deleted_at en vez de DELETE
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUDField define un campo de un recurso CRUD.
|
||||||
|
// Type debe ser uno de: TEXT, INTEGER, REAL, BLOB.
|
||||||
|
// Required fuerza NOT NULL en la tabla y validacion en create.
|
||||||
|
// Unique anade un UNIQUE constraint en la tabla.
|
||||||
|
// Default es el valor SQL por defecto (vacio = sin default).
|
||||||
|
// Validations define reglas de validacion: min_length, max_length, pattern, min, max, enum.
|
||||||
|
type CRUDField struct {
|
||||||
|
Name string // nombre del campo (snake_case)
|
||||||
|
Type string // tipo SQLite: TEXT, INTEGER, REAL, BLOB
|
||||||
|
Required bool // NOT NULL + validacion en create
|
||||||
|
Unique bool // UNIQUE constraint
|
||||||
|
Default string // valor por defecto en CREATE TABLE
|
||||||
|
Validations map[string]string // reglas: min_length, max_length, pattern, min, max, enum
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUDListParams agrupa los parametros de paginacion, orden y filtro del endpoint list.
|
||||||
|
// Page es 1-based (default 1). PerPage tiene default 20 y max 100.
|
||||||
|
// SortBy es el nombre del campo por el que ordenar. SortDir es "asc" o "desc".
|
||||||
|
// Filters contiene pares campo -> valor para filtros exactos en WHERE.
|
||||||
|
type CRUDListParams struct {
|
||||||
|
Page int // pagina actual (1-based, default 1)
|
||||||
|
PerPage int // items por pagina (default 20, max 100)
|
||||||
|
SortBy string // campo por el que ordenar
|
||||||
|
SortDir string // "asc" o "desc"
|
||||||
|
Filters map[string]string // campo -> valor para WHERE exacto
|
||||||
|
}
|
||||||
|
|
||||||
|
// CRUDListResult resultado paginado de una lista CRUD, serializable a JSON.
|
||||||
|
type CRUDListResult struct {
|
||||||
|
Items []map[string]any `json:"items"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PerPage int `json:"per_page"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
---
|
||||||
|
name: CRUDField
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
algebraic: product
|
||||||
|
definition: |
|
||||||
|
type CRUDField struct {
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
Required bool
|
||||||
|
Unique bool
|
||||||
|
Default string
|
||||||
|
Validations map[string]string
|
||||||
|
}
|
||||||
|
description: "Define un campo de un recurso CRUD con su tipo SQLite, constraints y validaciones. Se agrega al slice Fields de CRUDResource."
|
||||||
|
tags: [crud, field, sqlite, validation, rest, infra]
|
||||||
|
uses_types: []
|
||||||
|
file_path: "functions/infra/crud_resource.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
field := CRUDField{
|
||||||
|
Name: "name",
|
||||||
|
Type: "TEXT",
|
||||||
|
Required: true,
|
||||||
|
Unique: true,
|
||||||
|
Validations: map[string]string{
|
||||||
|
"min_length": "1",
|
||||||
|
"max_length": "100",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Tipo producto. Type debe ser uno de: TEXT, INTEGER, REAL, BLOB. Default es SQL literal (por ejemplo "'active'" o "0"). Validations es un mapa generico con claves: min_length, max_length, pattern (TEXT), min, max (INTEGER/REAL), enum ("a,b,c" para TEXT). Las validaciones se evaluan en Go antes de cada INSERT/UPDATE.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: CRUDListParams
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
algebraic: product
|
||||||
|
definition: |
|
||||||
|
type CRUDListParams struct {
|
||||||
|
Page int
|
||||||
|
PerPage int
|
||||||
|
SortBy string
|
||||||
|
SortDir string
|
||||||
|
Filters map[string]string
|
||||||
|
}
|
||||||
|
description: "Parametros de paginacion, orden y filtrado de un endpoint list CRUD. Se extrae de los query params de la request HTTP."
|
||||||
|
tags: [crud, list, pagination, filter, sort, infra]
|
||||||
|
uses_types: []
|
||||||
|
file_path: "functions/infra/crud_resource.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
params := CRUDListParams{
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 20,
|
||||||
|
SortBy: "created_at",
|
||||||
|
SortDir: "desc",
|
||||||
|
Filters: map[string]string{"status": "active"},
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Tipo producto. Page es 1-based con default 1. PerPage tiene default 20 y se satura a 100. SortDir solo acepta "asc" o "desc" (default "desc"). Filters usa igualdad exacta en WHERE — los campos se validan contra la definicion del recurso para evitar SQL injection.
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
---
|
||||||
|
name: CRUDListResult
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
algebraic: product
|
||||||
|
definition: |
|
||||||
|
type CRUDListResult struct {
|
||||||
|
Items []map[string]any `json:"items"`
|
||||||
|
Total int `json:"total"`
|
||||||
|
Page int `json:"page"`
|
||||||
|
PerPage int `json:"per_page"`
|
||||||
|
TotalPages int `json:"total_pages"`
|
||||||
|
}
|
||||||
|
description: "Resultado paginado de un endpoint list CRUD. Incluye los items de la pagina y metadatos de paginacion para el cliente."
|
||||||
|
tags: [crud, list, pagination, result, infra]
|
||||||
|
uses_types: []
|
||||||
|
file_path: "functions/infra/crud_resource.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
result := CRUDListResult{
|
||||||
|
Items: []map[string]any{{"id": "a", "name": "X"}},
|
||||||
|
Total: 42,
|
||||||
|
Page: 1,
|
||||||
|
PerPage: 20,
|
||||||
|
TotalPages: 3,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Tipo producto. Items es la lista de registros de la pagina actual serializados como map[string]any (schema dinamico). Total es el conteo global sin paginacion. TotalPages = ceil(Total / PerPage). Se serializa directamente como JSON con HTTPJSONResponse.
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
---
|
||||||
|
name: CRUDResource
|
||||||
|
lang: go
|
||||||
|
domain: infra
|
||||||
|
version: "1.0.0"
|
||||||
|
algebraic: product
|
||||||
|
definition: |
|
||||||
|
type CRUDResource struct {
|
||||||
|
Name string
|
||||||
|
Table string
|
||||||
|
Fields []CRUDField
|
||||||
|
SoftDelete bool
|
||||||
|
}
|
||||||
|
description: "Define un recurso CRUD completo (nombre, tabla, campos y modo de borrado) para generar handlers HTTP y SQL DDL. Se usa como input de crud_generate_table_sql y crud_generate_handlers."
|
||||||
|
tags: [crud, resource, http, sqlite, rest, infra]
|
||||||
|
uses_types: [CRUDField_go_infra]
|
||||||
|
file_path: "functions/infra/crud_resource.go"
|
||||||
|
---
|
||||||
|
|
||||||
|
## Ejemplo
|
||||||
|
|
||||||
|
```go
|
||||||
|
res := CRUDResource{
|
||||||
|
Name: "project",
|
||||||
|
Table: "projects",
|
||||||
|
Fields: []CRUDField{
|
||||||
|
{Name: "name", Type: "TEXT", Required: true, Unique: true},
|
||||||
|
{Name: "description", Type: "TEXT"},
|
||||||
|
},
|
||||||
|
SoftDelete: false,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
|
||||||
|
Tipo producto. Name es el singular en snake_case ("project"), Table es el plural SQL ("projects"). Fields no incluye id ni timestamps: esos los gestiona el generador. Si SoftDelete es true, la tabla tendra una columna deleted_at TEXT y el handler delete hara UPDATE en vez de DELETE.
|
||||||
Reference in New Issue
Block a user