logo cosasdedevs
Configura PostgreSQL con Drizzle ORM en SvelteKit: Base de datos del blog

Configura PostgreSQL con Drizzle ORM en SvelteKit: Base de datos del blog



My Profile
Dic 31, 2024

En este tutorial, aprenderás el proceso de configuración de una base de datos PostgreSQL en un proyecto SvelteKit utilizando Drizzle ORM. 

Veremos cómo crear un entorno con Docker, definir tablas, generar un usuario administrador desde un script y finalizar con la importación de datos de prueba. 

Después de esto ya podremos meternos a fondo con Svelte 5 y Sveltekit.

Antes de empezar 🛑 y como siempre, si te has perdido alguno de los tutoriales de esta serie, aquí te dejo todos los tutoriales escritos hasta ahora:

Instalar PostgreSQL

Para el tutorial vamos a utilizar Docker, pero si prefieres instalar PostgreSQL en tu máquina, puedes hacerlo desde este enlace https://www.postgresql.org/download/ siguiendo las instrucciones.

Si prefieres utilizar Docker, ve a la raíz del proyecto y crea un archivo llamado docker-compose.yml con el siguiente contenido:

services:
    blog-db:
      container_name: cosasdedevs_pg
      image: postgres:16.2
      restart: always
      volumes:
        - ./postgres:/var/lib/postgresql/data
      ports:
        - 5432:5432
      environment:
        - POSTGRES_USER=postgres
        - POSTGRES_PASSWORD=123456
        - POSTGRES_DB=blogdb

En este archivo docker-compose.yml, configuramos un servicio llamado blog-db que utiliza la imagen oficial de PostgreSQL en su versión 16.2. 

Hemos definido un volumen local (./postgres) para almacenar los datos de la base de datos, asegurando que no se pierdan incluso si el contenedor es eliminado. 

El servicio estará disponible en el puerto 5432, que vinculamos tanto en el host como en el contenedor. Además, configuramos las variables de entorno necesarias para la autenticación y especificamos el nombre de la base de datos (blogdb).

Ahora lanza el siguiente comando y con eso levantaremos el contenedor y la base de datos:

docker-compose up

El siguiente paso es añadir en el archivo .gitignore la carpeta postgres para evitar que se suba al repositorio:

postgres

Instalación y configuración de Drizzle ORM

Para trabajar con nuestra base de datos, vamos a utilizar Drizzle ORM, para instalarlo, vamos a la terminal y lanzamos los siguientes comandos en la raíz del proyecto:

npm i drizzle-orm pg dotenv
npm i -D drizzle-kit @types/pg tsx

Una vez hecho esto, crearemos el archivo .env en la raíz del proyecto para guardar nuestras variables de entorno y ahí añadiremos la siguiente:

DATABASE_URL="postgresql://postgres:123456@localhost:5432/blogdb?schema=public"

Recuerda sustituir el usuario, contraseña, puerto y nombre de la base de datos si no estás utilizando los mismos que utilicé yo en el archivo docker-compose.yml.

Si vas a subir el proyecto a un repositorio, te recomiendo que crees un archivo llamado .env.example con ejemplos de las variables de entorno que vayamos añadiendo para que las tengas de ejemplo en un futuro por si las necesitas.

Creación de la conexión a la base de datos y el schema

El siguiente paso es establecer la conexión a la base de datos que utilizaremos en nuestro proyecto. Para ello, primero debemos crear una carpeta llamada server dentro de la carpeta lib. Dentro de server, crearemos otra carpeta llamada db y, en su interior, un archivo llamado index.ts con el siguiente contenido:

import { DATABASE_URL } from "$env/static/private";
import { drizzle } from "drizzle-orm/node-postgres";
export default drizzle(DATABASE_URL);

En este archivo, primero importamos la variable de entorno DATABASE_URL utilizando el sistema de gestión de variables de SvelteKit. 

Es importante tener en cuenta que las variables sin el prefijo PUBLIC_ solo están disponibles en los archivos que se ejecutan exclusivamente del lado del servidor, garantizando así su seguridad. 

A continuación, importamos drizzle para PostgreSQL y, finalmente, configuramos la conexión a la base de datos utilizando dicha variable de entorno.

Ahora vamos a crear el schema. Para que te hagas una idea, este es diagrama de tablas que vamos a utilizar:

Como puedes ver, tenemos una tabla para usuarios, otra para controlar el sistema de sesiones, la tabla donde guardaremos los posts y la tabla donde almacenaremos los comentarios de los usuarios.

Para convertir este diagrama a código que después podamos utilizar para generar nuestras tablas, vamos al proyecto y creamos un archivo con el nombre src\lib\server\db\schema.ts con el siguiente contenido:

import type { InferSelectModel } from "drizzle-orm";
import { boolean, integer, pgTable, text, timestamp, varchar } from "drizzle-orm/pg-core";
export const usersTable = pgTable("users", {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    username: varchar({ length: 255 }).notNull(),
    password: varchar({ length: 255 }).notNull(),
    email: varchar({ length: 255 }).notNull().unique(),
    createdAt: timestamp("created_at", {
        withTimezone: true,
        mode: "date"
    }).defaultNow().notNull(),
    isActive: boolean("is_active").default(true).notNull(),
    isAdmin: boolean("is_admin").default(false).notNull(),
});
export const sessionsTable = pgTable("sessions", {
    id: text("id").primaryKey(),
    userId: integer("user_id")
        .notNull()
        .references(() => usersTable.id),
    expiresAt: timestamp("expires_at", {
        withTimezone: true,
        mode: "date"
    }).notNull()
});
export const postsTable = pgTable("posts", {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    userId: integer("user_id")
        .notNull()
        .references(() => usersTable.id),
    title: varchar({ length: 255 }).notNull().unique(),
    slugUrl: varchar('slug_url', { length: 255 }).notNull().unique(),
    imageHeader: varchar('image_header', { length: 255 }).notNull(),
    content: text().notNull(),
    createdAt: timestamp("created_at", {
        withTimezone: true,
        mode: "date"
    }).defaultNow().notNull(),
    isDraft: boolean("is_draft").default(true).notNull(),
});
export const commentsTable = pgTable("comments", {
    id: integer().primaryKey().generatedAlwaysAsIdentity(),
    userId: integer("user_id")
        .notNull()
        .references(() => usersTable.id),
    postId: integer("post_id")
        .notNull()
        .references(() => postsTable.id),
    content: text().notNull(),
    createdAt: timestamp("created_at", {
        withTimezone: true,
        mode: "date"
    }).defaultNow().notNull(),
});
export type UserEntity = InferSelectModel<typeof usersTable>;
export type SessionEntity = InferSelectModel<typeof sessionsTable>;
export type PostEntity = InferSelectModel<typeof postsTable>;
export type CommentEntity = InferSelectModel<typeof commentsTable>;

Este código define el esquema de tablas para nuestra base de datos PostgreSQL usando Drizzle ORM, además de generar tipos TypeScript para representar los datos. A continuación, te explico cada sección en detalle:


1. Tabla usersTable

Esta tabla representa los usuarios del sistema.

  • id: Entero auto-generado, clave primaria.
  • username: Cadena de caracteres obligatoria, máximo 255 caracteres.
  • password: Cadena de caracteres obligatoria que almacenará contraseñas encriptadas.
  • email: Cadena obligatoria y única para identificar usuarios.
  • createdAt: Fecha y hora de creación con zona horaria, valor por defecto, la fecha/hora actual.
  • isActive: Booleano que indica si el usuario está activo, por defecto true.
  • isAdmin: Booleano que indica si el usuario tiene privilegios de administrador, por defecto false.

2. Tabla sessionsTable

Esta tabla almacena las sesiones activas de los usuarios.

  • id: Cadena de texto única y clave primaria para identificar sesiones.
  • userId: Entero obligatorio que referencia al ID de un usuario en usersTable.
  • expiresAt: Fecha y hora obligatoria que indica cuándo expira la sesión.

3. Tabla postsTable

Representa los posts o publicaciones creadas por los usuarios.

  • id: Entero auto-generado, clave primaria.
  • userId: Entero obligatorio que referencia al ID del usuario que creó el post.
  • title: Cadena obligatoria, única, que almacena el título del post.
  • slugUrl: Cadena obligatoria, única, que sirve como URL amigable del post.
  • imageHeader: Cadena obligatoria que almacena la URL de la imagen principal del post.
  • content: Texto obligatorio que contiene el contenido del post.
  • createdAt: Fecha y hora de creación con valor por defecto la fecha/hora actual.
  • isDraft: Booleano que indica si el post es un borrador, por defecto true.

4. Tabla commentsTable

Almacena los comentarios hechos en los posts.

  • id: Entero auto-generado, clave primaria.
  • userId: Entero obligatorio que referencia al ID del usuario que hizo el comentario.
  • postId: Entero obligatorio que referencia al ID del post en postsTable.
  • content: Texto obligatorio que almacena el contenido del comentario.
  • createdAt: Fecha y hora de creación con valor por defecto la fecha/hora actual.

5. Tipos TypeScript

Los tipos definidos permiten usar las tablas como modelos fuertemente tipados en TypeScript:

  • UserEntity: Representa los datos de la tabla usersTable.
  • SessionEntity: Representa los datos de la tabla sessionsTable.
  • PostEntity: Representa los datos de la tabla postsTable.
  • CommentEntity: Representa los datos de la tabla commentsTable.

Una vez tenemos definido el schema, debemos crear el archivo de configuración de drizzle para poder crear las tablas en la base de datos. Para ello, debemos crear un archivo llamado drizzle.config.ts en la raiz del proyecto con el siguiente contenido:

import 'dotenv/config';
import { defineConfig } from 'drizzle-kit';
export default defineConfig({
    out: './drizzle',
    schema: './src/lib/server/db/schema.ts',
    dialect: 'postgresql',
    dbCredentials: {
        url: process.env.DATABASE_URL!,
    },
});

En este archivo configuramos lo siguiente:

  1. Ubicación del esquema: Especificamos la ruta al archivo que contiene el esquema de la base de datos que definimos anteriormente.
  2. Carpeta de archivos generados: Indicamos que los archivos generados por Drizzle, como las migraciones, se guardarán en la carpeta ./drizzle.
  3. Tipo de base de datos: Declaramos que estamos utilizando PostgreSQL como sistema de gestión de bases de datos.
  4. Conexión a la base de datos: Configuramos la conexión utilizando la URL definida en las variables de entorno.

Es importante notar que, en este caso, no utilizamos el sistema de variables de entorno propio de SvelteKit. En su lugar, empleamos dotenv y el soporte nativo de Node.js. 

Esto se debe a que este archivo está ubicado en la raíz del proyecto, donde las variables de entorno de SvelteKit no están disponibles. Por lo tanto, recurrimos a dotenv para cargar dichas variables y utilizarlas en esta configuración.

Crear las tablas en nuestra base de datos

Con esta configuración y con el contenedor de la base de datos ya podemos crear nuestras tablas. Para ello debemos usar el siguiente comando:

npx drizzle-kit push

Script para crear nuestro usuario de administración

El siguiente paso a realizar será un script con el que vamos a crear nuestro usuario de administración para poder hacer pruebas y así ver como funciona Drizzle ORM.

Para ello primero vamos a instalar la librería @node-rs/argon2 que utilizaremos para encriptar nuestras contraseñas. Recuerda y esto es muy importante, que por la ley de protección de datos, al menos en la UE, no puedes almacenar las contraseñas en texto plano.

El comando para instalarla es el siguiente:

npm i @node-rs/argon2

Una vez hecho esto, crearemos el archivo src\lib\server\db\create-user-admin.ts y añadiremos el siguiente código:

import 'dotenv/config';
import { usersTable } from "$lib/server/db/schema";
import { hash, verify } from "@node-rs/argon2";
import { eq } from "drizzle-orm";
import { drizzle } from "drizzle-orm/node-postgres";
const db = drizzle(process.env.DATABASE_URL || '');
export async function hashPassword(password: string): Promise<string> {
    return await hash(password);
}
export async function verifyPasswordHash(hash: string, password: string): Promise<boolean> {
    return await verify(hash, password);
}
async function main() {
    const passwordRaw = 'admin123';
    
    const user: typeof usersTable.$inferInsert = {
      username: 'Alberto',
      password: await hashPassword(passwordRaw),
      email: 'alberto@cosasdedevs.com',
      isAdmin: true,
    };
    await db.insert(usersTable).values(user);
    console.log('New user created!');
    const getUsers = await db
        .select()
        .from(usersTable)
        .where(eq(usersTable.email, user.email));
    if (!getUsers[0]) {
        throw new Error('User not exists');
    }
    const isValidPassword = await verifyPasswordHash(getUsers[0].password, passwordRaw);
    if (!isValidPassword) {
        throw new Error('Invalid password');
    }
    console.log('User has been created successfully');
}
main();

Este script se encarga de crear el primer usuario de la base de datos y será el usuario que utilizaremos como usuario administrador. A continuación te explico el código en profundidad:

  1. Configuración de la base de datos
    • Se importa y configura el módulo dotenv para cargar las variables de entorno necesarias.
    • Se establece una conexión a la base de datos PostgreSQL utilizando Drizzle ORM.
  2. Funciones de manejo de contraseñas
    • hashPassword: Esta función toma una contraseña en texto plano y devuelve su versión hasheada utilizando la librería @node-rs/argon2.
    • verifyPasswordHash: Esta función compara una contraseña en texto plano con su hash para verificar si coinciden.
  3. Función principal (main)
    • Creación de un usuario: Se define un objeto con los datos del usuario, incluyendo un hash de su contraseña, y se inserta en la tabla usersTable mediante el ORM Drizzle.
    • Validación del usuario:
      • Se consulta la base de datos para verificar que el usuario fue creado correctamente.
      • Se valida que la contraseña proporcionada coincide con el hash almacenado en la base de datos.
    • Mensajes de confirmación: Si todo es correcto, se imprimen mensajes en la consola indicando que el usuario fue creado y que la validación de la contraseña fue exitosa.

Puedes sustituir los datos del usuario en el objeto user (como username, email y passwordRaw) por los que prefieras para adaptarlo a tus necesidades.

Importar datos de posts a la base de datos

Para tener unos datos de prueba, he creado dentro del proyecto un archivo con unos inserts para los posts, los puedes encontrar aquí:

https://github.com/albertorc87/blog-svelte-5/blob/tutorial-3-bases-de-datos/data/test-data.sql

Puedes utilizar una herramienta para conectarte a la base de datos e insertar los datos. Yo he utilizado TablePlus. Te dejo en enlace para que puedas descargarla:

https://tableplus.com/download

Importar imágenes de los posts de prueba

También he subido al repositorio las imágenes que utilizaremos para los posts, se encuentran aquí:

https://github.com/albertorc87/blog-svelte-5/tree/tutorial-3-bases-de-datos/data/test-images

Necesito que las guardes dentro de la carpeta assets/posts (la carpeta posts no existe, debes crearla).

Actualizar .gitignore para que no se suban las imágenes al repo

Por último, si vas a subir el proyecto a un repositorio, añade dentro del archivo .gitignore la siguiente línea para que no se suban estas imágenes:

static/posts

Y eso es todo por este tutorial. Recordad que cualquier duda podéis escribirla en la caja de comentarios y también os dejo el enlace a la rama que corresponde a este tutorial por si tenéis cualquier problema:

https://github.com/albertorc87/blog-svelte-5/tree/tutorial-3-bases-de-datos

Espero que este post te ayude y como siempre, te recomiendo seguirme en Twitter para estar al tanto de los nuevo contenido. Ahora también puedes seguirme en Instagram donde estoy subiendo tips, tutoriales en vídeo e información sobre herramientas para developers.

Por último os dejo mi guía para aprender a trabajar con APIs donde explico todo el funcionamiento de una API, el protocolo HTTP y veremos como construir una API con arquitectura REST.

Nos leemos 👋.

209 vistas

🐍 Sígueme en Twitter

Si te gusta el contenido que subo y no quieres perderte nada, sígueme en Twitter y te avisaré cada vez que cree contenido nuevo 💪
Luego ¡Te sigo!

Nos tomamos en serio tu privacidad

Utilizamos cookies propias y de terceros para recopilar y analizar datos sobre la interacción de los usuarios con cosasdedevs.com. Ver política de cookies.