Recentemente escrevi um tutorial ensinando a criar e configurar um projeto Node.js para uso com o query builder Knex.js. Nós fizemos o setup inicial, configuração, criamos o banco e a migration que criou a tabela “books” pra gente. Nesta parte 2 eu quero te mostrar como realizar as operações de CRUD na referida tabela.
Vamos lá!
#1 – Types
Uma vez que estamos usando TypeScript em nosso projeto, queremos nos beneficiar da type-safety e também do autocomplete, certo? Isso é possível no Knex.js mas para isso precisamos dizer a ele os types das nossas tabelas, caso contrário ele não tem como adivinhar nada. Para isso, crie uma pasta serc/types e dentro dela coloque um index.ts.
Vamos começar definindo a interface para a nossa tabela de livros, sem nada especial aqui, apenas TypeScript puro.
|
1 2 3 4 5 6 7 8 9 10 |
export interface Book { id: number; title: string; author: string; year: number; created_at: Date; updated_at: Date; } |
A seguir, vamos declarar um módulo que será usado pelo Knex para conhecer os types das nossas tabelas, bem como exportar outros types específicos para insert e update (variações do type padrão de livro).
|
1 2 3 4 5 6 7 8 9 10 11 12 |
import type { Knex } from "knex"; export type BookInsert = Omit<Book, "id" | "created_at" | "updated_at">; export type BookUpdate = Partial<BookInsert>; declare module "knex/types/tables.js" { interface Tables { books: Knex.CompositeTableType<Book, BookInsert, BookUpdate>; } } |
Com isso, estamos dizendo ao Knex três coisas:
- o type de Book é a interface que definimos primeiro, ela é a versão “completa”;
- o type para inserts é o mesmo de book, mas omitindo (Omit) os campos id, created_at e updated_at;
- o type para updates é o mesmo de book, mas com todos campos opcionais (Partial);
O funcionamento dessa declaração ficará mais evidente quando estiver codificando as etapas seguintes, então por enquanto apenas se preocupe em deixar tudo preparado.
#2 – Create (INSERT)
Agora que temos a nossa tabela books criada, mas vazia, o primeiro passo é adicionar registros nela, para que então depois possamos avançar pelas demais operações. Nós temos duas opções para adicionar registros: manualmente ou via seeds. Seeds são códigos para geração de carga inicial de dados, seja para testes ou porque alguns sistemas realmente já tem de começar com alguns dados no banco. Vamos fazer ambos neste tópico, a começar pelas seeds.
Paera criar uma seed com Knex, use o comando abaixo no terminal, na pasta do projeto.
|
1 2 3 |
npx knex seed:make default-books -x ts |
Esse comando vai criar uma pasta seeds na raiz do seu projeto, e dentro dela, um arquivo default-books.ts com um código de adição inicial de livros dentro. Como definimos os types de Book no passo anterior, você terá ajuda do autocomplete do VS Code durante o processo de codificação da seed.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 |
import type { Knex } from "knex"; export async function seed(knex: Knex): Promise<void> { await knex("books").del(); await knex("books").insert([ { title: "Book 1", author: "Author 1", year: 2020 }, { title: "Book 2", author: "Author 2", year: 2021 }, { title: "Book 3", author: "Author 3", year: 2022 } ]); }; |
Os dados dos livros pouco importam, mas repare na estrutura. A função seed será executada quando fizermos o comando a seguir. Ela começa excluindo todos os registros (del sem filtro) da tabela books e depois com a função insert passa um array de novos livros a serem inseridos, lembrando que os campos id e de timestamps são automáticos.
Rode o comando abaixo para executar a seed e depois verifique no pgAdmin ou aguarde implementarmos as consultas a seguir.
|
1 2 3 |
npx knex seed:run |
Como citei antes, além de poder adicionar elementos via seeds, podemos fazê-lo obviamente de maneira manual, via código da nossa aplicação.
|
1 2 3 4 5 6 7 8 9 10 |
async function createBook(book: BookInsert) : Promise<Book | undefined> { const [createdBook] = await knex("books") .insert(book) .returning("*"); console.log(createdBook); return createdBook; } createBook({ title: "New Book", author: "New Author", year: 2023 }); |
A função insert espera um objeto ou array com os dados do(s) livro(s) a serem criados e retorna o(s) id(s) dos mesmos. No entanto, quando encadeamos com a função returning, nós já trazemos os objetos recém cadastrados no banco sem a necessidade de fazer um select separado como veremos a seguir.
#3 – Read (SELECT)
Agora que já temos registros em nossa tabela de livros, vamos aprender a consultar eles. Após selecionar qualquer tabela com o objeto knex, podemos usar as seguintes funções:
- select: para dizer quais campos queremos retornar;
- limit: para dizer quantas linhas queremos retornar;
- offset: para dizer quantas linhas queremos pular;
- orderBy: para dizer a ordenação, sendo que o primeiro parâmetro é para o campo e o segundo para asc ou desc;
Por exempo, trazer os primeiros 10 livros ordenados por título no index.ts:
|
1 2 3 4 5 6 7 8 9 10 |
import knex from "./config/knex.js"; const books = await knex("books") .select("*") .limit(10) .offset(0) .orderBy("title", "asc"); console.log(books); |
Verifique, por exemplo, que a constante books possui o tipo Book[] o que está perfeitamente correto com o nosso arquivo de types.
Agora se quiser retornar com filtro, temos de usar a função where, onde podemos passar um objeto com os filtros ou então três parâmetros, nesta ordem: campo, operador, valor. No exemplo abaixo ainda usei a função first, que transforma o retorno em objeto único ao invés de array de objetos, retornando undefined caso não tenha retorno algum na consulta.
|
1 2 3 4 5 6 7 |
const book = await knex("books") .select("*") .where({ id }) .first(); console.log(book); |
E por fim, querendo encadear condições bem diferentes, que não se adequam aos dois formatos que passei acima, você pode chamar as funções andWhere e orWhere sucessivas vezes.
#4 – Update
Agora que já sabemos como cadastrar e ler informações do banco usando Knex, chegou a hora de atualizar dados. A atualização via Knex guarda semelhanças com o insert e com o select. Isso porque temos de filtrar o que vamos atualizar (assim como no select) e podemos retornar os dados atualizados (assim como no insert).
|
1 2 3 4 5 6 7 8 9 10 11 |
async function updateBook(id: number, book: BookUpdate) : Promise<Book | undefined> { const [updatedBook] = await knex("books") .where({ id }) .update(book) .returning("*"); console.log(updatedBook); return updatedBook; } updateBook(1, { title: "Updated Book 1", author: "Updated Author 1", year: 2023 }); |
Assim, começamos nosso where passando os campos usados no filtro da atualização, seguido da função de update em si. Assim como no caso do insert, o update retorna os ids dos registros atualizados pela operação, mas quando encadeamos com a função returning, nós já trazemos os objetos inteiros após a atualização, sem precisar de um select adicional.
#5 – Delete
E por fim, hora de apredermos como excluir registros no banco de dados via Knex, o que não é exatamente algo complexo nesse ponto da nossa jornada.
|
1 2 3 4 5 6 7 8 9 10 11 |
async function deleteBook(id: number) : Promise<Book | undefined> { const [deletedBook] = await knex("books") .where({ id }) .del() .returning("*"); console.log(deletedBook); return deletedBook; } deleteBook(2); |
Funções de delete não costumam ter retorno (void), se não deu erro, então assumimos que deu tudo certo. Ainda assim, querendo ter certeza que algo foi exlcuída, o Knex devolve os ids excluídos como retorno da função del (tem a sobrecarga delete também, que faz a mesma coisa) e, opcionalmente, você pode usar o returning como já vimos anteriormente para retornar o registro inteiro.
E com isso, finalizamos nosso estudo de operações CRUD com Knex.js, PostgreSQL e TypeScript.
Até a próxima!
Olá, tudo bem?
O que você achou deste conteúdo? Conte nos comentários.




