Skip to content

Repository files navigation

sqlitype

English

Introducción

sqlitype es un mini ORM para trabajar con bases de datos SQLite, que combina:

  • Validación de tipos en tiempo de compilación (TypeScript)
  • Validación de datos en tiempo de ejecución (TypeBox)
  • Operaciones CRUD type-safe
  • Soporte para relaciones entre modelos

Conceptos Clave

🤔 Definir Modelos

Los modelos representan tus tablas de base de datos. Cada modelo necesita:

  • Un esquema TypeBox que define la estructura
  • Configuración básica (nombre de tabla y conexión a DB)
import { Type } from '@sinclair/typebox';
import sqlitype from 'sqlitype';
import Database from 'bun:sqlite';

// Definición del esquema
const User = Type.Object({
  id    : Type.Number(),
  name  : Type.String(),
  email : Type.String(),
  age   : Type.Optional(Type.Number())
},
{ 
  $id : "Users" // nombre de la tabla
});

// Inferir el tipo TypeScript
type User = Static<typeof User>;

// Creación del modelo
const Users = new sqlitype.Model(User);

sqlitype.useConnection(new Database('mydb.sqlite'));

// También puedes usar fromTypebox
const Users = sqlitype.fromTypebox(UserSchema);

sqlitype se encarga de crear o actualizar la tabla para mantenerla sincronizada con el esquema. (dentro de lo que SQLite permite)

Puedes cambiar la conexión en cualquier momento con useClient:

sqlitype.useClient(new Database('otra.db'));
// Todos los modelos existentes usarán la nueva conexión

📀 Insertar datos

Se utiliza la validación de TypeBox en tiempo de ejecución antes de insertar nuevos datos. En tiempo de compilación utilizará la de Typescript 😍

Para los errores de validación se utiliza el formato de TypeBox.

const newUser = await Users.insert({
  name: "María García",
  email: "maria@ejemplo.com",
  age: 28
});

console.log(newUser.id); // ID auto-generado

Si los datos no pasan la validación se lanza un error:

try {
  await Users.insert({ name: "Pepe", email: 123 }); // Error! email debe ser string
} catch (e) {
  console.log(e.message); // "Validation error"
  console.log(e.errors);  // Array de ValueError de TypeBox
}

🔍 Buscar datos

Métodos disponibles:

  • find({...}) - Con filtros
  • findById(id) - Por ID único
// Todos los usuarios
const allUsers = await Users.find();

// Usuarios de 28 años
const adultos = await Users.find({
  age: 28
});

const antonios = await Users.find({
  name : "%Antonio%"
})

// usuarios con menos de 18 años
const jovenes = await Users.find({
  age : { $lt : 18 }
})

// Usuario específico
const user = await Users.findById(1);

Operadores disponibles para los filtros:

Operador Ejemplo SQL
(valor directo) { age: 28 } "age" = ?
$gt { age: { $gt: 18 } } "age" > ?
$gte { age: { $gte: 18 } } "age" >= ?
$lt { age: { $lt: 18 } } "age" < ?
$lte { age: { $lte: 18 } } "age" <= ?
$in { age: { $in: [18, 21] } } "age" IN (?,?)
$nin { age: { $nin: [18] } } "age" NOT IN (?)
% wildcard { name: "%Ana%" } "name" LIKE ?
$ne + % { name: { $ne: "%Ana%" } } "name" NOT LIKE ?
$ne { name: { $ne: "Pepa" } } "name" <> ?
$ne: null { name: { $ne: null } } "name" IS NOT NULL
null { name: null } "name" IS NULL

📊 Ordenar, limitar y paginar

Todos los métodos find y findAndJoin aceptan FindOptions como segundo parámetro:

const resultados = await Users.find(
  { age: { $gt: 18 } },
  {
    order: { name: "asc" },
    limit: 10,
    offset: 20
  }
);

El order soporta paths anidados para ordenar por campos de relaciones:

const libros = await Books.findAndJoin(
  {},
  {
    order: { title: "asc", "author.name": "desc" },
    limit: 5
  }
);

TypeScript autocompleta los paths válidos según el esquema del modelo.

📝 Actualizar datos

const updated = await Users.update(1, {
  age: 29  // Nuevo valor
});

🫂 Relaciones entre modelos

Se pueden definir relaciones con otro modelo utilizando ModelReference

Las relaciones pueden ser obligatorias u opcionales:

const Book = Type.Object({
  id: Type.Number(),
  title: Type.String(),
  author: sqlitype.ModelReference(Authors),          // ⬅️ Obligatoria
  editor: Type.Optional(sqlitype.ModelReference(Authors)) // ⬅️ Opcional
}, { $id : "Book" })

Para las opcionales puedes filtrar por si tienen o no referencia:

// Libros sin editor
const sinEditor = await Books.find({ editor: null })

// Libros con editor
const conEditor = await Books.find({ editor: { $ne: null } })
// Modelo Autor
const Book = Type.Object({
  id: Type.Number(),
  name: Type.String()
}, { $id : "Author" });
const Authors = new sqlitype.Model(Author);

// Modelo Libro (relacionado con Autor)
const Book = Type.Object({
  id: Type.Number(),
  title: Type.String(),
  author: sqlitype.ModelReference(Authors)  // ⬅️ Relación
}, { $id : "Book" })

const Books = new sqlitype.Model(Book);

// Uso
const author = await Authors.insert({ name: "Gabriel García Márquez" });
const book = await Books.insert({
  title: "Cien años de soledad",
  author: author  // Asignamos la relación
});

Luego se podrá filtrar por ese campo haciendo findAndJoin de varias maneras

const [bookWithAuthor] = await Books.findAndJoin({
  id : 1,
  author : {} // esto populará el autor del libro
})

const booksByAuthor = await Books.findAndJoin({
  "author": {
    name : "%Gabriel%"
  } // esto filtrará todos los libros de un autor
});

const books = await Books.findAndJoin({
  title : "%soledad%",
  author : {
    name : "%Gabriel%"
  }
}); // todos los libros con soledad en el titulo escritos por alguien que se llame Gabriel 😳

También puedes combinar operadores en los campos anidados:

// Libros cuyo autor NO se llame Gabriel, o libros sin autor
const libros = await Books.findAndJoin({
  author: {
    name: { $ne: "%Gabriel%" } // NOT LIKE
  }
});

// Libros de varios autores específicos
const libros = await Books.findAndJoin({
  author: {
    id: { $in: [1, 2, 3] }
  }
});

Con relaciones opcionales puedes popular aunque no tengan referencia:

// Popula el author, aunque el editor sea null
const [book] = await Books.findAndJoin({
  editor: null,
  author: {}
});
// book.author está populado, book.editor es undefined

Y con FindOptions puedes ordenar por campos anidados con type-safe:

const libros = await Books.findAndJoin(
  { author: { name: "%Gabriel%" } },
  { order: { title: "asc", "author.name": "desc" }, limit: 5 }
);

🔢 Contar resultados

const total = await Users.count(); // 5

const adultos = await Users.count({ age: { $gt: 18 } }); // 3

// También con relaciones
const libros = await Books.count({ author: { name: "%Gabriel%" } });

Tipos de Datos Soportados

 TypeBox SQLite Descripción
Type.String() TEXT Strings en general
Type.Number() REAL Números
Type.Boolean() INTEGER flags, booleanos..
Type.Date() INTEGER fechas (almacenadas como timestamp)
Type.Object() TEXT datos JSON (almacenados como texto)
Type.Any() TEXT datos JSON (almacenados como texto)
Type.Array() TEXT listas (almacenadas como JSON)

About

A little TypeBox based ORM for sqlite

Resources

Stars

Watchers

Forks

Releases

Packages

Used by

Contributors

Languages