Files
fn_registry/dev/issues/completed/0021-crud-generator.md
T
2026-04-18 17:18:11 +02:00

16 KiB

0021 — CRUD Generator

Metadata

Campo Valor
ID 0021
Estado pendiente
Prioridad media
Tipo feature

Dependencias

  • 0009 (HTTP Server Foundation) — los handlers generados usan http_json_response, http_error_response, http_parse_body y se registran via http_router.
  • 0015 (DB Migrations) — la tabla generada por crud_generate_table_sql se ejecuta como migracion.

Objetivo

Generar handlers REST completos (list, get, create, update, delete) a partir de una definicion declarativa de recurso, sin escribir codigo repetitivo. El 80% de los endpoints de cualquier API son CRUD identico — solo cambian el nombre de la tabla y los campos.

Contexto

  • Ya existen wails_bind_crud_go_infra (genera bindings CRUD para Wails como string de codigo Go) y crud_page_ts_ui (layout CRUD en frontend). Pero no hay nada que genere endpoints HTTP CRUD funcionales.
  • Las apps del registry (sqlite_api, deploy_server, futuras) construyen sus handlers CRUD a mano cada vez: misma estructura de list con paginacion, mismo get por ID, mismo create con validacion, mismo update parcial, mismo delete.
  • Este issue NO genera archivos ni templates — genera handler factories en runtime. Le pasas una definicion de recurso y te devuelve http.HandlerFunc listas para usar.
  • Especifico para SQLite (usa database/sql con mattn/go-sqlite3). No abstrae multiples motores de BD.

Arquitectura

functions/infra/
  crud_define_resource.go          — NEW: construye CRUDResource a partir de nombre y campos
  crud_define_resource.md          — NEW
  crud_generate_table_sql.go       — NEW: genera CREATE TABLE a partir de CRUDResource
  crud_generate_table_sql.md       — NEW
  crud_generate_handlers.go        — NEW: genera los 5 handlers a partir de CRUDResource + *sql.DB
  crud_generate_handlers.md        — NEW
  crud_register_routes.go          — NEW: registra rutas CRUD en router
  crud_register_routes.md          — NEW
  crud_list_handler.go             — NEW: handler generico list con paginacion/filtro/sort
  crud_list_handler.md             — NEW
  crud_get_handler.go              — NEW: handler get por ID
  crud_get_handler.md              — NEW
  crud_create_handler.go           — NEW: handler create con validacion
  crud_create_handler.md           — NEW
  crud_update_handler.go           — NEW: handler update parcial por ID
  crud_update_handler.md           — NEW
  crud_delete_handler.go           — NEW: handler delete por ID
  crud_delete_handler.md           — NEW

types/infra/
  crud_resource.md                 — NEW: metadata del tipo CRUDResource
  crud_field.md                    — NEW: metadata del tipo CRUDField
  crud_list_params.md              — NEW: metadata del tipo CRUDListParams
  crud_list_result.md              — NEW: metadata del tipo CRUDListResult

Patron pure core / impure shell

  • Pure: crud_define_resource (construye struct), crud_generate_table_sql (string SQL), crud_generate_handlers (retorna handlers sin ejecutar I/O, pero los handlers en si hacen I/O al invocarse — la funcion factory es pure, el handler resultante es impure)
  • Impure: crud_register_routes (muta router), crud_list_handler, crud_get_handler, crud_create_handler, crud_update_handler, crud_delete_handler (todos hacen I/O contra SQLite y HTTP)

Nota sobre crud_generate_handlers: La funcion que genera los handlers es pura (recibe definicion + db, retorna funciones). Pero los handlers retornados son closures impuros que leen/escriben BD y HTTP. Se clasifica como pure porque la funcion en si no hace I/O — solo construye closures.

Diseno

Tipos

// CRUDResource define un recurso CRUD completo
type CRUDResource struct {
    Name       string      // nombre del recurso (singular, snake_case: "project")
    Table      string      // nombre de la tabla SQLite ("projects")
    Fields     []CRUDField // campos del recurso (sin ID ni timestamps)
    SoftDelete bool        // si true, usa deleted_at en vez de DELETE real
}

// CRUDField define un campo del recurso
type CRUDField struct {
    Name        string            // nombre del campo (snake_case: "display_name")
    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 (vacio = sin default)
    Validations map[string]string // reglas: "min_length":"3", "max_length":"255", "pattern":"^[a-z]+"
}

// CRUDListParams parametros de paginacion, orden y filtro
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 (default "created_at")
    SortDir string            // "asc" o "desc" (default "desc")
    Filters map[string]string // campo -> valor para WHERE exacto
}

// CRUDListResult resultado paginado
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"`
}

Funciones

Funcion Purity Firma (simplificada)
crud_define_resource pure (name string, table string, fields []CRUDField, softDelete bool) CRUDResource
crud_generate_table_sql pure (res CRUDResource) string
crud_generate_handlers pure (res CRUDResource, db *sql.DB) map[string]http.HandlerFunc
crud_register_routes impure (mux *http.ServeMux, basePath string, res CRUDResource, db *sql.DB)
crud_list_handler impure (res CRUDResource, db *sql.DB) http.HandlerFunc
crud_get_handler impure (res CRUDResource, db *sql.DB) http.HandlerFunc
crud_create_handler impure (res CRUDResource, db *sql.DB) http.HandlerFunc
crud_update_handler impure (res CRUDResource, db *sql.DB) http.HandlerFunc
crud_delete_handler impure (res CRUDResource, db *sql.DB) http.HandlerFunc

Comportamiento de cada handler

List (GET /basePath):

  • Query params: page, per_page, sort_by, sort_dir, filter_{campo}={valor}
  • Genera SELECT * FROM table WHERE ... ORDER BY ... LIMIT ... OFFSET ...
  • Cuenta total con SELECT COUNT(*) FROM table WHERE ...
  • Retorna CRUDListResult como JSON
  • Si soft_delete, agrega WHERE deleted_at IS NULL automaticamente

Get (GET /basePath/{id}):

  • Genera SELECT * FROM table WHERE id = ?
  • 404 si no existe (o si soft_delete y tiene deleted_at)
  • Retorna el registro como JSON

Create (POST /basePath):

  • Parsea body JSON con http_parse_body
  • Valida campos required y validaciones de cada campo
  • Genera UUID para id, timestamp para created_at y updated_at
  • INSERT INTO table (id, field1, ..., created_at, updated_at) VALUES (?, ?, ..., ?, ?)
  • Retorna 201 con el registro creado

Update (PUT /basePath/{id}):

  • Parsea body JSON — solo campos presentes se actualizan (partial update)
  • Valida campos enviados contra sus reglas
  • UPDATE table SET field1=?, updated_at=? WHERE id=?
  • 404 si no existe
  • Retorna el registro actualizado

Delete (DELETE /basePath/{id}):

  • Si soft_delete: UPDATE table SET deleted_at=? WHERE id=?
  • Si no: DELETE FROM table WHERE id=?
  • 404 si no existe
  • Retorna 204 sin body

SQL generado por crud_generate_table_sql

-- Para un recurso "projects" con campos name (TEXT, required, unique) y description (TEXT)
CREATE TABLE IF NOT EXISTS projects (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    description TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL
);

-- Si soft_delete:
CREATE TABLE IF NOT EXISTS projects (
    id TEXT PRIMARY KEY,
    name TEXT NOT NULL UNIQUE,
    description TEXT,
    created_at TEXT NOT NULL,
    updated_at TEXT NOT NULL,
    deleted_at TEXT
);

Validaciones soportadas

Clave Aplica a Ejemplo
min_length TEXT "3" — minimo 3 caracteres
max_length TEXT "255" — maximo 255 caracteres
pattern TEXT "^[a-z_]+$" — regex match
min INTEGER, REAL "0" — valor minimo
max INTEGER, REAL "1000" — valor maximo
enum TEXT "active,inactive,archived" — valores permitidos

Tareas

Fase 1: Tipos y definicion

  • 1.1 Crear tipos CRUDResource, CRUDField, CRUDListParams, CRUDListResult en functions/infra/ con .md en types/infra/
  • 1.2 crud_define_resource — construye CRUDResource validando que haya al menos un campo y que los tipos sean validos (TEXT, INTEGER, REAL, BLOB)
  • 1.3 crud_generate_table_sql — genera DDL CREATE TABLE IF NOT EXISTS con las constraints derivadas de los campos

Fase 2: Handlers individuales

  • 2.1 crud_list_handler — parsea query params a CRUDListParams, construye SQL dinamico con paginacion y filtros, retorna CRUDListResult
  • 2.2 crud_get_handler — busca por ID, escanea columnas dinamicamente a map[string]any, responde 404 si no existe
  • 2.3 crud_create_handler — valida input contra definicion, genera UUID, inserta, retorna 201
  • 2.4 crud_update_handler — partial update con solo los campos enviados, valida los presentes, 404 si no existe
  • 2.5 crud_delete_handler — hard/soft delete segun config, 404 si no existe, retorna 204

Fase 3: Composicion y tests

  • 3.1 crud_generate_handlers — llama a los 5 handlers individuales, retorna map[string]http.HandlerFunc con keys "list", "get", "create", "update", "delete"
  • 3.2 crud_register_routes — registra en http.ServeMux las rutas GET /base, GET /base/{id}, POST /base, PUT /base/{id}, DELETE /base/{id}
  • 3.3 Tests con httptest.NewServer + SQLite in-memory (:memory:) para cada handler
  • 3.4 Test de integracion: define recurso, genera tabla, registra rutas, CRUD completo via HTTP
  • 3.5 fn index y verificar con fn show que todas las funciones y tipos aparecen

Ejemplo de uso

// 1. Definir el recurso
resource := infra.CRUDDefineResource("project", "projects", []infra.CRUDField{
    {Name: "name", Type: "TEXT", Required: true, Unique: true,
        Validations: map[string]string{"min_length": "1", "max_length": "100"}},
    {Name: "description", Type: "TEXT"},
    {Name: "status", Type: "TEXT", Required: true, Default: "'active'",
        Validations: map[string]string{"enum": "active,archived,deleted"}},
    {Name: "priority", Type: "INTEGER", Default: "0",
        Validations: map[string]string{"min": "0", "max": "10"}},
}, false) // soft_delete = false

// 2. Crear la tabla
ddl := infra.CRUDGenerateTableSQL(resource)
db.Exec(ddl)

// 3. Registrar rutas (una linea)
mux := http.NewServeMux()
infra.CRUDRegisterRoutes(mux, "/api/projects", resource, db)

// Listo. Endpoints disponibles:
// GET    /api/projects              — list con paginacion
// GET    /api/projects/{id}         — get por ID
// POST   /api/projects              — create
// PUT    /api/projects/{id}         — update parcial
// DELETE /api/projects/{id}         — delete

// 4. Multiples recursos en la misma API
infra.CRUDRegisterRoutes(mux, "/api/users", userResource, db)
infra.CRUDRegisterRoutes(mux, "/api/tasks", taskResource, db)

// 5. Componer con middlewares de 0009
middleware := infra.HttpMiddlewareChain(
    infra.HttpCorsMiddleware([]string{"*"}, []string{"GET", "POST", "PUT", "DELETE"}),
    infra.HttpLoggerMiddleware(os.Stdout),
)

ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
defer cancel()

infra.HttpServe(":8080", middleware(mux), ctx)
# Uso desde curl:

# List con paginacion y filtros
curl "localhost:8080/api/projects?page=1&per_page=10&sort_by=name&sort_dir=asc&filter_status=active"

# Get
curl "localhost:8080/api/projects/abc-123"

# Create
curl -X POST localhost:8080/api/projects \
  -H 'Content-Type: application/json' \
  -d '{"name":"mi-proyecto","description":"Desc","status":"active","priority":5}'

# Update parcial (solo cambia description)
curl -X PUT localhost:8080/api/projects/abc-123 \
  -H 'Content-Type: application/json' \
  -d '{"description":"Nueva descripcion"}'

# Delete
curl -X DELETE localhost:8080/api/projects/abc-123

Decisiones de diseno

  • Runtime handler factories, no code generation: wails_bind_crud_go_infra genera codigo como string para Wails. Este issue toma el camino opuesto: las funciones son los handlers, no generan texto. Esto evita templates, archivos generados y el ciclo generate-compile. El tradeoff es que los handlers son genericos (map[string]any) en vez de tipados, pero para APIs REST sobre SQLite es aceptable.
  • map[string]any en vez de structs tipados: Como los campos se definen en runtime, no es posible usar structs Go tipados. Toda la serializacion pasa por map[string]any. Esto es idiomatico para APIs JSON + SQLite donde el schema es dinamico.
  • SQLite especifico, no multi-motor: Simplifica enormemente el SQL generado (TEXT para timestamps, sin SERIAL, sin esquemas). Si en el futuro se necesita Postgres, se crea una variante separada.
  • Validacion en Go, no en SQLite: Las constraints de SQLite (CHECK, NOT NULL) son la ultima linea de defensa. La validacion principal ocurre en el handler antes del INSERT/UPDATE, con mensajes de error descriptivos para el cliente.
  • UUID como ID: Todos los recursos usan id TEXT PRIMARY KEY con UUID generado server-side. No IDs autoincrement — evita problemas de concurrencia y es mas portable.
  • Timestamps como TEXT ISO 8601: Consistente con como registry.db y operations.db ya almacenan timestamps.
  • Partial update en PUT: Normalmente PATCH es para partial update y PUT para replace completo. Aqui se usa PUT con partial update por simplicidad — solo los campos presentes en el JSON se actualizan. Es pragmatico para APIs internas.

Relacion con funciones existentes

Funcion existente Relacion
wails_bind_crud_go_infra Genera codigo Go como string para desktop (Wails). Este issue genera handlers HTTP en runtime para REST APIs. Complementarios, no solapados.
crud_page_ts_ui Frontend CRUD layout en React. Los handlers generados aqui serian el backend que esa pagina consume. Stack completo: crud_define_resource (definicion) + handlers (backend) + crud_page (frontend).

Riesgos

  • SQL injection en filtros: Los filtros se pasan como query params y se inyectan en WHERE. Mitigado validando que el campo exista en la definicion del recurso y usando siempre ? placeholders — nunca interpolacion de strings en SQL.
  • Performance en tablas grandes: El SELECT COUNT(*) para paginacion se ejecuta en cada list request. Para tablas con millones de filas esto es lento. Mitigado: las apps del registry manejan miles de registros, no millones. Si se necesita, se anade cache de count como mejora futura.
  • Scope creep hacia un ORM: Hay tentacion de agregar relaciones, joins, nested resources, hooks before/after. Mitigado limitando el scope a CRUD plano de una tabla. Relaciones y logica de negocio van en handlers custom, no en el generador.
  • Colision con 0009 si cambia la API: Los handlers usan http_json_response y http_error_response de 0009. Si esas firmas cambian, hay que actualizar. Mitigado: las funciones de 0009 son primitivas estables con firmas simples.