Promptly 02 - Listar prompts


Sesión 2

Punto de partida

Objetivos

Se mantienen tal cual.

Tareas

Los 3 elementos previos apuntan a que la app debe persistir los datos y hacerlo de forma segura (que los prompts sean de cada usuario y privados por defecto). Lo que es lo mismo, que un usuario liste y vea solo sus prompts y otro usuario los suyos. Esto se consigue de 2 formas tradicionalmente: una base de datos propia (cuando el usuario está por ejemplo en un entorno dedicado, o bien la plataforma crea una para cada cuenta) o la misma base de datos / tablas pero un sistema robusto de RLS (Row Level Security) que aisle los datos de cada cliente. Habitualmente lo 2º es más sencillo y eficiente en términos de costes, así que iremos por ese camino.

🔝Es claro que los requisitos funcionales (gestionar los prompts), van acompañados necesariamente de requisitos de otro tipo. En un enfoque de mínimos, comenzaremos por una seguridad / usabilidad básica... pero más adelante, si la cosa crece, habría que revisar otros como el rendimiento del sistema o su mantenibilidad.

Acciones

Persistencia

Seguramente la estructura evolucionará, pero en una primera versión, y con ayuda de ChatGPT, esta es la estructura propuesta, con una sola tabla

CREATE TABLE prompts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    description TEXT,
    tags TEXT[],
    is_public BOOLEAN DEFAULT FALSE,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);

En Supabase no hay una tabla public.users, para hacer la FK. En la sección Authentication > Users muestra que existe sin embargo, con los campos UID, Display name, Email, Provider type, etc. Esta tabla se encuentra en realidad bajo auth.users, así que omitiremos el FK explícito, pero habrá que tenerlo en cuenta para manejar la integridad de los datos (con algún tipo de borrado manual en cascada). Reemplazamos pues: user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE con un simple user_id UUID NOT NULL

Usaremos RLS. Inicialmente Supabase (mediante un asistente con AI por cierto para los SQL muy útil), sugiere unas reglas que tienen bastante sentido, para limitar, via CREATE POLICY las operaciones a nivel de SELECT, INSERT, UPDATE, DELETE. Si más adelante incluimos el is_public, crearemos otra policy para ver los prompts compartidos

-- RLS basica
CREATE POLICY "Authenticated users can view their own prompts" 
ON public.prompts 
FOR SELECT 
TO authenticated 
USING ((select auth.uid()) = user_id);

CREATE POLICY "Authenticated users can insert prompts" 
ON public.prompts 
FOR INSERT 
TO authenticated 
WITH CHECK (true);

CREATE POLICY "Authenticated users can update their own prompts" 
ON public.prompts 
FOR UPDATE 
TO authenticated 
USING ((select auth.uid()) = user_id) 
WITH CHECK (true);

CREATE POLICY "Authenticated users can delete their own prompts" 
ON public.prompts 
FOR DELETE 
TO authenticated 
USING ((select auth.uid()) = user_id);

🔝Supabase tiene una UI para definirlo, pero el SQL Editor es más flexible para crearlos.

Hay librerías custom para generar datos 'semilla' de prueba, y muchas formas de hacerlo, pero lo más sencillo es pasarle el create table a un LLM cualquiera y pedirle N filas, con las restricciones que apliquen, por ejemplo:

sql
CREATE TABLE prompts (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    user_id UUID NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    title TEXT NOT NULL,
    content TEXT NOT NULL,
    description TEXT,
    tags TEXT[],
    created_at TIMESTAMP WITH TIME ZONE DEFAULT now(),
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT now()
);


Generame 10 ejemplos de prompt para la tabla previa, para el usuario con id='18e3e69f-9723-462f-8e5f-edfca2e1232b', teniendo en cuenta que estoy utilizando Supabase, sobre un PostgreSQL 15.8

🔝Por cierto, este sería un buen ejemplo de un prompt a almacenar en la plataforma, como un ejemplo de generador de mocks de postgres. ChatGPT genera un snippet compacto, con generate_series, algo como:

-- Enable pgcrypto if not already enabled
CREATE EXTENSION IF NOT EXISTS pgcrypto;

-- Insert 10 random prompts
INSERT INTO prompts (user_id, title, content, description, tags)
SELECT
  'el-id-del-usuario-que-toque'::uuid,
  'Prompt #' || gs AS title,
  'Escribe un texto creativo sobre ' || 
    (ARRAY[
      'el futuro de la inteligencia artificial',
      'una conversación entre dos árboles',
      'una ciudad sumergida',
      'el último pensamiento de un robot',
      'un experimento fallido de viaje en el tiempo'
    ])[floor(random() * 5 + 1)] || '.' AS content,
  'Prompt auto-generado para hacer pruebas. Ref: ' || md5text AS description,
  ARRAY[
    (ARRAY['creatividad', 'tecnologia', 'ficcion', 'historia', 'misterio'])[floor(random() * 5 + 1)],
    (ARRAY['filosofia', 'futuro', 'ia', 'naturaleza', 'distopia'])[floor(random() * 5 + 1)]
  ] AS tags
FROM generate_series(1, 10) AS gs;

🔝Y esto se ejecuta sin problema en Supabase. Bien Supabase, bien 💌

Se puede tal vez agregar un tag extra, para borrarlos más adelante. Prompt en el propio Supabase, que tiene un interfaz en modo texto llamada "Assistant": Actualiza todo registro en "prompts" y agrega un tag "random_mock", respetando las tags actuales... mmm parece que ahora mismo no funciona bien (dicen que está en Alpha), vamos a ChatGPT y genera un SQL que funciona bien:

UPDATE prompts
SET tags = array_append(tags, 'random_mock')
WHERE NOT ('random_mock' = ANY(tags));

UI básica para prompts

Es muy sencillo con el cliente server-side para Supabase, las instrucciones en la template y un poco de ayuda de Copilot en VSCode para las clases de TailwindCSS, el resultado:

promptly_02_prompts_list.png

Lo mínimo es eliminar algunos botones y referencias a la template. Aun quedará dead-code y cosas a limpiar en el futuro, pero sirve para avanzar. Vamos a usar como "nombre del proyecto": Promptly

Se necesita un usuario logado, y otra página, por lo que es un buen momento para considerar por primer vez las "rutas" de forma más general, y los caminos que seguirán los usuarios.

Rutas

Hasta ahora tenemos estas rutas (cada una correspondiendo con un page.tsx de NextJS):
Actual

  1. /: la "home", de momento con el contenido de la plantilla y solo para usuarios no logados. Seguramente modificaremos la lógica, pues idealmente los usuarios podrán obtener valor de la app, aun sin registrarse (pero también tras hacerlo: ejemplo últimos prompts subidos, búsqueda por tags, votos, etc.).
  2. /sign-up: para crear la cuenta. Inicialmente activo solo el modo usuario & clave. Más adelante consideraremos social-login con Auth.
  3. /sign-in: para logarse
  4. /forgot-password: para recuperar el pwd via email
  5. /protected: información privada del usuario. En este punto solo el listado de prompts del usuario
  6. /protected/reset-password: para rellenar ese flujo relacionado con la clave.

Y considerando lo que queremos hacer, esta podría ser una buena estructura. Por simplicidad, dejemos fuera cuestiones en el diseño de las urls como paginación, filtros, etc. (que veremos más adelante).

(a) Rutas públicas

Ruta Descripción
/ Home pública
/prompts Catálogo público de prompts. Navegable, con filtros por tags, nombre, etc.
/prompts/[id] Detalle de un prompt específico. Accesible sin login.
/sign-up Registro de usuario.
/sign-in Login de usuario.

(b) Rutas privadas

Ruta Descripción
/dashboard Página principal del usuario logado. Puede listar sus prompts, perfil, etc.
/dashboard/prompts Listado de prompts del usuario logado (tipo /protected actual).
/dashboard/prompts/new Crear nuevo prompt.
Hazme un logo sencillo, en SVG que diga "Promptly", con la P más destacada, tomando como referencia algo similar a este de NextJS:

--y-le-pego-el-SVG-tal-cual-de-NextJS (omitido)

Y nos genera algo para empezar...
promptly_03_home_with_hero.png

Vale sí, es bastante feo, pero sirve para empezar. Además, como es un SVG con el fill="currentColor", se adapta también a la feature de theme de la template (tema oscuro), por lo que de momento👌.

Crear y listar prompts

Las rutas son algo importante y en lo que es fácil cometer errores. En la plantilla actualmente las rutas están hardcodeadas, y repetidas en varios lugares. Vamos a crear un fichero "routes.ts" para tenerlas controladas y poder cambiarlas después sin romper.

Tras varios refactor, con algo de ayuda de ChatGPT / Copilot se puede avanzar y tener una app funcional para el flujo básico: guardar y listar prompts de un usuario en modo privado.

Resumen

En este punto, ya tenemos:

Si revisamos la lista de tareas tenemos: