chore: auto-commit (57 archivos)

- frontend/functions/core/format_datetime_short.md
- frontend/functions/core/format_datetime_short.test.ts
- frontend/functions/core/format_datetime_short.ts
- frontend/functions/core/format_duration.md
- frontend/functions/core/format_duration.test.ts
- frontend/functions/core/format_duration.ts
- frontend/functions/core/month_grid.md
- frontend/functions/core/month_grid.test.ts
- frontend/functions/core/month_grid.ts
- frontend/functions/core/string_hash_palette.md
- ...

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-09 03:41:58 +02:00
parent 4d5a5bd3ea
commit 8618aa1be3
58 changed files with 2923 additions and 0 deletions
@@ -0,0 +1,17 @@
package infra
import "net/http"
// SessionCookieClear invalidates the named session cookie by setting
// MaxAge=-1. The browser removes the cookie immediately on receipt.
// It does not return an error because http.SetCookie never fails at runtime.
func SessionCookieClear(w http.ResponseWriter, name string) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: "",
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: -1,
})
}
@@ -0,0 +1,49 @@
---
name: http_session_cookie_clear
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SessionCookieClear(w http.ResponseWriter, name string)"
description: "Invalida la cookie de sesion en el browser fijando MaxAge=-1. Path='/', HttpOnly=true, SameSite=Lax. No retorna error porque http.SetCookie no falla en runtime."
tags: [http, session, cookie, auth, logout, response]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["net/http"]
params:
- name: w
desc: "ResponseWriter donde se escribe el header Set-Cookie"
- name: name
desc: "nombre de la cookie a invalidar (debe coincidir con el nombre usado al crearla)"
output: "escribe el header Set-Cookie con MaxAge=-1 en w; sin valor de retorno"
tested: true
tests:
- "cookie clear setea MaxAge negativo"
- "cookie clear valor es vacio"
- "header Set-Cookie contiene HttpOnly y SameSite=Lax"
test_file_path: "functions/infra/http_session_cookie_clear_test.go"
file_path: "functions/infra/http_session_cookie_clear.go"
---
## Ejemplo
```go
func handleLogout(db *DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
token := infra.SessionTokenExtract(r, "my_session")
if token != "" {
_ = infra.SessionDelete(db.conn, token)
}
infra.SessionCookieClear(w, "my_session")
w.WriteHeader(http.StatusNoContent)
}
}
```
## Notas
Extraido de apps/kanban/backend/auth.go. MaxAge=-1 hace que el browser elimine la cookie inmediatamente independientemente de la fecha Expires original. La funcion no retorna error porque `http.SetCookie` escribe directamente en los headers y nunca falla. Complemento de `http_session_cookie_set_go_infra`.
@@ -0,0 +1,50 @@
package infra
import (
"net/http/httptest"
"strings"
"testing"
)
func TestSessionCookieClear(t *testing.T) {
t.Run("cookie clear setea MaxAge negativo", func(t *testing.T) {
w := httptest.NewRecorder()
SessionCookieClear(w, "my_session")
cookies := w.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("expected 1 cookie, got %d", len(cookies))
}
c := cookies[0]
if c.Name != "my_session" {
t.Errorf("Name: got %q, want %q", c.Name, "my_session")
}
if c.MaxAge >= 0 {
t.Errorf("MaxAge: got %d, want negative", c.MaxAge)
}
})
t.Run("cookie clear valor es vacio", func(t *testing.T) {
w := httptest.NewRecorder()
SessionCookieClear(w, "sess")
cookies := w.Result().Cookies()
if len(cookies) == 0 {
t.Fatal("no cookie set")
}
if cookies[0].Value != "" {
t.Errorf("Value: got %q, want empty", cookies[0].Value)
}
})
t.Run("header Set-Cookie contiene HttpOnly y SameSite=Lax", func(t *testing.T) {
w := httptest.NewRecorder()
SessionCookieClear(w, "sess")
header := w.Header().Get("Set-Cookie")
if !strings.Contains(header, "HttpOnly") {
t.Errorf("Set-Cookie header missing HttpOnly: %s", header)
}
if !strings.Contains(header, "SameSite=Lax") {
t.Errorf("Set-Cookie header missing SameSite=Lax: %s", header)
}
})
}
@@ -0,0 +1,21 @@
package infra
import (
"net/http"
"time"
)
// SessionCookieSet writes a session cookie to the response.
// The cookie is HttpOnly, Path="/", SameSite=Lax and expires at the
// Unix timestamp expiresAt (seconds). It does not return an error
// because http.SetCookie never fails at runtime.
func SessionCookieSet(w http.ResponseWriter, name, token string, expiresAt int64) {
http.SetCookie(w, &http.Cookie{
Name: name,
Value: token,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
Expires: time.Unix(expiresAt, 0),
})
}
@@ -0,0 +1,48 @@
---
name: http_session_cookie_set
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func SessionCookieSet(w http.ResponseWriter, name, token string, expiresAt int64)"
description: "Escribe una cookie de sesion HttpOnly en la respuesta HTTP. Path='/', SameSite=Lax, Expires=time.Unix(expiresAt,0). No retorna error porque http.SetCookie no falla en runtime."
tags: [http, session, cookie, auth, response]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["net/http", "time"]
params:
- name: w
desc: "ResponseWriter donde se escribe el header Set-Cookie"
- name: name
desc: "nombre de la cookie (p.ej. 'kanban_session')"
- name: token
desc: "valor del token de sesion"
- name: expiresAt
desc: "timestamp Unix (segundos) de expiracion de la cookie"
output: "escribe el header Set-Cookie en w; sin valor de retorno"
tested: true
tests:
- "cookie set con nombre y token correctos"
- "header Set-Cookie contiene HttpOnly y SameSite=Lax"
test_file_path: "functions/infra/http_session_cookie_set_test.go"
file_path: "functions/infra/http_session_cookie_set.go"
---
## Ejemplo
```go
sess, err := infra.SessionCreate(db, userID, 7*24*time.Hour, nil)
if err != nil {
http.Error(w, "internal error", 500)
return
}
infra.SessionCookieSet(w, "my_session", sess.Token, sess.ExpiresAt)
```
## Notas
Extraido de apps/kanban/backend/auth.go. La funcion no retorna error porque `http.SetCookie` escribe directamente en los headers del ResponseWriter y nunca falla. El campo `error_type` se omite porque la firma no tiene retorno de error — hay precedente en el registry (componentes C++ y otros helpers HTTP impuros sin error_type).
@@ -0,0 +1,46 @@
package infra
import (
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestSessionCookieSet(t *testing.T) {
t.Run("cookie set con nombre y token correctos", func(t *testing.T) {
w := httptest.NewRecorder()
expires := time.Now().Add(24 * time.Hour).Unix()
SessionCookieSet(w, "my_session", "tok_abc", expires)
cookies := w.Result().Cookies()
if len(cookies) != 1 {
t.Fatalf("expected 1 cookie, got %d", len(cookies))
}
c := cookies[0]
if c.Name != "my_session" {
t.Errorf("Name: got %q, want %q", c.Name, "my_session")
}
if c.Value != "tok_abc" {
t.Errorf("Value: got %q, want %q", c.Value, "tok_abc")
}
if c.Path != "/" {
t.Errorf("Path: got %q, want %q", c.Path, "/")
}
if !c.HttpOnly {
t.Errorf("expected HttpOnly=true")
}
})
t.Run("header Set-Cookie contiene HttpOnly y SameSite=Lax", func(t *testing.T) {
w := httptest.NewRecorder()
SessionCookieSet(w, "s", "v", time.Now().Add(time.Hour).Unix())
header := w.Header().Get("Set-Cookie")
if !strings.Contains(header, "HttpOnly") {
t.Errorf("Set-Cookie header missing HttpOnly: %s", header)
}
if !strings.Contains(header, "SameSite=Lax") {
t.Errorf("Set-Cookie header missing SameSite=Lax: %s", header)
}
})
}
@@ -0,0 +1,19 @@
package infra
import "net/http"
// SessionTokenExtract extracts a session token from the request.
// It checks the cookie named cookieName first; if present and non-empty,
// that value is returned. Otherwise it checks the Authorization header
// for a "Bearer <token>" prefix and returns the token part.
// Returns "" if no token is found in either source.
func SessionTokenExtract(r *http.Request, cookieName string) string {
if c, err := r.Cookie(cookieName); err == nil && c.Value != "" {
return c.Value
}
auth := r.Header.Get("Authorization")
if len(auth) > 7 && auth[:7] == "Bearer " {
return auth[7:]
}
return ""
}
@@ -0,0 +1,51 @@
---
name: http_session_token_extract
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: pure
signature: "func SessionTokenExtract(r *http.Request, cookieName string) string"
description: "Extrae el token de sesion de un request HTTP. Comprueba primero la cookie con el nombre indicado; si no esta, parsea el header Authorization 'Bearer <token>'. Retorna cadena vacia si no hay token."
tags: [http, session, cookie, bearer, auth, token]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: ""
imports: ["net/http"]
params:
- name: r
desc: "request HTTP entrante"
- name: cookieName
desc: "nombre de la cookie de sesion a buscar (p.ej. 'kanban_session')"
output: "token extraido de la cookie o del header Authorization; cadena vacia si no hay token en ninguna fuente"
tested: true
tests:
- "cookie present retorna token de cookie"
- "bearer header retorna token de header"
- "cookie gana sobre bearer header"
- "sin token retorna cadena vacia"
test_file_path: "functions/infra/http_session_token_extract_test.go"
file_path: "functions/infra/http_session_token_extract.go"
---
## Ejemplo
```go
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := infra.SessionTokenExtract(r, "my_session")
if token == "" {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
// validate token...
next.ServeHTTP(w, r)
})
}
```
## Notas
Extraido de apps/kanban/backend/auth.go. Funcion pura: solo lee el request, no muta estado. La cookie tiene precedencia sobre el header Authorization para mantener consistencia con el comportamiento del browser (la cookie es el canal primario; el header es para clientes API que no gestionan cookies).
@@ -0,0 +1,46 @@
package infra
import (
"net/http"
"testing"
)
func TestSessionTokenExtract(t *testing.T) {
const cookieName = "test_session"
t.Run("cookie present retorna token de cookie", func(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{Name: cookieName, Value: "tok_cookie"})
got := SessionTokenExtract(r, cookieName)
if got != "tok_cookie" {
t.Errorf("got %q, want %q", got, "tok_cookie")
}
})
t.Run("bearer header retorna token de header", func(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
r.Header.Set("Authorization", "Bearer tok_bearer")
got := SessionTokenExtract(r, cookieName)
if got != "tok_bearer" {
t.Errorf("got %q, want %q", got, "tok_bearer")
}
})
t.Run("cookie gana sobre bearer header", func(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
r.AddCookie(&http.Cookie{Name: cookieName, Value: "tok_cookie"})
r.Header.Set("Authorization", "Bearer tok_bearer")
got := SessionTokenExtract(r, cookieName)
if got != "tok_cookie" {
t.Errorf("got %q, want %q (cookie should win)", got, "tok_cookie")
}
})
t.Run("sin token retorna cadena vacia", func(t *testing.T) {
r, _ := http.NewRequest("GET", "/", nil)
got := SessionTokenExtract(r, cookieName)
if got != "" {
t.Errorf("got %q, want empty string", got)
}
})
}
@@ -0,0 +1,88 @@
package infra
import (
"database/sql"
"fmt"
"io/fs"
"sort"
"strings"
)
// ApplyMigrations reads SQL files matching glob from fsys, sorts them
// lexicographically (NNN_name.sql order), and executes every statement
// found in each file against conn.
//
// If glob is empty, it defaults to "migrations/*.sql".
//
// Each statement is executed individually. Errors that look like
// idempotent SQLite errors (duplicate column, already exists) are
// silently ignored so that migrations can be replayed safely against
// a database that was partially migrated.
//
// NOTE: the internal statement parser splits on ";" at the end of a
// trimmed line. This is intentionally simple and will break if SQL
// strings contain a literal semicolon at end-of-line. Avoid using
// such patterns in migration files.
func ApplyMigrations(conn *sql.DB, fsys fs.FS, glob string) error {
if glob == "" {
glob = "migrations/*.sql"
}
files, err := fs.Glob(fsys, glob)
if err != nil {
return err
}
sort.Strings(files)
for _, f := range files {
b, err := fs.ReadFile(fsys, f)
if err != nil {
return err
}
for _, stmt := range splitSQLStatements(string(b)) {
s := strings.TrimSpace(stmt)
if s == "" {
continue
}
if _, err := conn.Exec(s); err != nil {
if isIdempotentMigrationError(err) {
continue
}
return fmt.Errorf("%s: %w", f, err)
}
}
}
return nil
}
// splitSQLStatements splits a SQL script into individual statements.
// Lines starting with "--" (comments) and blank lines are skipped.
// A statement ends when a trimmed line ends with ";".
func splitSQLStatements(s string) []string {
out := []string{}
cur := strings.Builder{}
for _, line := range strings.Split(s, "\n") {
trim := strings.TrimSpace(line)
if strings.HasPrefix(trim, "--") || trim == "" {
continue
}
cur.WriteString(line)
cur.WriteString("\n")
if strings.HasSuffix(trim, ";") {
out = append(out, cur.String())
cur.Reset()
}
}
if cur.Len() > 0 {
out = append(out, cur.String())
}
return out
}
// isIdempotentMigrationError returns true for SQLite errors that arise
// from re-applying a migration that was already applied (duplicate
// column, table/index already exists).
func isIdempotentMigrationError(err error) bool {
msg := err.Error()
return strings.Contains(msg, "duplicate column") ||
strings.Contains(msg, "already exists")
}
@@ -0,0 +1,58 @@
---
name: sqlite_apply_migrations
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func ApplyMigrations(conn *sql.DB, fsys fs.FS, glob string) error"
description: "Aplica migraciones SQL desde un fs.FS en orden lexicografico. Lee archivos con glob (default 'migrations/*.sql'), divide por sentencias y ejecuta cada una contra conn. Errores idempotentes (duplicate column, already exists) se ignoran."
tags: [database, sqlite, migration, schema, embed, fs]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "fmt", "io/fs", "sort", "strings"]
params:
- name: conn
desc: "conexion *sql.DB donde se ejecutan las migraciones"
- name: fsys
desc: "sistema de archivos con los .sql (tipicamente embed.FS del caller)"
- name: glob
desc: "patron glob para seleccionar archivos (vacio = 'migrations/*.sql')"
output: "nil si todas las migraciones se aplicaron correctamente; error del primer fallo no idempotente con nombre de archivo incluido"
tested: true
tests:
- "una migracion se aplica correctamente"
- "multiples migraciones en orden"
- "error real se propaga"
- "ALTER TABLE ADD COLUMN duplicado se ignora"
test_file_path: "functions/infra/sqlite_apply_migrations_test.go"
file_path: "functions/infra/sqlite_apply_migrations.go"
---
## Ejemplo
```go
//go:embed migrations/*.sql
var migrationsFS embed.FS
func openDB(path string) (*sql.DB, error) {
db, err := infra.SQLiteOpen(path, "")
if err != nil {
return nil, err
}
if err := infra.ApplyMigrations(db, migrationsFS, ""); err != nil {
db.Close()
return nil, fmt.Errorf("migrate: %w", err)
}
return db, nil
}
```
## Notas
Extraido de apps/kanban/backend/db.go. El parser de sentencias SQL es intencionalmente simple: separa por `;` al final de una linea (ignorando comentarios `--` y lineas vacias). Esta logica falla si el SQL contiene un `;` dentro de un string literal al final de linea — evitar ese patron en los archivos de migracion.
Los errores idempotentes (`duplicate column`, `already exists`) se ignoran para que las migraciones sean re-ejecutables contra DBs que ya tenian parte del schema. Esto permite un flujo sin tabla `_migrations` para proyectos pequenos; para proyectos con muchas migraciones y rollback, usar `migration_up_go_infra` / `migration_down_go_infra`.
@@ -0,0 +1,98 @@
package infra
import (
"database/sql"
"testing"
"testing/fstest"
_ "github.com/mattn/go-sqlite3"
)
// makeFS builds a simple in-memory FS with the given path→content pairs.
func makeFS(files map[string]string) fstest.MapFS {
m := make(fstest.MapFS, len(files))
for path, content := range files {
m[path] = &fstest.MapFile{Data: []byte(content)}
}
return m
}
func TestApplyMigrations(t *testing.T) {
t.Run("una migracion se aplica correctamente", func(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
fsys := makeFS(map[string]string{
"migrations/001_init.sql": `CREATE TABLE IF NOT EXISTS items (id INTEGER PRIMARY KEY, name TEXT);`,
})
if err := ApplyMigrations(db, fsys, ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Verify table exists.
var count int
if err := db.QueryRow(`SELECT COUNT(*) FROM sqlite_master WHERE type='table' AND name='items'`).Scan(&count); err != nil {
t.Fatal(err)
}
if count != 1 {
t.Errorf("table 'items' not created; count=%d", count)
}
})
t.Run("multiples migraciones en orden", func(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
fsys := makeFS(map[string]string{
"migrations/001_init.sql": `CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY);`,
"migrations/002_add_column.sql": `ALTER TABLE t ADD COLUMN val TEXT;`,
})
if err := ApplyMigrations(db, fsys, ""); err != nil {
t.Fatalf("unexpected error: %v", err)
}
// Insert using the new column.
if _, err := db.Exec(`INSERT INTO t (id, val) VALUES (1, 'hello')`); err != nil {
t.Fatalf("insert failed (column may be missing): %v", err)
}
})
t.Run("error real se propaga", func(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
fsys := makeFS(map[string]string{
"migrations/001_bad.sql": `THIS IS NOT VALID SQL;`,
})
if err := ApplyMigrations(db, fsys, ""); err == nil {
t.Errorf("expected error for invalid SQL, got nil")
}
})
t.Run("ALTER TABLE ADD COLUMN duplicado se ignora", func(t *testing.T) {
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatal(err)
}
defer db.Close()
setup := `CREATE TABLE IF NOT EXISTS t (id INTEGER PRIMARY KEY, val TEXT);`
if _, err := db.Exec(setup); err != nil {
t.Fatal(err)
}
// Run ALTER that would fail with "duplicate column".
fsys := makeFS(map[string]string{
"migrations/001_dup.sql": `ALTER TABLE t ADD COLUMN val TEXT;`,
})
if err := ApplyMigrations(db, fsys, ""); err != nil {
t.Errorf("duplicate column error should be ignored, got: %v", err)
}
})
}
+30
View File
@@ -0,0 +1,30 @@
package infra
import (
"database/sql"
"fmt"
)
// ColumnExists reports whether the named column exists in the given table
// by querying PRAGMA table_info. Returns false if the table does not exist.
func ColumnExists(conn *sql.DB, table, name string) (bool, error) {
rows, err := conn.Query(fmt.Sprintf("PRAGMA table_info(%s)", table))
if err != nil {
return false, err
}
defer rows.Close()
for rows.Next() {
var cid int
var colName, ctype string
var notnull int
var dflt sql.NullString
var pk int
if err := rows.Scan(&cid, &colName, &ctype, &notnull, &dflt, &pk); err != nil {
return false, err
}
if colName == name {
return true, nil
}
}
return false, rows.Err()
}
+48
View File
@@ -0,0 +1,48 @@
---
name: sqlite_column_exists
kind: function
lang: go
domain: infra
version: "1.0.0"
purity: impure
signature: "func ColumnExists(conn *sql.DB, table, name string) (bool, error)"
description: "Comprueba si una columna existe en una tabla SQLite consultando PRAGMA table_info. Retorna false sin error si la tabla no existe."
tags: [database, sqlite, schema, pragma, column, migration]
uses_functions: []
uses_types: []
returns: []
returns_optional: false
error_type: "error_go_core"
imports: ["database/sql", "fmt"]
params:
- name: conn
desc: "conexion SQLite abierta"
- name: table
desc: "nombre de la tabla a inspeccionar"
- name: name
desc: "nombre de la columna a buscar"
output: "true si la columna existe, false si no existe o la tabla no existe; error si la query falla"
tested: true
tests:
- "columna existente retorna true"
- "columna inexistente retorna false"
- "tabla inexistente retorna false sin error"
test_file_path: "functions/infra/sqlite_column_exists_test.go"
file_path: "functions/infra/sqlite_column_exists.go"
---
## Ejemplo
```go
exists, err := ColumnExists(db, "cards", "assignee_id")
if err != nil {
return err
}
if !exists {
_, err = db.Exec(`ALTER TABLE cards ADD COLUMN assignee_id TEXT`)
}
```
## Notas
Extraido de apps/kanban/backend/db.go. Util como comprobacion previa a ALTER TABLE ADD COLUMN en scripts de migracion que necesitan ser idempotentes. PRAGMA table_info retorna cero filas si la tabla no existe, por lo que la funcion retorna false sin error en ese caso.
@@ -0,0 +1,59 @@
package infra
import (
"database/sql"
"testing"
_ "github.com/mattn/go-sqlite3"
)
func openMemDB(t *testing.T) *sql.DB {
t.Helper()
db, err := sql.Open("sqlite3", ":memory:")
if err != nil {
t.Fatalf("open :memory: db: %v", err)
}
t.Cleanup(func() { db.Close() })
return db
}
func TestColumnExists(t *testing.T) {
t.Run("columna existente retorna true", func(t *testing.T) {
db := openMemDB(t)
if _, err := db.Exec(`CREATE TABLE t (id INTEGER PRIMARY KEY, name TEXT)`); err != nil {
t.Fatal(err)
}
got, err := ColumnExists(db, "t", "name")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !got {
t.Errorf("expected true for existing column 'name'")
}
})
t.Run("columna inexistente retorna false", func(t *testing.T) {
db := openMemDB(t)
if _, err := db.Exec(`CREATE TABLE t (id INTEGER PRIMARY KEY)`); err != nil {
t.Fatal(err)
}
got, err := ColumnExists(db, "t", "missing")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got {
t.Errorf("expected false for missing column")
}
})
t.Run("tabla inexistente retorna false sin error", func(t *testing.T) {
db := openMemDB(t)
got, err := ColumnExists(db, "no_such_table", "col")
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if got {
t.Errorf("expected false for non-existent table")
}
})
}