كيفية استخدام MySQL مع Deno و Oak: دليل شامل لبناء واجهات برمجة تطبيقات قوية

دقائق القراءة: 21

كيفية استخدام MySQL مع Deno و Oak: دليل شامل لبناء واجهات برمجة تطبيقات قوية

مرحباً بكم في منصة قيد! بصفتي خبيراً في تحسين محركات البحث ومحرراً تقنياً، يسعدني أن أقدم لكم هذا الدليل الشامل حول كيفية دمج قاعدة بيانات MySQL في مشاريع Deno التي تستخدم إطار عمل Oak. يأتي هذا المقال استكمالاً لدليل سابق تناول بناء واجهة برمجة تطبيقات (API) لإدارة المهام (Todo API) باستخدام Deno و Oak دون الاعتماد على قاعدة بيانات. إذا كنت قد تابعت ذلك الدليل، فستجد أن هذا الشرح سيبني على تلك الأساسات ليضيف طبقة هامة من استمرارية البيانات.

الهدف من هذا الدليل هو تمكينك من بناء واجهات برمجة تطبيقات قوية وفعالة قادرة على التفاعل مع قواعد بيانات MySQL، مما يفتح آفاقاً واسعة لتطوير تطبيقات ويب أكثر تعقيداً ومرونة. سنغطي جميع الخطوات الأساسية، بدءاً من إعداد الاتصال وصولاً إلى تنفيذ عمليات CRUD (الإنشاء، القراءة، التحديث، الحذف) بشكل كامل.

إذا كنت ترغب في الاطلاع على الكود المصدري الكامل لهذا الدليل، فهو متاح على GitHub في فرع chapter_2:mysql. لا تتردد في وضع نجمة (star) على المستودع إذا أعجبك المحتوى!

المتطلبات الأساسية قبل البدء

قبل الغوص في التفاصيل التقنية، من الضروري التأكد من توفر البيئة المناسبة. نفترض أنك قد أكملت الدليل السابق حول بناء Todo API في Deno + Oak. إذا لم تكن قد فعلت ذلك بعد، يُرجى مراجعته والعودة إلى هنا عند الانتهاء.

بالإضافة إلى ذلك، يجب أن يكون لديك عميل MySQL مثبت ويعمل بشكل صحيح على جهازك. إليك الأدوات الموصى بها:

بالنسبة لمستخدمي نظام التشغيل macOS، قد تواجهون بعض التحديات في إعداد MySQL. لقد قمت بكتابة دليل صغير لمساعدتكم في هذه العملية، يمكنكم الاطلاع عليه هنا. أما مستخدمو Windows، فيمكنهم استخدام نفس الأدوات أو الاستعانة بـ XAMPP لتشغيل نسخة MySQL بسهولة من لوحة التحكم الخاصة به.

بمجرد التأكد من أن لديك نسخة MySQL تعمل، يمكننا البدء في دمجها مع مشروعنا.

أهداف الدليل: ما الذي سنحققه؟

بافتراض أنك قادم من المقال السابق حول بناء Todo API في Deno + Oak (بدون قاعدة بيانات)، سنقوم بتحقيق الأهداف التالية في هذا الدليل:

  • إنشاء اتصال بقاعدة بيانات MySQL.
  • كتابة سكربت صغير يقوم بإعادة تهيئة قاعدة البيانات في كل مرة يتم فيها تشغيل خادم Deno.
  • تنفيذ عمليات CRUD (الإنشاء، القراءة، التحديث، الحذف) على جدول في قاعدة البيانات.
  • إضافة وظائف CRUD إلى متحكمات (controllers) واجهة برمجة التطبيقات الخاصة بنا.

لمن يرغب في رؤية الفرق الكامل في الكود بين الفصل الأول وهذا الفصل الذي يضيف MySQL، يمكنكم مراجعة هذا الرابط الذي يوضح الإضافات الجديدة.

إعداد بنية المشروع والاتصال بقاعدة البيانات

في مجلد الجذر لمشروعك (والذي أسميته chapter_2:mysql، لكن يمكنك تسميته كما تشاء)، قم بإنشاء مجلد جديد باسم db. داخل هذا المجلد، سننشئ ملفين أساسيين:

1. تعريف إعدادات قاعدة البيانات: ملف config.ts

داخل مجلد db، أنشئ ملفاً باسم config.ts وأضف المحتوى التالي إليه:

export const DATABASE: string = "deno";
export const TABLE = {
  TODO: "todo",
};

هنا، لا يوجد شيء معقد. نحن ببساطة نُعرّف اسم قاعدة البيانات الخاصة بنا (deno) بالإضافة إلى كائن (object) يحتوي على أسماء الجداول (في حالتنا، جدول واحد باسم todo)، ثم نقوم بتصديرها. سيحتوي مشروعنا على قاعدة بيانات واحدة تُسمى deno، وداخلها سيكون لدينا جدول واحد فقط يُسمى todo.

2. إعداد عميل MySQL: ملف client.ts

بعد ذلك، داخل مجلد 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 الخاصة بـ Deno. هذا الكلاس سيساعدنا في الاتصال بقاعدة البيانات وتنفيذ العمليات عليها.
  • نقوم باستيراد الثوابت DATABASE و TABLE من ملف الإعدادات config.ts الذي أنشأناه للتو.
  • نقوم بإنشاء نسخة جديدة من Client وتخزينها في المتغير client.
  • نستخدم التابع connect لإنشاء اتصال بقاعدة بيانات MySQL. يتطلب هذا التابع كائناً يحتوي على معلومات الاتصال مثل hostname (اسم المضيف، وعادة ما يكون 127.0.0.1 أو localhostusername (اسم المستخدم)، password (كلمة المرور)، و db (اسم قاعدة البيانات).

ملاحظة هامة: تأكد من أن اسم المستخدم username الخاص بك ليس له كلمة مرور password إذا كنت تستخدم إعدادات افتراضية بسيطة للتطوير، حيث قد يتعارض ذلك مع الاتصال بمكتبة MySQL في Deno. إذا كنت لا تعرف كيفية إعداد ذلك، يمكنك مراجعة هذا الدليل.

لقد تركنا حقل db فارغاً هنا عمداً، لأننا نرغب في تحديد قاعدة البيانات يدوياً لاحقاً ضمن السكربت الخاص بنا.

سكربت تهيئة قاعدة البيانات (اختياري لكن موصى به)

لتبسيط عملية التطوير وضمان بيئة نظيفة في كل مرة نبدأ فيها الخادم، سنضيف سكربتاً يقوم بتهيئة قاعدة البيانات. هذا السكربت سينشئ قاعدة بيانات باسم deno (إذا لم تكن موجودة)، ثم يختارها للاستخدام، وأخيراً ينشئ جدول 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(). دعنا نفصّل هذه الدالة:

  • await client.execute(`CREATE DATABASE IF NOT EXISTS ${DATABASE}`);: ينشئ قاعدة بيانات باسم deno إذا لم تكن موجودة بالفعل.
  • await client.execute(`USE ${DATABASE}`);: يحدد قاعدة البيانات deno لتكون هي قاعدة البيانات النشطة للاستخدام.
  • await client.execute(`DROP TABLE IF EXISTS ${TABLE.TODO}`);: يحذف الجدول todo من قاعدة البيانات deno إذا كان موجوداً مسبقاً. هذه الخطوة تضمن بيئة نظيفة في كل تشغيل.
  • await client.execute(`CREATE TABLE ${TABLE.TODO} (...)`);: ينشئ جدولاً جديداً باسم todo داخل قاعدة البيانات deno ويُعرّف هيكله كالتالي:
    • id: رقم صحيح (int(11))، لا يمكن أن يكون فارغاً (NOT NULL)، ويزداد تلقائياً (AUTO_INCREMENT). سيكون هو المفتاح الأساسي (PRIMARY KEY).
    • todo: سلسلة نصية (varchar(100))، لا يمكن أن تكون فارغة (NOT NULL).
    • isCompleted: قيمة منطقية (boolean)، لا يمكن أن تكون فارغة (NOT NULL)، وقيمتها الافتراضية هي false.

السبب وراء كتابة هذا السكربت هو تجنب تراكم البيانات غير الضرورية في نسخة MySQL الخاصة بنا. ففي كل مرة يتم فيها تشغيل السكربت، يقوم بإعادة تهيئة كل شيء. لست مضطراً لإضافة هذا السكربت، ولكن إذا لم تفعل، فسيتعين عليك إنشاء قاعدة البيانات والجدول يدوياً. يمكنك أيضاً مراجعة وثائق مكتبة Deno MySQL حول إنشاء قواعد البيانات والجداول لمزيد من التفاصيل.

بالعودة إلى أجندتنا، لقد حققنا الآن هدفين من الأربعة المذكورة في بداية المقال:

  • إنشاء اتصال بقاعدة بيانات MySQL.
  • كتابة سكربت صغير يقوم بإعادة تهيئة قاعدة البيانات في كل مرة نبدأ فيها خادم Deno.

هذا يمثل 50% من الدليل! للأسف، لا يمكننا رؤية الكثير يحدث الآن. دعنا نُضيف بعض الوظائف لرؤية النتائج.

تنفيذ عمليات CRUD على الجدول وإضافتها إلى متحكمات API

1. تحديث واجهة Todo

أولاً، نحتاج إلى تحديث واجهة Todo الخاصة بنا. انتقل إلى ملف interfaces/Todo.ts وأضف المحتوى التالي:

export default interface Todo {
  id?: number,
  todo?: string,
  isCompleted?: boolean,
}

علامة الاستفهام ? هنا تجعل الخاصية اختيارية في الكائن. لقد فعلت ذلك لأنني سأستخدم لاحقاً دوالاً مختلفة لتمرير كائنات تحتوي على id فقط، أو todo، أو isCompleted، أو جميعها مرة واحدة. إذا كنت ترغب في معرفة المزيد عن الخصائص الاختيارية في TypeScript، يمكنك التوجه إلى وثائقهم هنا.

2. إنشاء نماذج البيانات (Models): مجلد models

بعد ذلك، أنشئ مجلداً جديداً يُسمى 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) => {},
};

في الوقت الحالي، الدوال فارغة، وهذا أمر طبيعي. سنقوم بملئها واحدة تلو الأخرى.

3. تحديث متحكمات API: ملف controllers/todo.ts

انتقل إلى ملف 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 /todos)

داخل ملف 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 الخاص به، والذي قمنا بتعريفه للتو. بما أنه يُرجع وعداً (promise)، فقد قمنا بتغليفه بـ async/await. ستقوم الدالة TodoModel.getAll() بإرجاع مصفوفة نقوم ببساطة بإرجاعها إلى response.body مع تعيين status إلى 200. إذا فشل الوعد أو حدث خطأ آخر، ننتقل ببساطة إلى كتلة catch ونُرجع حالة 400 مع تعيين success إلى false. كما نقوم بتعيين message إلى ما نحصل عليه من كتلة catch. هذا كل شيء، لقد انتهينا.

الآن دعنا نشغل الطرفية الخاصة بنا. تأكد من أن نسخة MySQL الخاصة بك تعمل. في الطرفية الخاصة بك، اكتب:

$ deno run --allow-net server.ts

يجب أن تبدو الطرفية الخاصة بك كما يلي:

شاشة طرفية تعرض تشغيل خادم Deno على المنفذ 8080 واتصال MySQL

تُخبرني الطرفية الخاصة بي هنا بأمرين:

  • أن خادم Deno API الخاص بي يعمل على المنفذ 8080.
  • أن نسخة MySQL الخاصة بي تعمل على 127.0.0.1، وهو localhost.

دعنا نختبر واجهة برمجة التطبيقات الخاصة بنا. أنا أستخدم Postman هنا، ولكن يمكنك استخدام عميل API المفضل لديك.

لقطة شاشة لـ Postman تعرض طلب GET إلى localhost:8080/todos ونتيجة بيانات فارغة

في الوقت الحالي، تُرجع فقط بيانات فارغة. ولكن بمجرد أن نضيف بيانات إلى جدول todo الخاص بنا، ستُرجع هذه المهام هنا. ممتاز! لقد انتهينا من واجهة برمجة تطبيقات واحدة، وبقيت أربع أخرى.

تنفيذ واجهة برمجة تطبيقات إضافة مهمة جديدة (POST /todos)

في ملف 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}`,
      };
    }
  },
}

دعنا نقسم هذا إلى جزأين:

الجزء الأول: التحقق من وجود بيانات في الطلب

 const body = await request.body();
 if (!request.hasBody) {
  response.status = 400;
  response.body = {
   success: false,
   message: "No data provided",
  };
  return;
 }

كل ما نقوم به هنا هو التحقق مما إذا كان المستخدم يرسل بيانات في جسم الطلب (body). إذا لم يكن كذلك، فإننا نُرجع حالة 400 وفي جسم الاستجابة نُرجع success: false و message: <erromessage-string>.

الجزء الثاني: إضافة المهمة والتعامل مع الأخطاء

 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 ورسالة تأكيد للمستخدم. وإلا، فإنها تُلقي خطأ مشابهاً لما فعلناه في واجهة برمجة التطبيقات السابقة.

الآن انتهينا. قم بتشغيل الطرفية الخاصة بك وتأكد من أن نسخة MySQL الخاصة بك تعمل. في الطرفية الخاصة بك، اكتب:

$ deno run --allow-net server.ts

انتقل إلى Postman وقم بتشغيل مسار API لهذا المتحكم:

لقطة شاشة لـ Postman تعرض طلب POST إلى localhost:8080/todos لإضافة مهمة جديدة
لقطة شاشة لـ Postman تعرض طلب GET إلى localhost:8080/todos بعد إضافة مهمة جديدة، مع ظهور المهمة المضافة

هذا رائع، لدينا الآن واجهتا برمجة تطبيقات تعملان. بقيت ثلاث أخرى.

تنفيذ واجهة برمجة تطبيقات جلب مهمة بواسطة المعرف (GET /todos/: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 تُرجع عنصر المهمة مقابل معرف معين:

     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}`,
  };
 }

هذا مشابه لما كنا نفعله في واجهات برمجة التطبيقات السابقة. هنا، نحن ببساطة نحصل على البيانات من قاعدة البيانات، ونُعيّن المتغير todo، ثم نُرجع الاستجابة. إذا كان هناك خطأ، فإننا نُرجع ببساطة رسالة خطأ قياسية في كتلة catch إلى المستخدم.

الآن قم بتشغيل الطرفية الخاصة بك وتأكد من أن نسخة MySQL الخاصة بك تعمل. في الطرفية الخاصة بك، اكتب:

$ deno run --allow-net server.ts

انتقل إلى Postman وقم بتشغيل مسار API لهذا المتحكم. تذكر أنه في كل مرة نعيد فيها تشغيل خادمنا، فإننا نعيد تعيين قاعدة البيانات. إذا كنت لا تريد هذا السلوك، يمكنك ببساطة التعليق على الدالة run في ملف db/client.ts.

لقطة شاشة لـ Postman تعرض طلب POST لإضافة مهمة جديدة
لقطة شاشة لـ Postman تعرض طلب GET لجميع المهام بعد الإضافة
لقطة شاشة لـ Postman تعرض طلب GET لمهمة محددة بواسطة المعرف ونتيجتها
لقطة شاشة لـ Postman تعرض طلب GET لمهمة بمعرف غير موجود ونتيجة 404

حتى الآن، قمنا بإنشاء واجهات برمجة تطبيقات لـ:

  • جلب جميع المهام.
  • إنشاء مهمة جديدة.
  • جلب مهمة بواسطة المعرف.

وهنا واجهات برمجة التطبيقات المتبقية:

  • تحديث مهمة بواسطة المعرف.
  • حذف مهمة بواسطة المعرف.

تنفيذ واجهة برمجة تطبيقات تحديث مهمة بواسطة المعرف (PUT /todos/:id)

دعنا نُنشئ نموذجاً (model) لواجهة برمجة التطبيقات هذه أولاً. انتقل إلى ملف 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 ثلاثة وسائط: 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}`,
    };
  }
},

هذا يكاد يكون مطابقاً لواجهات برمجة التطبيقات السابقة التي كتبناها. الجزء الجديد هنا هو هذا:

 // 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 لهذا المتحكم:

لقطة شاشة لـ Postman تعرض طلب PUT لتحديث مهمة بواسطة المعرف
لقطة شاشة لـ Postman تعرض طلب GET لجميع المهام بعد التحديث، مع ظهور المهمة المحدثة

تنفيذ واجهة برمجة تطبيقات حذف مهمة بواسطة المعرف (DELETE /todos/: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 لهذا المتحكم:

لقطة شاشة لـ Postman تعرض طلب GET لجميع المهام قبل الحذف
لقطة شاشة لـ Postman تعرض طلب DELETE لحذف مهمة بواسطة المعرف
لقطة شاشة لـ Postman تعرض طلب GET لجميع المهام بعد الحذف، مع اختفاء المهمة المحذوفة

بهذا، نكون قد انتهينا من دليلنا حول Deno + Oak + MySQL. الكود المصدري الكامل متاح هنا: https://github.com/adeelibr/deno-playground. إذا وجدت مشكلة، فقط أخبرني. أو لا تتردد في تقديم طلب سحب (pull request) وسأمنحك الفضل في المستودع.

إذا وجدت هذا الدليل مفيداً، يُرجى مشاركته. وكالعادة، أنا متاح على Twitter تحت اسم @adeelibr. أود أن أسمع أفكاركم حوله.

الخلاصة التقنية

لقد أثبت هذا الدليل بوضوح قوة ومرونة استخدام Deno مع إطار عمل Oak وقاعدة بيانات MySQL لبناء واجهات برمجة تطبيقات (APIs) كاملة الميزات. من خلال تبني TypeScript، نستفيد من التحقق من النوع (type-checking) الذي يعزز موثوقية الكود ويقلل الأخطاء، خاصة عند التعامل مع هياكل البيانات المعقدة وعمليات قاعدة البيانات. إن تصميم فصل طبقة النموذج (Model layer) عن المتحكمات (Controllers) يتبع مبادئ التصميم الجيدة، مما يجعل الكود أكثر قابلية للصيانة والتوسع والاختبار. كما أن استخدام الاستعلامات المُجهزة (prepared statements) مع العلامات الاستفهامية (?) يساهم بشكل كبير في حماية التطبيق من هجمات حقن SQL، وهي ممارسة أمنية أساسية في تطوير الويب الحديث. هذا التكامل يوفر بيئة تطوير قوية وفعالة، مثالية للمطورين الذين يبحثون عن بديل حديث وقوي لـ Node.js.

اترك تعليقاً

لن يتم نشر عنوان بريدك الإلكتروني. الحقول الإلزامية مشار إليها بـ *