fad4006f60
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
16 KiB
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_bodyy se registran viahttp_router. - 0015 (DB Migrations) — la tabla generada por
crud_generate_table_sqlse 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) ycrud_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.HandlerFunclistas para usar. - Especifico para SQLite (usa
database/sqlcon 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
CRUDListResultcomo JSON - Si
soft_delete, agregaWHERE deleted_at IS NULLautomaticamente
Get (GET /basePath/{id}):
- Genera
SELECT * FROM table WHERE id = ? - 404 si no existe (o si
soft_deletey tienedeleted_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 paracreated_atyupdated_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,CRUDListResultenfunctions/infra/con.mdentypes/infra/ - 1.2
crud_define_resource— construyeCRUDResourcevalidando que haya al menos un campo y que los tipos sean validos (TEXT, INTEGER, REAL, BLOB) - 1.3
crud_generate_table_sql— genera DDLCREATE TABLE IF NOT EXISTScon las constraints derivadas de los campos
Fase 2: Handlers individuales
- 2.1
crud_list_handler— parsea query params aCRUDListParams, construye SQL dinamico con paginacion y filtros, retornaCRUDListResult - 2.2
crud_get_handler— busca por ID, escanea columnas dinamicamente amap[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, retornamap[string]http.HandlerFunccon keys "list", "get", "create", "update", "delete" - 3.2
crud_register_routes— registra enhttp.ServeMuxlas rutasGET /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 indexy verificar confn showque 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_infragenera 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]anyen vez de structs tipados: Como los campos se definen en runtime, no es posible usar structs Go tipados. Toda la serializacion pasa pormap[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 KEYcon 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_responseyhttp_error_responsede 0009. Si esas firmas cambian, hay que actualizar. Mitigado: las funciones de 0009 son primitivas estables con firmas simples.