Наскоро писах за това как да направя Todo API в Deno + Oak (без да използвате база данни) . Можете да намерите репото под chapter_1:oak на GitHub.
Този урок продължава откъдето е спрял другият и ще разгледам как да интегрирам MySQL в проект на Deno и Oak.
Ако по всяко време искате да видите целия изходен код, използван в този урок, той е наличен на chapter_2:mysql . Чувствайте се свободни да му дадете звезда в GitHub, ако ви харесва.
Предполагам, че вече сте завършили последния урок, споменат по-горе. Ако не, проверете го тук и се върнете, когато приключите.
Преди да започнем, уверете се, че имате инсталиран и работещ MySQL клиент:
- Сървър на общността на MySQL [Изтеглете тук]
- MySQL Workbench [Изтеглете тук]
Написах малко ръководство за потребители на Mac OS относно настройката на MySQL, защото и аз се борих с него. Вижте го тук.
Ако сте на машина с Windows, можете да използвате същите инструменти или можете също да използвате XAMPP, за да имате MySQL екземпляр, работещ във вашето табло.
След като стартирате MySQL екземпляр, можем да започнем нашия урок.
Да започнем
Ако приемем, че идвате от тази статия, Todo API в Deno + Oak (без използване на база данни) , ще направим следното:
- Създайте връзка с MySQL база данни
- Напишете малък скрипт, който нулира базата данни всеки път, когато стартираме нашия Deno сървър
- Извършване на CRUD операции върху маса
- Добавете CRUD функционалността към нашите API контролери
И последно нещо – ето цялата разлика в комит, която беше направена в Глава 1 за добавяне на MySQL към проекта (изходен код, който показва новите допълнения, направени от глава 1).
В основната папка на вашия проект – моята се нарича chapter_2:mysql
, все пак вашият може да се нарича както искате – създайте папка, наречена db . В тази папка създайте файл, наречен config.ts и добавете към него следното съдържание:
export const DATABASE: string = "deno";
export const TABLE = {
TODO: "todo",
};
Тук няма нищо фантастично, просто дефинираме името на нашата база данни заедно с обект за таблици и след това го експортираме. Нашият проект ще има една база данни, наречена "deno" и вътре в тази db ще имаме само една таблица, наречена "todo".
След това вътре в db папка, създайте друг файл, наречен client.ts и добавете следното съдържание:
import { Client } from "https://deno.land/x/mysql/mod.ts";
// config
import { DATABASE, TABLE } from "./config.ts";
const client = await new Client();
client.connect({
hostname: "127.0.0.1",
username: "root",
password: "",
db: "",
});
export default client;
Тук се случват няколко неща.
Импортираме Client
от mysql
библиотека. Client
ще ни помогне да се свържем с нашата база данни и да извършим операции в базата данни.
client.connect({
hostname: "127.0.0.1",
username: "root",
password: "",
db: "",
});
Client
предоставя метод, наречен connect
който приема обект, където можем да предоставим hostname
, username
, password
и db
. С тази информация може да установи връзка с нашия MySQL екземпляр.
Уверете се, че вашето username
няма password
, тъй като ще противоречи на свързването с MySQL библиотеката на Deno. Ако не знаете как да направите това, прочетете този урок, който написах.
Напуснах database
полето е празно тук, защото искам да го избера ръчно по-късно в моя скрипт.
Нека добавим скрипт, който ще инициализира база данни, наречена "deno", ще я изберете и вътре в този db ще създадем таблица, наречена "todo".
Вътре в db/client.ts
файл нека направим някои нови допълнения:
import { Client } from "https://deno.land/x/mysql/mod.ts";
// config
import { 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;
Тук импортираме DATABASE
и TABLE
от нашия конфигурационен файл, след което използвайки тези стойности в нова функция, наречена run()
.
Нека разбием този run()
функция. Добавих коментари във файла, за да ви помогна да разберете работния процес:
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();
- Създайте база данни, наречена
deno
. Ако вече съществува, не правете нищо. - След това изберете базата данни, която да използвате, която се нарича
deno
- Изтрийте таблицата вътре в
deno
нареченtodo
ако вече съществува. - След това създайте нова таблица в
deno
db, наречете гоtodo
, и дефинирайте неговата структура:Той ще има уникално автоматично увеличениеid
което ще бъде цяло число, друго поле, нареченоtodo
който ще бъде низ и накрая поле, нареченоisCompleted
което е булева. Дефинирам същоid
като мой първичен ключ.
Причината да напиша този скрипт беше, че не искам да имам допълнителна информация в MySQL екземпляр. Всеки път, когато скриптът се изпълнява, той просто инициализира всичко отново.
Не е нужно да добавяте този скрипт. Но ако не го направите, тогава ще трябва ръчно да създадете db и таблицата.
Също така, разгледайте документите на библиотеката Deno MySQL относно създаването на db и създаването на таблица.
Връщайки се към нашия дневен ред, току-що постигнахме две неща от четирите, споменати в горната част на статията:
- Създайте връзка с MySQL база данни
- Напишете малък скрипт, който нулира базата данни всеки път, когато стартираме нашия Deno сървър
Това вече е 50% от урока. За съжаление не можем да видим какво се случва в момента. Нека бързо да добавим някои функции, за да видим как работи.
Извършване на CRUD операции върху таблица и добавяне на функционалността към нашите API контролери
Първо трябва да актуализираме нашия Todo интерфейс. Отидете на interfaces/Todo.ts
файл и добавете следното:
export default interface Todo {
id?: number,
todo?: string,
isCompleted?: boolean,
}
Какво е това ?
прави е, че прави ключа в обекта незадължителен. Направих това, защото по-късно ще използвам различни функции за предаване на обекти само с id
, todo
, isCompleted
, или всички наведнъж.
Ако искате да научите повече за незадължителните свойства в TypeScript, преминете към техните документи тук.
След това създайте нова папка, наречена models и в тази папка създайте файл, наречен todo.ts . Добавете следното съдържание към файла:
import client from "../db/client.ts";
// config
import { TABLE } from "../db/config.ts";
// Interface
import 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) => {},
};
В момента функциите са празни, но това е добре. Ще ги пълним един по един.
След това отидете на controllers/todo.ts
файл и се уверете, че сте добавили следното:
// interfaces
import Todo from "../interfaces/Todo.ts";
// models
import 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 },
) => {},
};
Тук имаме и празни функции. Нека започнем да ги пълним.
[Get] all todos API
Вътре в models/todo.ts
, добавете дефиниция за функция, наречена getAll
:
import client from "../db/client.ts";
// config
import { TABLE } from "../db/config.ts";
// Interface
import Todo from "../interfaces/Todo.ts";
export default {
/**
* Will return all the entries in the todo column
* @returns array of todos
*/
getAll: async () => {
return await client.query(`SELECT * FROM ${TABLE.TODO}`);
},
}
Client
също така разкрива друг метод освен connect
(използвахме метод за свързване в db/client.ts
файл) и това е query
. client.query
метод ни позволява да изпълняваме MySQL заявки директно от нашия Deno код такъв, какъвто е.
След това отидете на controllers/todo.ts
добавете дефиниция за getAllTodos
:
// interfaces
import Todo from "../interfaces/Todo.ts";
// models
import 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}`,
};
}
},
}
Всичко, което правим, е да импортираме TodoModel
и използвайки неговия метод, наречен getAll
, който току-що дефинирахме сега. Тъй като се връща като обещание, ние го обвихме в async/await.
Методът TodoModel.getAll()
ще ни върне масив, който просто връщаме в response.body
с status
зададен на 200
.
Ако обещанието се провали или има друга грешка, просто отиваме в нашия блок catch и връщаме състояние 400 с success
зададено на false. Ние също така задаваме message
до това, което получаваме от блока за хващане.
Това е, приключихме. Сега нека задействаме нашия терминал.
Уверете се, че вашият MySQL екземпляр работи. Във вашия терминал въведете:
$ deno run --allow-net server.ts
Вашият терминал трябва да изглежда така:
Моята конзола ми казва две неща тук.
- Че моят Deno API сървър работи на порт 8080
- Че моят MySQL екземпляр работи на
127.0.0.1
, което еlocalhost
Нека тестваме нашия API. Тук използвам Postman, но вие можете да използвате любимия си API клиент.
В момента връща само празни данни. Но след като добавим данни към нашата todo
таблица, ще върне тези задачи тук.
Страхотно. Един API приключи и остават още четири.
[Post] добавяне на API за задачи
В models/todo.ts
файл, добавете следната дефиниция за add()
функция:
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,
],
);
},
}
Функцията add приема обект като аргумент, който има два елемента:todo
и isCompleted
.
Така че add: async ({ todo, isCompleted }: Todo) => {}
може да се запише и като ({todo, isCompleted}: {todo:string, isCompleted:boolean})
. Но тъй като вече имаме интерфейс, дефиниран в нашия interfaces/Todo.ts
файл, който е
export default interface Todo {
id?: number,
todo?: string,
isCompleted?: boolean,
}
можем просто да напишем това като add: async ({ todo, isCompleted }: Todo) => {}
. Това казва на TypeScript, че тази функция има два аргумента, todo
, което е низ, и isCompleted
, което е булева.
Ако искате да прочетете повече за интерфейсите, TypeScript има отличен документ за него, който можете да намерите тук.
В нашата функция имаме следното:
return await client.query(
`INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`,
[
todo,
isCompleted,
],
);
Тази заявка може да бъде разделена на две части:
INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)
. Двата въпросителни тук означават използване на променливи в тази заявка.- Другата част,
[todo, isCompleted]
, е променливите, които ще бъдат включени в първата част на заявката и да бъде заменен с(?, ?)
Table.Todo
е просто низ, идващ от файлdb/config.ts
къдетоTable.Todo
стойността е "todo
"
Следва в нашите controllers/todo.ts
файл, отидете на дефиницията на createTodo()
функция:
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}`,
};
}
},
}
Нека го разделим на две части:
Част 1
const body = await request.body();
if (!request.hasBody) {
response.status = 400;
response.body = {
success: false,
message: "No data provided",
};
return;
}
Всичко, което правим тук, е да проверяваме дали потребителят изпраща данни в тялото. Ако не, тогава връщаме състояние 400
и в тялото върнете success: false
и message: <erromessage-string>
.
Част 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}`,
};
}
Ако няма грешка, TodoModel.add()
функцията се извиква и просто връща състояние 200
и съобщение за потвърждение до потребителя.
В противен случай просто хвърля подобна грешка, която направихме в предишния API.
Сега сме готови. Стартирайте терминала си и се уверете, че вашият MySQL екземпляр работи. Във вашия терминал въведете:
$ deno run --allow-net server.ts
Отидете на Postman и стартирайте API маршрута за този контролер:
Това е страхотно, сега имаме два работещи API. Остават само още три.
[GET] API за задачи по id
Във вашия models/todo.ts
файл, добавете дефиниция за тези две функции, 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],
);
},
}
Нека поговорим за всяка функция една по една:
doesExistById
приемаid
и връщаboolean
указващ дали конкретна задача съществува в базата данни или не.
Нека разбием тази функция:
const [result] = await client.query(
`SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`,
[id],
);
return result.count > 0;
Ние просто проверяваме броя тук в таблицата спрямо конкретен идентификатор на задача. Ако броят е по-голям от нула, връщаме true
. В противен случай връщаме false
.
getById
връща елемента todo срещу определен идентификатор:
return await client.query(
`SELECT * FROM ${TABLE.TODO} WHERE id = ?`,
[id],
);
Ние просто изпълняваме MySQL заявка тук, за да получим задача по идентификатор и връщаме резултата такъв, какъвто е.
След това отидете на вашия controllers/todo.ts
файл и добавете дефиниция за getTodoById
метод на контролера:
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}`,
};
}
},
}
Нека го разделим на две по-малки части:
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
Първо проверяваме дали задачата съществува в базата данни срещу идентификатор, като използваме този метод:
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
Тук трябва да преобразуваме params.id
в Number
защото нашият интерфейс за задачи приема само id
като число. След това просто предаваме params.id
към doesExistById
метод. Този метод ще се върне като булев.
След това просто проверяваме дали задачата не е налична и връщаме 404
метод с нашия стандартен отговор, както с предишните крайни точки:
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
Тогава имаме:
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}`,
};
Това е подобно на това, което правехме в предишните ни API. Тук просто получаваме данни от db, задавайки променливата todo
и след това връщане на отговора. Ако има грешка, ние просто връщаме стандартно съобщение за грешка в блока catch обратно на потребителя.
Сега стартирайте терминала си и се уверете, че вашият MySQL екземпляр работи. Във вашия терминал въведете:
$ deno run --allow-net server.ts
Отидете на Postman и стартирайте API маршрута за този контролер.
Не забравяйте, че всеки път, когато рестартираме нашия сървър, ние нулираме db. Ако не искате това поведение, можете просто да коментирате run
функция във файла db/client.ts
.
Досега сме правили API за:
- Вземете всички задачи
- Създайте нова задача
- Вземете задача по ID
А ето и останалите API:
- Актуализиране на задача по ID
- Изтриване на задача по ID
[PUT] актуализация на задачата по идентификатор API
Нека първо създадем модел за този API. Отидете в нашия models/todo.ts
файл и добавете дефиниция за 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;
},
updateById
приема 3 параметъра:id
, todo
и isCompleted
.
Просто изпълняваме MySQL заявка в тази функция:
onst result = await client.query(
`UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`,
[
todo,
isCompleted,
id,
],
);
Това актуализира todo
на един елемент задача и isCompleted
чрез конкретен id
.
След това връщаме броя на редовете, актуализирани от тази заявка, като направим:
// return count of rows updated
return result.affectedRows;
Броят ще бъде или 0, или 1, но никога повече от 1. Това е така, защото имаме уникални идентификатори в нашата база данни – множество задачи с един и същ идентификатор не могат да съществуват.
След това отидете на нашия controllers/todo.ts
файл и добавете дефиниция за updateTodoById
функция:
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}`,
};
}
},
Това е почти същото като на предишните ни API, които написахме. Частта, която е нова тук, е следната:
// if todo found then update todo
const body = await request.body();
const updatedRows = await TodoModel.updateById({
id: Number(params.id),
...body.value,
});
Ние просто получаваме тялото, което потребителят ни изпраща в JSON, и предаваме тялото на нашия TodoModel.updateById
функция.
Трябва да преобразуваме id
до номер, който да отговаря на нашия Todo интерфейс.
Заявката се изпълнява и връща броя на актуализираните редове. Оттам просто го връщаме в нашия отговор. Ако има грешка, тя отива в блока catch, където връщаме нашето стандартно съобщение за отговор.
Нека стартираме това и да видим дали работи. Уверете се, че вашият MySQL екземпляр работи и стартирайте следното от вашия терминал:
$ deno run --allow-net server.ts
Отидете на Postman и стартирайте API маршрута за този контролер:
[DELETE] API за задачи по id
Във вашия models/todo.ts
файл създайте функция, наречена deleteById
:
/**
* 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;
},
Тук просто предаваме id
като параметър и след това използвайте заявката за изтриване на MySQL. След това връщаме актуализирания брой редове. Актуализираният брой ще бъде 0 или 1, тъй като идентификаторът на всяка задача е уникален.
След това отидете във вашия controllers/todo.ts
файл и дефинирайте deleteByTodoId
метод:
/**
* @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}`,
};
}
},
Това е доста просто. Предаваме params.id
към нашия TodoModel.deleteById
метод и върнете броя на редовете, актуализирани с тази заявка.
Ако нещо се обърка, в блока catch се извежда грешка, която връща стандартния ни отговор за грешка.
Нека да проверим това.
Уверете се, че вашият MySQL екземпляр работи. Във вашия терминал въведете:
$ deno run --allow-net server.ts
Отидете на Postman и стартирайте API маршрута за този контролер:
С това приключихме с нашия урок за Deno + Oak + MySQL.
Целият изходен код е достъпен тук:https://github.com/adeelibr/deno-playground. Ако откриете проблем, просто ме уведомете. Или не се колебайте да направите заявка за изтегляне и аз ще ви дам кредит в хранилището.
Ако намерите този урок за полезен, моля, споделете го. И както винаги, аз съм достъпен в Twitter под @adeelibr. Ще се радвам да чуя вашите мисли по въпроса.