Files

16 KiB

id, title, status, type, domain, scope, priority, depends, blocks, related, created, updated, tags
id title status type domain scope priority depends blocks related created updated tags
0021 CRUD Generator completado feature
multi-app media
2026-05-17 2026-05-17

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.