Jag skrev nyligen om hur man gör ett Todo API i Deno + Oak (utan att använda en databas) . Du hittar repet under chapter_1:oak på GitHub.
Den här handledningen fortsätter där den andra slutade, och jag ska gå igenom hur man integrerar MySQL i ett Deno och Oak-projekt.
Om du någon gång vill se hela källkoden som används i den här handledningen finns den tillgänglig på chapter_2:mysql . Ge den gärna en stjärna på GitHub om du gillar den.
Jag antar att du redan har slutfört den senaste handledningen som nämns ovan. Om inte, kolla in det här och kom tillbaka när du är klar.
Innan vi börjar, se till att du har en MySQL-klient installerad och kör:
- MySQL-gemenskapsserver [Ladda ner här]
- MySQL Workbench [Ladda ner här]
Jag skrev en liten guide för Mac OS-användare om att ställa in MySQL eftersom jag också kämpade med det. Kolla in det här.
Om du använder en Windows-dator kan du använda samma verktyg eller så kan du använda XAMPP för att köra en MySQL-instans i din instrumentpanel.
När du har en MySQL-instans igång kan vi börja vår handledning.
Låt oss börja
Förutsatt att du kommer från den här artikeln, Todo API i Deno + Oak (utan att använda en databas) , kommer vi att göra följande:
- Skapa en MySQL-databasanslutning
- Skriv ett litet skript som återställer databasen varje gång vi startar vår Deno-server
- Utför CRUD-operationer på ett bord
- Lägg till CRUD-funktionaliteten i våra API-kontroller
En sista sak – här är hela commit-skillnaden som gjordes i kapitel 1 för att lägga till MySQL till projektet (källkod som visar de nya tilläggen från kapitel 1).
I din projektrotmapp – min heter chapter_2:mysql
, även om din kan heta vad du vill – skapa en mapp som heter db . Inuti den mappen skapar du en fil som heter config.ts and lägg till följande innehåll:
export const DATABASE: string = "deno";
export const TABLE = {
TODO: "todo",
};
Inget märkvärdigt här, bara att definiera vårt databasnamn tillsammans med ett objekt för tabeller och sedan exportera det. Vårt projekt kommer att ha en databas som heter "deno" och inuti den db kommer vi bara att ha en tabell som heter "todo".
Därefter inuti db skapar du en annan fil som heter client.ts och lägg till följande innehåll:
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;
Ett par saker händer här.
Vi importerar Client
från mysql
bibliotek. Client
hjälper oss att ansluta till vår databas och utföra operationer i databasen.
client.connect({
hostname: "127.0.0.1",
username: "root",
password: "",
db: "",
});
Client
tillhandahåller en metod som heter connect
som tar in objekt där vi kan tillhandahålla hostname
, username
, password
och db
. Med denna information kan den upprätta en anslutning till vår MySQL-instans.
Se till att ditt username
har inget password
, eftersom det kommer i konflikt med att ansluta till Denos MySQL-bibliotek. Om du inte vet hur du gör det, läs den här handledningen jag skrev.
Jag har lämnat database
fältet tomt här eftersom jag vill välja det manuellt senare i mitt skript.
Låt oss lägga till ett skript som kommer att initiera en databas som heter "deno", markera den och inuti den db skapar en tabell som heter "todo".
Inuti db/client.ts
fil låt oss göra några nya tillägg:
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;
Här importerar vi DATABASE
och TABLE
från vår konfigurationsfil och sedan använda dessa värden i en ny funktion som heter run()
.
Låt oss dela upp denna run()
fungera. Jag har lagt till kommentarer i filen för att hjälpa dig förstå arbetsflödet:
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();
- Skapa en databas som heter
deno
. Gör ingenting om det redan finns. - Välj sedan den databas som ska användas, som kallas
deno
- Ta bort tabellen inuti
deno
kallastodo
om det redan finns. - Skapa sedan en ny tabell i
deno
db, kalla dettodo
, och definiera dess struktur:Den kommer att ha en unik automatisk ökning avid
som kommer att vara ett heltal, ett annat fält som hetertodo
som kommer att vara en sträng och slutligen ett fält som heterisCompleted
som är en boolean. Jag definierar ocksåid
som min primära nyckel.
Anledningen till att jag skrev det här skriptet var för att jag inte vill ha extra information i MySQL-instansen. Varje gång skriptet körs återinitieras allt.
Du behöver inte lägga till det här skriptet. Men om du inte gör det måste du skapa en db och tabellen manuellt.
Kolla också in Deno MySQL-bibliotekets dokument om db-skapande och om tabellskapande.
Om vi går tillbaka till vår agenda, vi har precis uppnått två saker av de fyra som nämns överst i artikeln:
- Skapa en MySQL-databasanslutning
- Skriv ett litet skript som återställer databasen varje gång vi startar vår Deno-server
Det är redan 50% av handledningen. Tyvärr kan vi inte se så mycket hända just nu. Låt oss snabbt lägga till några funktioner så att det fungerar.
Utföra CRUD-operationer på en tabell och lägga till funktionaliteten till våra API-kontrollanter
Vi måste uppdatera vårt Todo-gränssnitt först. Gå till interfaces/Todo.ts
fil och lägg till följande:
export default interface Todo {
id?: number,
todo?: string,
isCompleted?: boolean,
}
Vad är det här ?
gör det gör nyckeln i objektet valfri. Jag gjorde detta eftersom jag senare kommer att använda olika funktioner för att skicka objekt med bara ett id
, todo
, isCompleted
, eller alla på en gång.
Om du vill lära dig mer om valfria egenskaper i TypeScript, gå till deras dokument här.
Skapa sedan en ny mapp som heter modeller och i den mappen skapar du en fil som heter todo.ts . Lägg till följande innehåll i filen:
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) => {},
};
Just nu är funktionerna tomma, men det är okej. Vi kommer att fylla på dem en efter en.
Gå sedan till controllers/todo.ts
fil och se till att du lägger till följande:
// 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 },
) => {},
};
Här har vi tomma funktioner också. Låt oss börja fylla på dem.
[Hämta] alla todos API
Inuti models/todo.ts
, lägg till en definition för en funktion som heter 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
visar också en annan metod förutom connect
(vi använde en "anslut"-metod i db/client.ts
fil) och det är query
. client.query
metoden låter oss köra MySQL-frågor direkt från vår Deno-kod som den är.
Gå sedan till controllers/todo.ts
lägg till definition för 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}`,
};
}
},
}
Allt vi gör är att importera TodoModel
och använder dess metod som heter getAll
, som vi precis definierat nu. Eftersom det återkommer som ett löfte har vi inslaget i async/await.
Metoden TodoModel.getAll()
kommer att returnera oss en array som vi helt enkelt returnerar till response.body
med status
inställd på 200
.
Om löftet misslyckas eller om det finns ett annat fel, går vi helt enkelt till vårt fångstblock och returnerar en status på 400 med success
inställt på falskt. Vi ställer också in message
till vad vi får från fångstblocket.
Det var allt, vi är klara. Låt oss nu sätta igång vår terminal.
Se till att din MySQL-instans körs. I din terminal skriver du:
$ deno run --allow-net server.ts
Din terminal bör se ut ungefär så här:
Min konsol säger mig två saker här.
- Att min Deno API-server körs på port 8080
- Att min MySQL-instans körs på
127.0.0.1
, som ärlocalhost
Låt oss testa vårt API. Jag använder Postman här, men du kan använda din favorit API-klient.
Just nu returnerar den bara tomma data. Men när vi väl lägger till data i vår todo
tabell, kommer den att returnera dessa uppgifter här.
Grymt bra. Ett API ner och fyra till kvar.
[Inlägg] lägg till ett todo-API
I models/todo.ts
fil, lägg till följande definition för add()
funktion:
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-funktionen tar in objekt som ett argument, som har två objekt:todo
och isCompleted
.
Så add: async ({ todo, isCompleted }: Todo) => {}
kan också skrivas som ({todo, isCompleted}: {todo:string, isCompleted:boolean})
. Men eftersom vi redan har ett gränssnitt definierat i våra interfaces/Todo.ts
fil som är
export default interface Todo {
id?: number,
todo?: string,
isCompleted?: boolean,
}
vi kan helt enkelt skriva detta som add: async ({ todo, isCompleted }: Todo) => {}
. Detta talar om för TypeScript att den här funktionen har två argument, todo
, som är en sträng, och isCompleted
, som är ett booleskt värde.
Om du vill läsa mer om gränssnitt, har TypeScript ett utmärkt dokument om det som du kan hitta här.
Inuti vår funktion har vi följande:
return await client.query(
`INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)`,
[
todo,
isCompleted,
],
);
Den här frågan kan delas upp i två delar:
INSERT INTO ${TABLE.TODO}(todo, isCompleted) values(?, ?)
. De två frågetecknen här anger en användning av variabler i den här frågan.- Den andra delen,
[todo, isCompleted]
, är variablerna som kommer att gå i den första delen av frågan och ersättas med(?, ?)
Table.Todo
är bara en sträng som kommer från filendb/config.ts
därTable.Todo
värdet är "todo
"
Nästa i våra controllers/todo.ts
fil, gå till definitionen av createTodo()
funktion:
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}`,
};
}
},
}
Låt oss dela upp detta i två delar:
Del 1
const body = await request.body();
if (!request.hasBody) {
response.status = 400;
response.body = {
success: false,
message: "No data provided",
};
return;
}
Allt vi gör här är att kontrollera om användaren skickar data i kroppen. Om inte, returnerar vi en status 400
och returnerar success: false
i brödtexten och message: <erromessage-string>
.
Del 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}`,
};
}
Om det inte finns något fel, TodoModel.add()
funktionen anropas och returnerar helt enkelt statusen 200
och ett bekräftelsemeddelande till användaren.
Annars ger det bara ett liknande fel som vi gjorde i föregående API.
Nu är vi klara. Starta din terminal och se till att din MySQL-instans körs. I din terminal skriver du:
$ deno run --allow-net server.ts
Gå till Postman och kör API-rutten för denna kontroller:
Det här är bra, nu har vi två fungerande API:er. Bara tre kvar.
[GET] uppgift av id API
I dina models/todo.ts
fil, lägg till definition för dessa två funktioner, doesExistById()
och 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],
);
},
}
Låt oss prata om varje funktion en efter en:
doesExistById
tar in ettid
och returnerar enboolean
anger om en viss uppgift finns i databasen eller inte.
Låt oss dela upp den här funktionen:
const [result] = await client.query(
`SELECT COUNT(*) count FROM ${TABLE.TODO} WHERE id = ? LIMIT 1`,
[id],
);
return result.count > 0;
Vi kontrollerar helt enkelt antalet här i tabellen mot ett visst uppgifts-ID. Om antalet är större än noll returnerar vi true
. Annars returnerar vi false
.
getById
returnerar att göra-objektet mot ett visst id:
return await client.query(
`SELECT * FROM ${TABLE.TODO} WHERE id = ?`,
[id],
);
Vi kör helt enkelt en MySQL-fråga här för att få en uppgift med id och returnera resultatet som det är.
Gå sedan till dina controllers/todo.ts
fil och lägg till en definition för en getTodoById
kontrollmetod:
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}`,
};
}
},
}
Låt oss dela upp detta i två mindre delar:
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
Först kontrollerar vi om uppgiften finns i databasen mot ett id genom att använda denna metod:
const isAvailable = await TodoModel.doesExistById(
{ id: Number(params.id) },
);
Här måste vi konvertera params.id
till ett Number
eftersom vårt todo-gränssnitt endast accepterar id
som ett nummer. Därefter skickar vi bara params.id
till doesExistById
metod. Denna metod kommer att återvända som en boolean.
Sedan kontrollerar vi helt enkelt om uppgiften inte är tillgänglig och returnerar en 404
metod med vårt standardsvar som med de tidigare endpoints:
if (!isAvailable) {
response.status = 404;
response.body = {
success: false,
message: "No todo found",
};
return;
}
Då har vi:
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}`,
};
Detta liknar det vi gjorde i våra tidigare API:er. Här hämtar vi helt enkelt data från db, ställer in variabeln todo
och sedan returnera svaret. Om det finns ett fel returnerar vi helt enkelt ett standardfelmeddelande i fångstblocket till användaren.
Starta nu din terminal och se till att din MySQL-instans körs. I din terminal skriver du:
$ deno run --allow-net server.ts
Gå till Postman och kör API-rutten för denna kontroller.
Kom ihåg att varje gång vi startar om vår server så återställer vi db. Om du inte vill ha detta beteende kan du helt enkelt kommentera run
funktion i filen db/client.ts
.
Hittills har vi gjort API:er för:
- Hämta alla uppgifter
- Skapa en ny uppgift
- Få en uppgift med ID
Och här är de återstående API:erna:
- Uppdatera en uppgift med ID
- Ta bort en uppgift med ID
[PUT] uppdatera uppgift av id API
Låt oss skapa en modell för detta API först. Gå in i våra models/todo.ts
fil och lägg till en definition för en updateById
funktion:
**
* 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
tar in 3 parametrar:id
, todo
och isCompleted
.
Vi kör helt enkelt en MySQL-fråga i denna funktion:
onst result = await client.query(
`UPDATE ${TABLE.TODO} SET todo=?, isCompleted=? WHERE id=?`,
[
todo,
isCompleted,
id,
],
);
Detta uppdaterar todo
för ett enskilt att göra-objekt och isCompleted
med ett specifikt id
.
Därefter returnerar vi ett antal rader uppdaterade av denna fråga genom att göra:
// return count of rows updated
return result.affectedRows;
Antalet kommer antingen att vara 0 eller 1, men aldrig mer än 1. Detta beror på att vi har unika ID:n i vår databas – flera uppgifter med samma ID kan inte existera.
Gå sedan till våra controllers/todo.ts
fil och lägg till en definition för en updateTodoById
funktion:
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}`,
};
}
},
Detta är nästan samma som för våra tidigare API:er vi skrev. Den del som är ny här är denna:
// if todo found then update todo
const body = await request.body();
const updatedRows = await TodoModel.updateById({
id: Number(params.id),
...body.value,
});
Vi hämtar helt enkelt texten som användaren skickar oss i JSON och skickar texten till vår TodoModel.updateById
funktion.
Vi måste konvertera id
till ett nummer för att överensstämma med vårt Todo-gränssnitt.
Frågan exekveras och returnerar antalet uppdaterade rader. Därifrån returnerar vi det helt enkelt i vårt svar. Om det finns ett fel går det till catch-blocket där vi returnerar vårt standardsvarsmeddelande.
Låt oss köra detta och se om det fungerar. Se till att din MySQL-instans körs och kör följande från din terminal:
$ deno run --allow-net server.ts
Gå till Postman och kör API-rutten för denna kontroller:
[DELETE] uppgift av id API
I dina models/todo.ts
fil skapa en funktion som heter 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;
},
Här skickar vi helt enkelt ett id
som en param och använd sedan radera MySQL-frågan. Vi returnerar sedan det uppdaterade antalet rader. Det uppdaterade antalet kommer antingen att vara 0 eller 1 eftersom ID:t för varje uppgift är unikt.
Gå sedan till dina controllers/todo.ts
fil och definiera en deleteByTodoId
metod:
/**
* @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}`,
};
}
},
Det här är ganska okomplicerat. Vi skickar params.id
till vår TodoModel.deleteById
metod och returnerar antalet rader som uppdaterats med denna fråga.
Om något går fel slängs ett fel i catch-blocket som returnerar vårt standardfelsvar.
Låt oss kolla upp det här.
Se till att din MySQL-instans körs. I din terminal skriver du:
$ deno run --allow-net server.ts
Gå till Postman och kör API-rutten för denna kontroller:
Med detta är vi klara med vår Deno + Oak + MySQL tutorial.
Hela källkoden finns tillgänglig här:https://github.com/adelibr/deno-playground. Om du hittar ett problem är det bara att meddela mig. Eller gör gärna en pull-förfrågan så ger jag dig kredit i förvaret.
Om du tyckte att den här handledningen var användbar, vänligen dela den. Och som alltid är jag tillgänglig på Twitter under @adelibr. Jag skulle älska att höra dina tankar om det.