Base-Database é uma biblioteca projetada para oferecer uma padronização eficiente no gerenciamento de dados, permitindo integração com diversos tipos de armazenamento. Através de sua estrutura modular, o desenvolvedor pode criar soluções personalizadas ao estender a classe abstrata Database.Custom
, que serve como base para diferentes estratégias de armazenamento.
-
Base-Database
- Funcionalidades
- Como funciona
- Exemplos de Uso
- Instalação
- Exemplo de Implementação
- Aplicação
-
Banco de dados
createDatabase
-
getDatabase
-
Database.Custom<db = never>
constructor(database: string)
connect(database: string): Promise<db>
disconnect(): Promise<void>
selectAll(table: string, query?: Database.QueryOptions): Promise<Array<Database.Row>>
selectOne(table: string, query?: Database.QueryOptions): Promise<Database.Row | null>
selectFirst(table: string, query?: Database.QueryOptions): Promise<Database.Row | null>
selectLast(table: string, query?: Database.QueryOptions): Promise<Database.Row | null>
insert(table: string, data: Database.Row): Promise<Database.Row>
update(table: string, data: Partial<Database.Row>, query: Database.QueryOptions): Promise<void>
delete(table: string, query: Database.QueryOptions): Promise<void>
length(table: string, query?: Database.QueryOptions): Promise<number>
createTable(table: string, columns: Database.SerializeDataType<any>): Promise<void>
deleteTable(table: string): Promise<void>
deleteDatabase(): Promise<void>
- Exemplo utilizando SQLite
- Flexibilidade: Suporte para diversas formas de armazenamento, incluindo SQLite, LocalStorage, MongoDB, entre outros.
- Personalização: Permite criar classes específicas que personalizam o comportamento do armazenamento e da gestão de dados.
-
Intermediação: A classe
Database.Database
atua como um intermediário universal, gerenciando a interação entre os dados e o tipo de armazenamento definido na implementação.
-
Criação de uma classe personalizada: O desenvolvedor estende a classe abstrata
Database.Custom
para criar sua própria solução de armazenamento e gerenciamento. -
Integração com
Database.Database
: A classeDatabase.Database
utiliza a implementação personalizada para operar de forma padronizada, facilitando a manipulação e consulta de dados.
Essa abordagem permite uma adaptação eficiente para diferentes contextos, mantendo uma arquitetura escalável e modular.
- Gerenciamento de dados local com
LocalStorage
. - Armazenamento em bancos de dados relacionais como
SQLite
. - Suporte a bancos NoSQL como
MongoDB
.
Base-Database é ideal para desenvolvedores que buscam uma solução robusta e flexível para unificar a gestão de dados em diferentes plataformas.
Para instalar a biblioteca para node.js, utilize o comando:
npm install base-database
A seguir, um exemplo de implementação de uma classe personalizada que estende Database.Custom
e criação de banco de dados na estrutura de aplicação usando a propriedate initializeApp
:
import { Database, initializeApp, getDatabase } from 'base-database';
const app = initializeApp();
class MyDatabase extends Database.Custom {
// Implementação dos métodos abstratos
}
const db = app.createDatabase({
database: ":memory:",
custom: MyDatabase,
tables: {
test: Database.columns({
name: {
type: Database.Types.TEXT,
},
gender: {
type: Database.Types.TEXT,
options: ["Female", "Male", "Other"] as const,
},
createdAt: {
type: Database.Types.DATETIME,
default: () => new Date(),
},
}),
},
});
export type MainDatabase = typeof db;
A partir desse ponto, é possível utilizar os métodos de getDatabase
para interagir com os dados de forma padronizada. Consulte a documentenção de Database.Custom
para mais informações sobre a implementação de uma classe personalizada.
A aplicação é responsável por gerenciar o banco de dados e outros serviços. A aplicação é utilizada para centralizar a gestão de dados e garantir a integridade das operações.
O método initializeApp
é responsável por criar uma instância de aplicação para gerenciar o banco de dados e outros serviços. A aplicação é utilizada para centralizar a gestão de dados e garantir a integridade das operações.
import { initializeApp } from 'base-database';
const app = initializeApp();
O método initializeApp
é mais focado para operações locais e simplificada, para métodos mais complexos, é recomendado utilizar a classe initializeServer
.
O método initializeServer
é responsável por criar uma instância de servidor para gerenciar o banco de dados e outros serviços. O servidor é utilizado para centralizar a gestão de dados e garantir a integridade das operações.
import { initializeServer } from 'base-database';
const server = initializeServer();
Para gerenciar os dados, é necessário criar uma classe que estenda Database.Custom
e implemente os métodos abstratos e configurar as tabelas que serão utilizadas. Todas as configurações devem ser aplicadas diretamente na aplicação criada, usando o método createDatabase
da instância de aplicação por através do método initializeApp
ou initializeServer
.
O método createDatabase
é responsável por criar uma instância de banco de dados com as configurações definidas. O método deve receber um objeto com as seguintes propriedades:
import { Database, initializeApp } from 'base-database';
const app = initializeApp();
class MyDatabase extends Database.Custom {
// Implementação dos métodos abstratos
}
const db = app.createDatabase({
database: ":memory:",
custom: MyDatabase,
tables: {
myTable: Database.columns({
name: {
type: Database.Types.TEXT,
},
gender: {
type: Database.Types.TEXT,
options: ["Female", "Male", "Other"] as const,
},
createdAt: {
type: Database.Types.DATETIME,
default: () => new Date(),
},
}),
},
});
export type MainDatabase = typeof db;
Note que estamos exportando o tipo
MainDatabase
para que possamos utilizar o tipo do banco de dados para garantir a integridade dos dados por meio do TypeScript.
O método Database.columns
é responsável por criar um objeto de serialização de dados para definir as colunas de uma tabela, para que possam ser armazenados e recuperados de forma segura correspondendo ao tipo de dados original.
Cada coluna da tabela deve ser definida com um objeto que contém as seguintes propriedades:
-
type
: Tipo de dado da coluna. -
primaryKey
: Indica se a coluna é uma chave primária. -
autoIncrement
: Indica se a coluna é autoincrementável. -
notNull
: Indica se a coluna não pode ser nula. -
default
: Valor padrão da coluna. -
unique
: Indica se a coluna deve ser única. -
check
: Uma função que valida o valor da coluna, caso o valor não seja válido, a função deve lançar um erro, retornando uma instância de Error ou emitindo um erro throw. -
options
: Uma lista de opções válidas para a coluna.
const columns = {
id: { type: Database.Types.INTEGER, primaryKey: true },
name: { type: Database.Types.TEXT, notNull: true },
date: { type: Database.Types.DATETIME },
amount: { type: Database.Types.FLOAT },
isValid: { type: Database.Types.BOOLEAN, default: false },
variant: { type: Database.Types.BIGINT, notNull: true },
email: { type: Database.Types.TEXT, unique: true, check: (value) => {
if (!value.includes("@")) throw new Error("Invalid email");
}},
options: { type: Database.Types.TEXT, options: ["Option 1", "Option 2", "Option 3"] as const },
};
O objeto Database.Types
contém os tipos de dados disponíveis para a criação de colunas. A seguir, a lista de tipos de dados disponíveis:
-
INTEGER
: Número inteiro. -
FLOAT
: Número de ponto flutuante. -
TEXT
: Texto. -
BOOLEAN
: Booleano. -
DATETIME
: Data e hora. -
BIGINT
: Número inteiro grande. -
NULL
: Nulo.
O método getDatabase
é responsável por gerenciar a interação entre os dados e o armazenamento definido na implementação de Database.Custom
.
import { getDatabase } from 'base-database';
import { MainDatabase } from './database';
const db = getDatabase<MainDatabase>();
Método que executa uma função dentro de uma transação. Deve ser utilizado para garantir que a operação seja realizada de forma segura.
db.ready(async (db) => {
// Operações dentro da transação
});
Método que desconecta do armazenamento de dados.
await db.disconnect();
O método table
contém propriedades úteis com ready
, query
, insert
, selectAll
, selectFirst
, selectLast
, length
, on
, once
, off
, offOnce
e schema
. O método além ter uma resposta sincrona, seus métodos poderão ser úteis em situações que pretente simplificar a utilização de uma tabela sem a necessidade de utilizar o método ready
, todos os médotos de readyTable
já realizam essa tarefa, que são executados assim que a tabela estiver pronta para ser utilizada.
const table = db.table("myTable");
table.query().where("name", Database.Operators.EQUAL, "John").get();
Método que executa uma função dentro de uma transação. Deve ser utilizado para garantir que a operação seja realizada de forma segura.
table.ready(async (table) => {
// Operações dentro da transação
});
Método que retorna uma instância de Database.Query
para a tabela.
const query = table.query();
Método que adiciona uma cláusula WHERE
à consulta.
query.where("name", Database.Operators.EQUAL, "John");
query.where("date", Database.Operators.GREATER_THAN, new Date());
query.where("amount", Database.Operators.LESS_THAN_OR_EQUAL, 100.50);
query.where("isValid", Database.Operators.EQUAL, true);
query.where("variant", Database.Operators.BETWEEN, [100, 200]);
query.where("email", Database.Operators.LIKE, "%@gmail.com");
query.where("email", Database.Operators.LIKE, new RegExp(".*@gmail.com"));
O objeto Database.Operators
contém os operadores disponíveis para a criação de cláusulas WHERE
. A seguir, a lista de operadores disponíveis:
-
EQUAL
: Igual a=
. -
NOT_EQUAL
: Diferente de!=
. -
GREATER_THAN
: Maior que>
. -
GREATER_THAN_OR_EQUAL
: Maior ou igual a>=
. -
LESS_THAN
: Menor que<
. -
LESS_THAN_OR_EQUAL
: Menor ou igual a<=
. -
BETWEEN
: EntreBETWEEN
. -
NOT_BETWEEN
: Não entreNOT BETWEEN
. -
LIKE
: Semelhante aLIKE
. -
NOT_LIKE
: Não semelhante aNOT LIKE
. -
IN
: EmIN
. -
NOT_IN
: Não emNOT IN
.
Método que adiciona uma cláusula WHERE
à consulta, o mesmo que where
.
query.filter({ name: "John", amount: 100.50 });
Método que limita o número de registros retornados pela consulta.
query.take(10);
Método que pula um número de registros na consulta.
query.skip(10);
Método que ordena os registros da consulta.
query.sort("name");
query.sort("date", false);
Método que ordena os registros da consulta.
query.order("name");
query.order("date", false);
Método que seleciona as colunas retornados pela consulta.
query.columns("name", "date");
Método que executa a consulta e retorna os registros.
const rows = await query.get();
Método que executa a consulta e retorna o primeiro registro.
const row = await query.first();
Método que executa a consulta e retorna o último registro.
const row = await query.last();
Método que executa a consulta e retorna um registro.
const row = await query.one();
Método que executa a consulta e verifica se um registro existe.
const exists = await query.exists();
Método que executa a consulta e retorna o número de registros.
const count = await query.count();
Método que executa a consulta e retorna o número de registros.
const length = await query.length();
Método que executa a consulta e atualiza os registros.
await query.set({ name: "John" });
Método que executa a consulta e atualiza os registros.
await query.update({ name: "John" });
Método que executa a consulta e deleta os registros.
await query.delete();
Método que insere um registro na tabela.
await table.insert({
id: 1,
name: "John",
date: new Date(),
amount: 100.50,
isValid: true,
variant: BigInt(100)
});
Método que seleciona todos os registros da tabela.
const rows = await table.selectAll();
// ou
const rows = await table.selectAll(table.query().where("name", Database.Operators.EQUAL, "John")); // com cláusula WHERE
// ou
const rows = await table.selectAll(table.query().where("name", Database.Operators.EQUAL, "John").columns("name", "date")); // com cláusula WHERE e colunas específicas
// ou
const rows = await table.selectAll(table.query().columns("name", "date")); // com colunas específicas mas sem cláusula WHERE
Método que seleciona um registro da tabela.
const row = await table.selectOne();
// ou
const row = await table.selectOne(table.query().where("name", Database.Operators.EQUAL, "John")); // com cláusula WHERE
// ou
const row = await table.selectOne(table.query().where("name", Database.Operators.EQUAL, "John").columns("name", "date")); // com cláusula WHERE e colunas específicas
// ou
const row = await table.selectOne(table.query().columns("name", "date")); // com colunas específicas mas sem cláusula WHERE
// ou
const row = await table.selectOne(table.query().order("name")); // com ordenação
Método que seleciona o primeiro registro da tabela.
const row = await table.selectFirst();
// ou
const row = await table.selectFirst(table.query().where("name", Database.Operators.EQUAL, "John")); // com cláusula WHERE
// ou
const row = await table.selectFirst(table.query().where("name", Database.Operators.EQUAL, "John").columns("name", "date")); // com cláusula WHERE e colunas específicas
// ou
const row = await table.selectFirst(table.query().columns("name", "date")); // com colunas específicas mas sem cláusula WHERE
// ou
const row = await table.selectFirst(table.query().order("name")); // com ordenação
Método que seleciona o último registro da tabela.
const row = await table.selectLast();
// ou
const row = await table.selectLast(table.query().where("name", Database.Operators.EQUAL, "John")); // com cláusula WHERE
// ou
const row = await table.selectLast(table.query().where("name", Database.Operators.EQUAL, "John").columns("name", "date")); // com cláusula WHERE e colunas específicas
// ou
const row = await table.selectLast(table.query().columns("name", "date")); // com colunas específicas mas sem cláusula WHERE
// ou
const row = await table.selectLast(table.query().order("name")); // com ordenação
Método que retorna o número de registros da tabela.
const count = await table.length();
Métodos para adicionar e remover eventos de uma tabela.
const onInsert = (row) => {
console.log("Row inserted:", row);
};
table.on("insert", onInsert);
const onceInsert = (row) => {
console.log("Row inserted once:", row);
};
table.once("insert", onceInsert);
table.off("insert", onInsert);
table.offOnce("insert", onceInsert);
Realiza a mesma função que o método bindSchema
.
Mapear dados para suas próprias classes permite que você armazene e carregue objetos de/para o banco de dados sem que eles percam seu tipo de classe. Depois de mapear tabela para uma classe, você nunca mais precisará se preocupar com serialização ou desserialização dos objetos => Armazene um User
, obtenha um User
. Quaisquer métodos específicos de classe podem ser executados diretamente nos objetos que você obtém de volta da tabela, porque eles serão uma instanceof
sua classe.
Por padrão, o Base-Database executa seu construtor de classe com um instantâneo dos dados para instanciar novos objetos e usa todas as propriedades da sua classe para serializá-los para armazenamento.
// User class implementation
class User {
name: string;
constructor(obj: Database.ExtractTableRow<typeof table>) {
this.name = obj.name;
}
}
// Mapping table to class
const UserTable = table.bindSchema(User);
Agora você pode fazer o seguinte:
let user = new User();
user.name = 'Ewout';
await UserTable.insert(user);
let users: User[] = await UserTable.selectAll();
// users[0] instanceof User === true
Se você não puder (ou não quiser) alterar o construtor da sua classe, adicione um método estático chamado createpara desserializar objetos armazenados:
class Pet {
// Constructor that takes multiple arguments
constructor(animal, name) {
this.animal = animal;
this.name = name;
}
// Static method that instantiates a Pet object
static create(obj: Database.ExtractTableRow<typeof table>) {
return new Pet(obj.animal, obj.name);
}
}
// Mapping table to class
const PetTable = table.bindSchema(Pet);
Se você quiser alterar como seus objetos são serializados para armazenamento, adicione um método chamado serialize
á sua classe. Você deve fazer isso se sua classe contiver propriedades que não devem ser serializadas (por exemplo, get
propriedades).
class Pet {
// Constructor that takes multiple arguments
constructor(animal, name) {
this.animal = animal;
this.name = name;
}
// Static method that instantiates a Pet object
static create(obj: Database.ExtractTableRow<typeof table>) {
return new Pet(obj.animal, obj.name);
}
// Method that serializes a Pet object
serialize(): Partial<Database.ExtractTableRow<typeof table>> {
return {
animal: this.animal,
name: this.name,
};
}
}
// Mapping table to class
const PetTable = table.bindSchema(Pet);
Se você quiser usar outros métodos para instanciação e/ou serialização além dos padrões explicados acima, você pode especificá-los manualmente na bind
chamada:
class Pet {
// ...
toDatabase(): Partial<Database.ExtractTableRow<typeof table>> {
return {
animal: this.animal,
name: this.name
}
}
static fromDatabase(obj: Database.ExtractTableRow<typeof table>) {
return new Pet(obj.animal, obj.name);
}
}
// Mapping table to class
const PetTable = table.bindSchema(Pet, {
creator: Pet.fromDatabase,
serializer: Pet.prototype.toDatabase
});
Se você deseja armazenar classes nativas ou de terceiros, ou não deseja estender as classes com métodos de (des)serialização:
// Mapping table to class
const RegExpTable = table.bindSchema(RegExp, {
creator: (obj) => new RegExp(obj.pattern, obj.flags),
serializer: (obj) => ({ pattern: obj.source, flags: obj.flags })
});
Método que deleta uma tabela.
await db.deleteTable("myTable");
Método que deleta o banco de dados.
await db.deleteDatabase();
A classe abstrata Database.Custom
define os métodos necessários para a implementação de uma solução personalizada de armazenamento. A seguir, a lista de métodos que devem ser implementados:
import { Database } from 'base-database';
import * as sqlite3 from 'sqlite3';
class MyDatabase extends Database.Custom<sqlite3.Database> {
// Implementação dos métodos abstratos
}
Construtor da classe personalizada. Deve receber o nome do banco de dados como parâmetro.
constructor(database: string) {
// Implementação do construtor
}
Método responsável por conectar ao armazenamento de dados. Deve retornar uma Promise
que resolve com o objeto de conexão.
import * as sqlite3 from 'sqlite3';
// Código anterior....
connect(database: string): Promise<sqlite3.Database> {
// Implementação da conexão
}
Método responsável por desconectar do armazenamento de dados. Deve retornar uma Promise
que resolve após a desconexão.
disconnect(): Promise<void> {
// Implementação da desconexão
}
Método responsável por selecionar todos os registros de uma tabela. Deve retornar uma Promise
que resolve com um array de registros.
selectAll(table: string, query?: Database.QueryOptions): Promise<Array<Database.Row>> {
// Implementação da seleção
}
Método responsável por selecionar um registro de uma tabela. Deve retornar uma Promise
que resolve com um registro ou null
.
selectOne(table: string, query?: Database.QueryOptions): Promise<Database.Row | null> {
// Implementação da seleção
}
Método responsável por selecionar o primeiro registro de uma tabela. Deve retornar uma Promise
que resolve com um registro ou null
.
selectFirst(table: string, query?: Database.QueryOptions): Promise<Database.Row | null> {
// Implementação da seleção
}
Método responsável por selecionar o último registro de uma tabela. Deve retornar uma Promise
que resolve com um registro ou null
.
selectLast(table: string, query?: Database.QueryOptions): Promise<Database.Row | null> {
// Implementação da seleção
}
Método responsável por inserir um registro em uma tabela. Deve retornar uma Promise
que resolve após a inserção.
insert(table: string, data: Database.Row): Promise<Database.Row> {
// Implementação da inserção
}
Método responsável por atualizar registros em uma tabela. Deve retornar uma Promise
que resolve após a atualização.
update(table: string, data: Partial<Database.Row>, query: Database.QueryOptions): Promise<void> {
// Implementação da atualização
}
Método responsável por deletar registros de uma tabela. Deve retornar uma Promise
que resolve após a deleção.
delete(table: string, query: Database.QueryOptions): Promise<void> {
// Implementação da deleção
}
Método responsável por obter o número de registros de uma tabela. Deve retornar uma Promise
que resolve com o número de registros.
length(table: string, query?: Database.QueryOptions): Promise<number> {
// Implementação do cálculo do número de registros
}
Método responsável por criar uma tabela. Deve retornar uma Promise
que resolve após a criação da tabela.
createTable(table: string, columns: Database.SerializeDataType<any>): Promise<void> {
// Implementação da criação da tabela
}
Método responsável por deletar uma tabela. Deve retornar uma Promise
que resolve após a deleção da tabela.
deleteTable(table: string): Promise<void> {
// Implementação da deleção da tabela
}
Método responsável por deletar o banco de dados. Deve retornar uma Promise
que resolve após a deleção do banco de dados.
deleteDatabase(): Promise<void> {
// Implementação da deleção do banco de dados
}
Para utilizar SQLite como armazenamento, é necessário instalar o pacote sqlite3
:
npm install sqlite3
Em seguida, implemente a classe personalizada para SQLite:
import Database, { SQLiteRegex } from 'base-database';
import sqlite3 from "sqlite3";
const formatDateToSQL = (date: Date): string => {
const pad = (n: number) => (n < 10 ? "0" + n : n);
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
};
const normalizeWhereCompare = (compare: any) => {
return typeof compare === "string" ? `'${compare}'` : compare instanceof Date ? formatDateToSQL(compare) : Array.isArray(compare) ? compare.map(normalizeWhereCompare) : compare;
};
const regexToSqlitePattern = (regex: RegExp) => {
if (!(regex instanceof RegExp)) throw new Error("O argumento deve ser uma instância de RegExp.");
let pattern = regex.source;
const flags = regex.flags;
if (flags.includes("i")) pattern = `(?i)${pattern}`;
if (flags.includes("m")) pattern = `(?m)${pattern}`;
if (flags.includes("s")) pattern = `(?s)${pattern}`;
return normalizeWhereCompare(pattern);
};
const parseQuery = (query?: Database.QueryOptions) => {
if (!query) return { columns: "*", where: "", order: "", limit: "", offset: "" };
let whereClause: string[] = [];
if (Array.isArray(query.wheres) && query.wheres.length > 0) {
query.wheres.forEach(({ column, operator, compare }) => {
compare = normalizeWhereCompare(compare);
switch (operator) {
case "=":
case "!=":
case "<":
case "<=":
case ">":
case ">=":
whereClause.push(`${column} ${operator} ${compare}`);
break;
case "IN":
case "NOT IN":
if (Array.isArray(compare) && compare.length > 0) whereClause.push(`${column} ${operator} (${compare.join(", ")})`);
break;
case "BETWEEN":
case "NOT BETWEEN":
if (Array.isArray(compare) && compare.length >= 2) whereClause.push(`${column} ${operator} ${compare[0]} AND ${compare[1]}`);
break;
case "LIKE":
case "NOT LIKE":
if (typeof compare === "string") whereClause.push(`${column} ${operator} ${compare}`);
if (compare instanceof RegExp) whereClause.push(`${column} ${operator === "LIKE" ? "" : "NOT "}REGEXP ${regexToSqlitePattern(compare)}`);
break;
}
});
}
Array.isArray(query.wheres) && query.wheres.length > 0 ? query.wheres.map((w) => `${w.column} ${w.operator} ${typeof w.compare === "string" ? `'${w.compare}'` : w.compare}`).join(" AND ") : "";
const columnClause = Array.isArray(query.wheres) && query.columns.length > 0 ? query.columns.join(", ") : "*";
const orderClause = Array.isArray(query.order) && query.order.length > 0 ? query.order.map(({ column, ascending }) => `${String(column)} ${ascending ? "ASC" : "DESC"}`).join(", ") : "";
return {
columns: columnClause,
where: whereClause.join(" AND ").trim() === "" ? "" : `WHERE ${whereClause.join(" AND ").trim()}`,
order: orderClause.trim() === "" ? "" : `ORDER BY ${orderClause.trim()}`,
limit: query.take ? `LIMIT ${query.take}` : "",
offset: query.skip ? `OFFSET ${query.skip}` : "",
};
};
class SQLite extends Database.Custom<sqlite3.Database> {
private db: sqlite3.Database | undefined;
connect(database: string): Promise<sqlite3.Database> {
return new Promise((resolve, reject) => {
try {
const db = (this.db = new sqlite3.Database(database));
db.loadExtension(SQLiteRegex.getLoadablePath());
db.serialize(() => {
resolve(db);
});
} catch (e) {
const message = e instanceof Error ? e.message : String(e);
return reject(new Error(message));
}
});
}
disconnect(): Promise<void> {
return new Promise((resolve, reject) => {
if (this.db) {
this.db.close((err) => {
if (err) reject(new Error(err.message));
else resolve();
});
} else {
reject(new Error("Database not connected"));
}
});
}
selectAll(table: string, query?: Database.QueryOptions): Promise<Array<Database.Row>> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const { columns, where, order, limit, offset } = parseQuery(query);
db.all<Database.Row>(`SELECT ${columns} FROM ${table} ${where} ${order} ${limit} ${offset}`.trim() + ";", (err, rows) => {
if (err) reject(new Error(err.message));
else resolve(rows);
});
});
});
}
selectOne(table: string, query?: Database.QueryOptions): Promise<Database.Row | null> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const { columns, where, order } = parseQuery(query);
db.get<Database.Row | null | undefined>(`SELECT ${columns} FROM ${table} ${where} ${order}`.trim() + ";", (err, row) => {
if (err) reject(new Error(err.message));
else resolve(row ?? null);
});
});
});
}
selectFirst(table: string, query?: Database.QueryOptions): Promise<Database.Row | null> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const { columns, where, order } = parseQuery(query);
db.get<Database.Row | null | undefined>(`SELECT ${columns} FROM ${table} ${where} ${order}`.trim() + ";", (err, row) => {
if (err) reject(new Error(err.message));
else resolve(row ?? null);
});
});
});
}
selectLast(table: string, query?: Database.QueryOptions): Promise<Database.Row | null> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const { columns, where, order } = parseQuery(query);
const offset = `LIMIT 1 OFFSET (SELECT COUNT(*) - 1 FROM ${table} ${where}`.trim() + ")";
db.get<Database.Row | null | undefined>(`SELECT ${columns} FROM ${table} ${where} ${order}`.trim() + ` ${offset};`, (err, row) => {
if (err) reject(new Error(err.message));
else resolve(row ?? null);
});
});
});
}
insert(table: string, data: Database.Row): Promise<Database.Row> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const columns = Object.keys(data).join(", ");
const values = Object.values(data)
.map(() => `?`)
.join(", ");
const stmt = db.prepare(`INSERT INTO ${table} (${columns}) VALUES (${values});`);
stmt.run(Object.values(data), function (err) {
if (err) return reject(err);
const lastRowID = this.lastID;
db.get<Database.Row>(`SELECT rowid, * FROM ${table} WHERE rowid = ?`, [lastRowID], function (err, row) {
if (err) return reject(err);
resolve(row);
});
});
stmt.finalize();
});
});
}
update(table: string, data: Partial<Database.Row>, query: Database.QueryOptions): Promise<void> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const setClause = Object.keys(data)
.map((column) => `${column} = ?`)
.join(", ");
const { where } = parseQuery(query);
if (where.trim() === "") {
reject(new Error("WHERE clause is required for UPDATE operation"));
return;
}
db.run(`UPDATE ${table} SET ${setClause} ${where}`.trim() + ";", Object.values(data), (err) => {
if (err) reject(new Error(err.message));
else resolve();
});
});
});
}
delete(table: string, query: Database.QueryOptions): Promise<void> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const { where } = parseQuery(query);
if (where.trim() === "") {
reject(new Error("WHERE clause is required for UPDATE operation"));
return;
}
db.run(`DELETE FROM ${table} ${where}`.trim() + ";", (err) => {
if (err) reject(new Error(err.message));
else resolve();
});
});
});
}
length(table: string, query?: Database.QueryOptions): Promise<number> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const { where } = parseQuery(query);
db.get<{
count: number;
}>(`SELECT COUNT(*) AS count FROM ${table} ${where}`.trim() + ";", (err, row) => {
if (err) reject(new Error(err.message));
else resolve(row?.count ?? 0);
});
});
});
}
createTable(table: string, columns: Database.SerializeDataType<any>): Promise<void> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
const columnClause = Object.fromEntries(
Object.entries(columns).map(([column, info]) => {
let clause = `${column} ${info.type}`;
if (info.primaryKey) clause += " PRIMARY KEY";
if (info.autoIncrement) clause += " AUTOINCREMENT";
if (info.notNull) clause += " NOT NULL";
if (info.default && typeof info.default !== "function") {
switch (typeof info.default) {
case "string":
clause += ` DEFAULT ${JSON.stringify(info.default)}`;
break;
case "number":
case "bigint":
case "boolean":
clause += ` DEFAULT ${info.default}`;
break;
case "object":
if (info.default instanceof Date) {
clause += ` DEFAULT ${info.default.getTime()}`;
}
break;
}
}
if (info.unique) clause += " UNIQUE";
return [column, clause];
}),
);
db.all<{
cid: number;
name: string;
type: string;
notnull: number;
dflt_value: string;
pk: number;
}>(`PRAGMA table_info(${table});`, (err, rows) => {
if (err) {
return reject(new Error(err.message));
}
const existingColumns = rows.map((row) => row.name); // Nomes das colunas existentes
// Verifica e cria colunas ausentes
const columnDefinitions: string[] = Object.keys(columns)
.filter((column) => !existingColumns.includes(column)) // Apenas colunas ausentes
.map((column) => {
return `ALTER TABLE ${table} ADD COLUMN ${columnClause[column]}`;
});
// Executa as colunas ausentes
const executeAddColumn = async () => {
for (const query of columnDefinitions) {
await new Promise<void>((resolveAdd, rejectAdd) => {
db.run(query, (err) => {
if (err) rejectAdd(new Error(err.message));
else resolveAdd();
});
});
}
};
db.run(`CREATE TABLE IF NOT EXISTS ${table} (${Object.values(columnClause).join(", ")});`, async (err) => {
if (err) {
return reject(new Error(err.message));
}
await executeAddColumn().catch(() => Promise.resolve());
resolve();
});
});
});
});
}
deleteTable(table: string): Promise<void> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
db.run(`DROP TABLE IF EXISTS ${table};`, (err) => {
if (err) reject(new Error(err.message));
else resolve();
});
});
});
}
deleteDatabase(): Promise<void> {
return this.ready(async (db) => {
return new Promise((resolve, reject) => {
db.close((err) => {
if (err) reject(new Error(err.message));
else resolve();
});
});
});
}
}