Eu escrevi recentemente sobre como fazer uma Todo API no Deno + Oak (sem usar um banco de dados). Que eu rotulei como chapter_1: oak. Este tutorial é uma parte do capítulo anterior, onde falarei sobre a integração do mysql no projeto Deno + Oak.
Se a qualquer momento você quiser ver o código fonte inteiro deste tutorial. Está disponível aqui chamado chapter_2: mysql (Dê uma estrela no Github, se quiser)
Estou assumindo que você está acompanhando chapter_1: oak :: Todo API no Deno + Oak (sem usar um banco de dados) porque é um requeridos pré-requisito deste tutorial e continua de onde saímos.
Por favor, certifique-se de ter o cliente MySQL instalado e funcionando. Estou em uma versão do Mac OS Catalina e usando o seguinte.
Escrevi um pequeno guia para usuários de Mac OS sobre como configurar o MySQL em suas máquinas. Porque eu lutei com isso também.[[[[Guia de instalação aqui]
Se você estiver em uma máquina Windows, poderá usar as mesmas ferramentas ou também XAMPP ter uma instância do MySQL em execução no seu painel.
Depois de ter uma instância do MySQL em execução, podemos começar nosso tutorial.
Vamos começar
Supondo que todos venham deste artigo Todo API no Deno + Oak (sem usar um banco de dados). Faremos o seguinte hoje;
- Crie uma conexão com o banco de dados MySQL
- Escreva um pequeno script que redefina o banco de dados toda vez que iniciarmos o servidor Deno
- Executar operações CRUD em uma tabela
- Adicione a funcionalidade CRUD aos nossos controladores de API
Uma última coisa, aqui está toda a diferença de consolidação que foi feita no Capítulo 1 para adicionar o MySQL ao projeto[[[[código fonte que mostra as novas adições feitas no capítulo 1]
Na pasta raiz do projeto, o meu é chamado chapter_2:mysql
(o seu pode ser o que você quiser) crie uma pasta chamada db & dentro dessa pasta, crie um arquivo chamado config.ts & adicione o seguinte conteúdo a ele.
export const DATABASE: string = "deno";export const TABLE = { TODO: "todo",};
Nada de especial aqui, basta definir o nome do banco de dados junto com um objeto para as tabelas e depois exportá-lo. (Nosso projeto terá um banco de dados chamado “deno” e, dentro desse banco de dados, teremos apenas uma tabela chamada “todo”)
Próximo dentro db pasta crie outro arquivo chamado client.ts e adicione o seguinte conteúdo
import { Client } from "https://deno.land/x/mysql/mod.ts";// configimport { DATABASE, TABLE } from "./config.ts";const client = await new Client();client.connect({ hostname: "127.0.0.1", username: "root", password: "", db: "",});export default client;
Algumas coisas que estão acontecendo aqui, estamos importando Client
de mysql
biblioteca. Client
nos ajudará a conectar-se ao nosso banco de dados e executar operações no banco de dados.
client.connect({ hostname: "127.0.0.1", username: "root", password: "", db: "",});
Client
fornece um método chamado connect
que leva em objeto onde podemos fornecer o hostname
username
password
& db
. Com essas informações, ele pode estabelecer uma conexão com nossa instância do MySQL.
Verifique se o seu
username
não tempassword
entrará em conflito com a conexão com a biblioteca de MySQL da deno. Se você não sabe como fazer isso, leia esta diretriz que escrevi[[[[ligação]
eu deixei
database
campo em branco aqui porque quero selecioná-lo manualmente no meu script que escreverei um pouco.
Vamos adicionar um script que inicialize um banco de dados chamado “deno”, selecione-o e dentro desse banco de dados crie uma tabela chamada “todo”
Dentro db/client.ts
arquivo, vamos fazer algumas novas adições.
import { Client } from "https://deno.land/x/mysql/mod.ts";// configimport { DATABASE, TABLE } from "./config.ts";const client = await new Client();client.connect({ hostname: "127.0.0.1", username: "root", password: "", db: "",});const run = async () => { // create database (if not created before) await client.execute(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`); // select db await client.execute(`USE ${DATABASE}`); // delete table if it exists before await client.execute(`DROP TABLE IF EXISTS ${TABLE.TODO}`); // create table await client.execute(` CREATE TABLE ${TABLE.TODO} ( id int(11) NOT NULL AUTO_INCREMENT, todo varchar(100) NOT NULL, isCompleted boolean NOT NULL default false, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `);};run();export default client;
O que está acontecendo de novo é que estamos importando DATABASE
& TABLE
do nosso arquivo de configuração e, em seguida, usar esses valores em uma nova função chamada run()
Vamos quebrar isso run()
, adicionei comentários no arquivo para ajudar você a entender o fluxo de trabalho.
const run = async () => { // create database (if not created before) await client.execute(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`); // select db await client.execute(`USE ${DATABASE}`); // delete table if it exists before await client.execute(`DROP TABLE IF EXISTS ${TABLE.TODO}`); // create table await client.execute(` CREATE TABLE ${TABLE.TODO} ( id int(11) NOT NULL AUTO_INCREMENT, todo varchar(100) NOT NULL, isCompleted boolean NOT NULL default false, PRIMARY KEY (id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8; `);};run();
- Crie um banco de dados chamado
deno
, se já existir, não faça nada. - Em seguida, selecione o banco de dados a ser usado. Que é chamado
deno
“ - Excluir tabela dentro
deno
chamadotodo
se já existe. - Em seguida, crie uma nova tabela dentro
deno
db e chame-otodo
& defina a estrutura que é a seguinte; ele terá um incremento automático exclusivoid
que será um número inteiro, outro campo chamadotodo
que será uma string e, finalmente, um campo chamadoisCompleted
que é um booleano. Eu também definoid
como minha chave primária.
A razão pela qual escrevi esse script foi por motivos pessoais, porque não quero ter informações extras na instância do MySQL. Toda vez que executo esse script, apenas reinicializamos tudo.
Você não precisa adicionar este script. Mas se você não o fizer, terá que criar manualmente um banco de dados e a tabela.
IMPORTANTE mencionar aqui que você também pode obter referências da biblioteca deno mysql, bem como criação de db e em criação de tabela.
Voltando à nossa agenda, acabamos de conseguir duas coisas das quatro mencionadas no topo, que é
- Crie uma conexão com o banco de dados MySQL
- Escreva um pequeno script que redefina o banco de dados toda vez que iniciarmos o servidor Deno
Isso já é 50% do tutorial. Infelizmente, não podemos ver muita coisa acontecendo agora. Vamos adicionar rapidamente algumas funcionalidades para vê-las funcionando
Executando operações CRUD em uma tabela e adicionando a funcionalidade aos nossos controladores de API
Precisamos atualizar nossa interface Todo primeiro, vá para interfaces/Todo.ts
arquivo e atualizar o conteúdo do arquivo
export default interface Todo { id?: number, todo?: string, isCompleted?: boolean,}
O que isso ?
faz é que torna a chave no objeto opcional. A razão pela qual eu fiz isso é. Em funções diferentes passarei um objeto com apenas id
ou todo, isCompleted
ou todos eles de uma só vez id, todo, isCompleted
que você verá mais tarde.
Se você quiser saber mais sobre propriedades opcionais em texto datilografado, vá até lá docs aqui para aprender mais sobre.
Em seguida, crie uma nova pasta chamada modelos & dentro dessa pasta, crie um arquivo chamado todo.ts. Vamos adicionar o seguinte conteúdo ao arquivo.
import client from "../db/client.ts";// configimport { TABLE } from "../db/config.ts";// Interfaceimport Todo from "../interfaces/Todo.ts";export default { /** * Takes in the id params & checks if the todo item exists * in the database * @param id * @returns boolean to tell if an entry of todo exits in table */ doesExistById: async ({ id }: Todo) => {}, /** * Will return all the entries in the todo column * @returns array of todos */ getAll: async () => {}, /** * Takes in the id params & returns the todo item found * against it. * @param id * @returns object of todo item */ getById: async ({ id }: Todo) => {}, /** * Adds a new todo item to todo table * @param todo * @param isCompleted */ add: async ( { todo, isCompleted }: Todo, ) => {}, /** * Updates the content of a single todo item * @param id * @param todo * @param isCompleted * @returns integer (count of effect rows) */ updateById: async ({ id, todo, isCompleted }: Todo) => {}, /** * Deletes a todo by ID * @param id * @returns integer (count of effect rows) */ deleteById: async ({ id }: Todo) => {},};
No momento, as funções estão vazias, mas tudo bem. Vamos preenchê-los um por um.
Em seguida, vá para controllers/todo.ts
e adicione o seguinte.
// interfacesimport Todo from "../interfaces/Todo.ts";// modelsimport TodoModel from "../models/todo.ts";export default { /** * @description Get all todos * @route GET /todos */ getAllTodos: async ({ response }: { response: any }) => {}, /** * @description Add a new todo * @route POST /todos */ createTodo: async ( { request, response }: { request: any; response: any }, ) => {}, /** * @description Get todo by id * @route GET todos/:id */ getTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => {}, /** * @description Update todo by id * @route PUT todos/:id */ updateTodoById: async ( { params, request, response }: { params: { id: string }; request: any; response: any; }, ) => {}, /** * @description Delete todo by id * @route DELETE todos/:id */ deleteTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => {},};
Aqui também temos funções vazias. Vamos começar a preenchê-los.
[Get] all API
Dentro models/todo.ts
adicionar definição para função getAll
o Client
também expõe outro método além connect
(usamos o método “connect” em db/client.ts
arquivo) e isso é query
. o client.query
método, vamos executar consultas MySQL diretamente do nosso código Deno como está.
Em seguida, vá para controllers/todo.ts
adicionar definição para getAllTodos
// interfacesimport Todo from "../interfaces/Todo.ts";// modelsimport TodoModel from "../models/todo.ts";export default { /** * @description Get all todos * @route GET /todos */ getAllTodos: async ({ response }: { response: any }) => { try { const data = await TodoModel.getAll(); response.status = 200; response.body = { success: true, data, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } },}
Tudo o que estamos fazendo é importar TodoModel
e usando o método chamado getAll
que acabamos de definir agora e, como retorna como uma promessa, envolvemos-no em assíncrono / espera.
O método TodoModel.getAll()
nos devolverá uma matriz à qual simplesmente retornamos response.body
com status
definido como 200
.
Se a promessa falhar ou houver outro erro, basta acessar nosso bloco catch e retornar um status de 400 com success
false e defina o message
para o que obtemos do bloco de captura como está.
É isso que terminamos. Vamos ligar o nosso terminal. Verifique se sua instância do MySQL está em execução. No seu tipo de terminal
$ deno run --allow-net server.ts
Seu terminal deve ser algo como isto
Meu console está me dizendo duas coisas aqui.
- Um servidor Deno API em execução na porta 8080
- Outro que minha instância do MySQL está executando
127.0.0.1
qual élocalhost
Vamos testar nossa API. estou usando carteiro aqui. Você pode usar seu cliente de API favorito.
No momento, ele nos retorna dados vazios, mas uma vez que adicionamos dados em nosso todo
mesa. Isso nos devolverá todos aqui.
Impressionante 1 API abaixo mais 4 para ir.
[Post] adicionar uma API de tarefas
No models/todo.ts
adicione a seguinte definição para add()
função
export default { /** * Adds a new todo item to todo table * @param todo * @param isCompleted */ add: async ( { todo, isCompleted }: Todo, ) => { return await client.query( `INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`, [ todo, isCompleted, ], ); },}
A função add recebe o objeto como argumento, que possui 2 itens todo & isCompleted
assim add: async ({ todo, isCompleted }: Todo) => {}
também pode ser escrito como ({todo, isCompleted}: {todo:string, isCompleted:boolean})
Mas como já temos uma interface definida em nosso interfaces/Todo.ts
arquivo que é
export default interface Todo { id?: number, todo?: string, isCompleted?: boolean,}
Podemos simplesmente escrever isso como add: async ({ todo, isCompleted }: Todo) => {}
que informa ao texto que esta função possui 2 argumentos todo
que é uma string e isCompleted
que é um booleano.
Se você quiser ler mais sobre interfaces. O texto datilografado possui um excelente documento. Que você pode encontrar aqui
Dentro de nossa função, temos o seguinte
return await client.query( `INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`, [ todo, isCompleted, ],);
Esta consulta pode ser dividida em 2 partes
INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)
Os dois pontos de interrogação aqui denotam um uso de variáveis dentro desta consulta.- A outra parte
[todo, isCompleted]
são as variáveis que irão no primeira parte da consulta e ser substituído por(?, ?)
Table.Todo
é apenas uma string que vem do arquivodb/config.ts
OndeTable.Todo
o valor é “todo
“
O próximo dentro da nossa controllers/todo.ts
vá para a definição de createTodo()
função
export default { /** * @description Add a new todo * @route POST /todos */ createTodo: async ( { request, response }: { request: any; response: any }, ) => { const body = await request.body(); if (!request.hasBody) { response.status = 400; response.body = { success: false, message: "No data provided", }; return; } try { await TodoModel.add( { todo: body.value.todo, isCompleted: false }, ); response.body = { success: true, message: "The record was added successfully", }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } },}
Vamos dividir isso em 2 partes
Parte 1
const body = await request.body();if (!request.hasBody) { response.status = 400; response.body = { success: false, message: "No data provided", }; return;}
Tudo o que estamos fazendo aqui é verificar se o usuário está enviando dados no corpo; caso contrário, retornamos um status 400
e no retorno do corpo success: false
& message:
Parte 2
try { await TodoModel.add( { todo: body.value.todo, isCompleted: false }, ); response.body = { success: true, message: "The record was added successfully", };} catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, };}
Se não houver erro TodoModel.add()
é chamada de função que acabamos de definir e simplesmente retornar um status de 200
& mensagem de confirmação ao usuário em resposta de retorno.
Caso contrário, basta lançar um erro semelhante ao que fizemos na API anterior.
É isso que terminamos. Vamos ligar o nosso terminal. Verifique se sua instância do MySQL está em execução. No seu tipo de terminal
$ deno run --allow-net server.ts
Vamos para carteiro & execute a rota da API para este controlador.
Isso é ótimo, agora temos 2 APIs funcionando. Apenas mais 3 para ir.
Sinta-se livre para dar uma olhada em todo o código fonte deste tutorial aqui
Se você é proveniente do tutorial do capítulo 1 e deseja ver a diferença entre os capítulos 1 e 2, pode ver a diferença de confirmações aqui
[GET] API de todo por id
Na tua models/todo.ts
arquivo adicionar definição dessas duas funções doesExistById()
& getById()
export default { /** * Takes in the id params & checks if the todo item exists * in the database * @param id * @returns boolean to tell if an entry of todo exits in table */ doesExistById: async ({ id }: Todo) => { const [result] = await client.query( `SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`, [id], ); return result.count > 0; }, /** * Takes in the id params & returns the todo item found * against it. * @param id * @returns object of todo item */ getById: async ({ id }: Todo) => { return await client.query( `SELECT * FROM ${TABLE.TODO} WHERE id = ?`, [id], ); },}
Vamos falar sobre cada função, uma por uma.
- doesExistById recebe um
id
& retorna umboolean
indicando se um todo específico existe no banco de dados ou não.
Vamos quebrar essa função
const [result] = await client.query( `SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`, [id],);return result.count > 0;
Simplesmente verificamos a contagem aqui na tabela em relação a um ID de tarefa específico; se a contagem for maior que zero, simplesmente retornamos true
de outra forma false
- getById retorna o item de tarefa em relação a um ID específico.
return await client.query( `SELECT * FROM ${TABLE.TODO} WHERE id = ?`, [id],);
Estamos simplesmente executando uma consulta MySQL aqui para obter um todo por id e retornando o resultado como está.
Em seguida, vá para o seu controllers/todo.ts
arquivar e definir definição para getTodoById
método de controlador.
export default { /** * @description Get todo by id * @route GET todos/:id */ getTodoById: async ( { params, response }: { params: { id: string }; response: any }, ) => { try { const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) }, ); if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return; } const todo = await TodoModel.getById({ id: Number(params.id) }); response.status = 200; response.body = { success: true, data: todo, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; } },}
Vamos dividir isso em duas partes menores
const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) },);if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return;}
Primeiro, verificamos se o todo existe no banco de dados novamente e não um ID usando este método
const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) },);
Aqui precisamos converter params.id
dentro de Number
porque nossa interface Todo aceita apenas id
como um número. Em seguida, apenas passamos params.id
para doesExistById
método. Este método retornará como um booleano.
Depois, basta verificar se o todo não está disponível, retornar um 404
com nossa resposta padrão, como nas APIs anteriores, conforme escrito abaixo.
if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return;}
Então nós temos
try {const todo: Todo = await TodoModel.getById({ id: Number(params.id) });response.status = 200;response.body = { success: true, data: todo,};} catch (error) {response.status = 400;response.body = { success: false, message: `Error: ${error}`,};
Isso é semelhante ao que estávamos fazendo nas APIs anteriores, aqui estamos simplesmente obtendo dados do banco de dados definindo-o como variável todo
e, em seguida, retornando a resposta e, se houver um erro, retorne ao usuário uma mensagem de erro padrão no bloco de captura.
É isso que terminamos. Vamos ligar o nosso terminal. Verifique se sua instância do MySQL está em execução. No seu tipo de terminal
$ deno run --allow-net server.ts
Vamos para carteiro & execute a rota da API para este controlador.
Como toda vez que reiniciamos nosso servidor, redefinimos nosso banco de dados. Se você não deseja esse comportamento, pode simplesmente comentar a função de execução no arquivo
db/client.ts -> run()
Grandes 3 APIs fizeram apenas mais 2 pela frente.
Até agora, criamos APIs para
- Obter todos os
- Crie um novo todo
- Obter um todo por ID
As APIs restantes são as seguintes
- Atualizar um todo por ID
- Excluir um todo por ID
[PUT] atualizar todo por API id
Vamos criar um modelo para esta API primeiro. Vá em nosso models/todo.ts
arquivo e adicionar definição para a função updateById
** * Updates the content of a single todo item * @param id * @param todo * @param isCompleted * @returns integer (count of effect rows) */updateById: async ({ id, todo, isCompleted }: Todo) => { const result = await client.query( `UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`, [ todo, isCompleted, id, ], ); // return count of rows updated return result.affectedRows;},
o updateById
leva em 3 params id, todo, isCompleted
Simplesmente executamos uma consulta MySQL dentro desta função
onst result = await client.query( `UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`, [ todo, isCompleted, id, ],);
O que atualiza um único item de tarefa todo
& isCompleted
por um específico id
Em seguida, simplesmente retornamos uma contagem de linhas atualizadas por esta consulta. Fazendo
// return count of rows updated return result.affectedRows;
A contagem será 0 ou 1, mas não será superior a 1. Como temos IDs exclusivos em nosso banco de dados e, ao mesmo tempo, vários nem todos podem existir com o mesmo ID.
Em seguida, vá para o nosso controllers/todo.ts
arquivo e adicionar definição para updateTodoById
função que é a seguinte
updateTodoById: async ( { params, request, response }: { params: { id: string }; request: any; response: any; },) => { try { const isAvailable = await TodoModel.doesExistById( { id: Number(params.id) }, ); if (!isAvailable) { response.status = 404; response.body = { success: false, message: "No todo found", }; return; } // if todo found then update todo const body = await request.body(); const updatedRows = await TodoModel.updateById({ id: Number(params.id), ...body.value, }); response.status = 200; response.body = { success: true, message: `Successfully updated ${updatedRows} row(s)`, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; }},
Isso é quase o mesmo das nossas APIs anteriores que acabamos de escrever. A parte nova aqui é essa;
// if todo found then update todoconst body = await request.body();const updatedRows = await TodoModel.updateById({ id: Number(params.id), ...body.value,});
Simplesmente obtemos o corpo que o usuário nos envia em JSON e passamos o corpo para o nosso TodoModel.updateById
função.
Temos que converter
id
to Number para estar em conformidade com a interface Todo
A consulta é executada e simplesmente retorna a contagem de linhas atualizadas, que simplesmente retornamos em nossa resposta e, se houver um erro, ela simplesmente vai para o bloco catch, onde retornamos nossa mensagem de resposta padrão.
Vamos executar isso e ver se funciona.
Vamos ligar o nosso terminal. Verifique se sua instância do MySQL está em execução. No seu tipo de terminal
$ deno run --allow-net server.ts
Vamos para carteiro & execute a rota da API para este controlador.
[DELETE] API de todo por id
Na tua models/todo.ts
arquivo criar uma função chamada deleteById
& defina como
/** * Deletes a todo by ID * @param id * @returns integer (count of effect rows) */deleteById: async ({ id }: Todo) => { const result = await client.query( `DELETE FROM ${TABLE.TODO} WHERE id = ?`, [id], ); // return count of rows updated return result.affectedRows;},
Aqui simplesmente passamos id
como um parâmetro e, em seguida, use a consulta de exclusão do MySQL. Em seguida, retornamos a contagem de linhas atualizadas. Qual será 0 ou 1, porque o ID de cada tarefa é único.
Em seguida, vá na sua controllers/todo.ts
arquivar e definir deleteByTodoId
método
/** * @description Delete todo by id * @route DELETE todos/:id */deleteTodoById: async ( { params, response }: { params: { id: string }; response: any },) => { try { const updatedRows = await TodoModel.deleteById({ id: Number(params.id), }); response.status = 200; response.body = { success: true, message: `Successfully updated ${updatedRows} row(s)`, }; } catch (error) { response.status = 400; response.body = { success: false, message: `Error: ${error}`, }; }},
Isso é bem direto, passamos o params.id
para nosso TodoModel.deleteById
método e, em seguida, retornamos a contagem de linhas atualizadas com esta consulta.
Se algo der errado, o erro é lançado no bloco catch, que retorna nossa resposta de erro padrão.
Vamos verificar isso.
Verifique se sua instância do MySQL está em execução. No seu tipo de terminal
$ deno run --allow-net server.ts
Vamos para carteiro & execute a rota da API para este controlador.
Com isso, terminamos o nosso tutorial deno + oak + mysql.
Todo o código está disponível aqui https://github.com/adeelibr/deno-playground Se você encontrou um problema, entre em contato ou sinta-se à vontade para fazer uma solicitação de recebimento. Darei crédito a você no repositório.
Se você achou útil, compartilhe-o e, como sempre, estou disponível em twitter @adeelibr. Gostaria de ouvir seus pensamentos sobre isso.