الجزء 3: البرمجة من جهة الخادم

حفظ البيانات في MongoDB

قبل أن ننتقل إلى الموضوع الرئيسي وهو حفظ البيانات في قاعدة بيانات، سنلقي نظرة على بضع طرق مختلفة لتصحيح أخطاء تطبيقات Node.

تصحيح أخطاء تطبيقات Node

تصحيح أخطاء تطبيقات Node أصعب قليلاً من تصحيح أخطاء JavaScript التي تعمل في متصفحك. الطباعة إلى الطرفية طريقة مجرَّبة وموثوقة، ويستحق فعلها دائماً. يعتقد بعض الناس أنه ينبغي استخدام طرق أكثر تطوراً بدلاً منها، لكنني لا أوافق. حتى نخبة مطوّري المصادر المفتوحة في العالم يستخدمون هذه الطريقة.

Visual Studio Code

قد يكون مصحّح أخطاء Visual Studio Code مفيداً في بعض الحالات. يمكنك تشغيل التطبيق في وضع تصحيح الأخطاء هكذا (في هذه الصورة والصور القليلة التالية، تحتوي الملاحظات على حقل date أُزيل من الإصدار الحالي من التطبيق):

لقطة شاشة توضح كيفية تشغيل مصحّح الأخطاء في vscode

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

ملاحظة قد يحتوي إصدار أحدث من Visual Studio Code على Run بدلاً من Debug. علاوة على ذلك، قد تحتاج إلى ضبط ملف launch.json لبدء تصحيح الأخطاء. يمكن فعل ذلك باختيار Add Configuration... من القائمة المنسدلة، الموجودة بجوار زر التشغيل الأخضر وفوق قائمة VARIABLES، ثم اختيار Run "npm start" in a debug terminal. للحصول على تعليمات إعداد أكثر تفصيلاً، زُر توثيق تصحيح الأخطاء في Visual Studio Code.

أدناه ترى لقطة شاشة توقّف فيها تنفيذ الشيفرة في منتصف حفظ ملاحظة جديدة:

لقطة شاشة vscode للتنفيذ عند نقطة توقف

توقّف التنفيذ عند نقطة التوقف في السطر 69. في وحدة التحكم، يمكنك رؤية قيمة المتغير note. وفي النافذة العلوية اليسرى، يمكنك رؤية أمور أخرى تتعلق بحالة التطبيق.

يمكن استخدام الأسهم في الأعلى للتحكم في سير مصحّح الأخطاء.

لسبب ما، لا أستخدم مصحّح أخطاء Visual Studio Code كثيراً.

أدوات مطوّري Chrome

يمكن تصحيح الأخطاء أيضاً باستخدام وحدة تحكم مطوّري Chrome عبر تشغيل تطبيقك بالأمر:

node --inspect index.js

يمكنك الوصول إلى مصحّح الأخطاء بالنقر على الأيقونة الخضراء - شعار node - التي تظهر في وحدة تحكم مطوّري Chrome:

أدوات المطوّر مع أيقونة شعار node الخضراء

يعمل عرض تصحيح الأخطاء بالطريقة نفسها التي كان عليها مع تطبيقات React. يمكن استخدام تبويب Sources لضبط نقاط التوقف حيث سيُوقَف تنفيذ الشيفرة.

تبويب sources في أدوات المطوّر مع نقطة توقف ومراقبة المتغيرات

ستظهر جميع رسائل console.log الخاصة بالتطبيق في تبويب Console في مصحّح الأخطاء. يمكنك أيضاً فحص قيم المتغيرات وتنفيذ شيفرة JavaScript الخاصة بك.

تبويب console في أدوات المطوّر يعرض كائن note مكتوباً

شكّ في كل شيء

قد يبدو تصحيح أخطاء تطبيقات Full Stack صعباً في البداية. قريباً سيكون لتطبيقنا قاعدة بيانات إضافة إلى الواجهة الأمامية والواجهة الخلفية، وستكون هناك مجالات كثيرة محتملة للأخطاء في التطبيق.

عندما «لا يعمل» التطبيق، علينا أولاً أن نكتشف أين تحدث المشكلة فعلاً. من الشائع جداً أن تكون المشكلة في مكان لم تتوقعه، وقد يستغرق العثور على مصدر المشكلة دقائق أو ساعات أو حتى أياماً.

المفتاح هو أن تكون منهجياً. وبما أن المشكلة قد توجد في أي مكان، عليك أن تشك في كل شيء، وأن تستبعد جميع الاحتمالات واحداً واحداً. سيساعدك التسجيل في الطرفية وPostman ومصحّحات الأخطاء والخبرة.

عندما تظهر الأخطاء، فإن أسوأ الاستراتيجيات الممكنة جميعها هي مواصلة كتابة الشيفرة. فذلك يضمن أن شيفرتك ستصاب قريباً بأخطاء أكثر، وأن تصحيحها سيكون أصعب. مبدأ Jidoka (توقف وأصلح) من أنظمة إنتاج تويوتا فعّال جداً في هذه الحالة أيضاً.

MongoDB

لحفظ ملاحظاتنا المحفوظة إلى أجل غير مسمى، نحتاج إلى قاعدة بيانات. تستخدم معظم المقررات التي تُدرَّس في جامعة هلسنكي قواعد بيانات علائقية. في معظم أجزاء هذه الدورة، سنستخدم MongoDB وهي قاعدة بيانات وثائقية.

سبب استخدام Mongo كقاعدة بيانات هو تعقيدها الأقل مقارنة بقاعدة بيانات علائقية. يعرض الجزء 13 من الدورة كيفية بناء واجهات خلفية بـ Node.js تستخدم قاعدة بيانات علائقية.

تختلف قواعد البيانات الوثائقية عن قواعد البيانات العلائقية في طريقة تنظيمها للبيانات وكذلك في لغات الاستعلام التي تدعمها. تُصنَّف قواعد البيانات الوثائقية عادةً ضمن المصطلح الشامل NoSQL.

يمكنك قراءة المزيد عن قواعد البيانات الوثائقية وNoSQL في مادة مقرر مقدمة في قواعد البيانات الأسبوع 7. للأسف، المادة متاحة حالياً بالفنلندية فقط.

اقرأ الآن الفصلين عن المجموعات والوثائق من دليل MongoDB للحصول على فكرة أساسية عن كيفية تخزين قاعدة بيانات وثائقية للبيانات.

بطبيعة الحال، يمكنك تثبيت MongoDB وتشغيلها على حاسوبك. غير أن الإنترنت مليء أيضاً بخدمات قواعد بيانات Mongo التي يمكنك استخدامها. مزوّد MongoDB المفضّل لدينا في هذه الدورة سيكون MongoDB Atlas.

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

mongodb اختيار shared وAWS والمنطقة

المزوّد المختار هو AWS والمنطقة هي Stockholm (eu-north-1). لاحظ أنك إذا اخترت شيئاً آخر، فسيكون نص اتصال قاعدة بياناتك مختلفاً قليلاً عن هذا المثال. انتظر حتى يصبح العنقود جاهزاً، وهو ما سيستغرق بضع دقائق.

ملاحظة لا تتابع قبل أن يصبح العنقود جاهزاً.

لنستخدم تبويب security لإنشاء بيانات اعتماد مستخدم لقاعدة البيانات. يرجى الانتباه إلى أن هذه ليست بيانات الاعتماد نفسها التي تستخدمها لتسجيل الدخول إلى MongoDB Atlas. ستُستخدم هذه ليتصل تطبيقك بقاعدة البيانات.

البداية السريعة لأمان mongodb

بعد ذلك، علينا تحديد عناوين IP المسموح لها بالوصول إلى قاعدة البيانات. للتبسيط، سنسمح بالوصول من جميع عناوين IP:

mongodb الوصول إلى الشبكة/إضافة قائمة عناوين IP

ملاحظة: إذا كانت القائمة المنبثقة مختلفة لديك، فوفقاً لتوثيق MongoDB، فإن إضافة 0.0.0.0 كعنوان IP تتيح الوصول من أي مكان أيضاً.

أخيراً، أصبحنا مستعدين للاتصال بقاعدة بياناتنا. لفعل ذلك، نحتاج إلى نص اتصال قاعدة البيانات، ويمكن إيجاده باختيار Connect ثم Drivers من العرض، ضمن قسم Connect to your application:

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

يعرض العرض MongoDB URI، وهو عنوان قاعدة البيانات الذي سنمرّره إلى مكتبة عميل MongoDB التي سنضيفها إلى تطبيقنا:

mongodb الاتصال بالتطبيق

يبدو العنوان هكذا:

mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0

أصبحنا الآن مستعدين لاستخدام قاعدة البيانات.

كان يمكننا استخدام قاعدة البيانات مباشرة من شيفرة JavaScript لدينا عبر مكتبة مشغّل MongoDB Node.js الرسمي، لكن استخدامها مرهق نوعاً ما. سنستخدم بدلاً منها مكتبة Mongoose التي تقدّم API بمستوى أعلى.

يمكن وصف Mongoose بأنها مُخطِّط كائنات إلى وثائق (object document mapper - ODM)، وحفظ كائنات JavaScript كوثائق Mongo مباشر مع هذه المكتبة.

لنثبّت Mongoose في الواجهة الخلفية لمشروع الملاحظات:

npm install mongoose

لن نضف بعد أي شيفرة تتعامل مع Mongo إلى الواجهة الخلفية. بدلاً من ذلك، لننشئ تطبيقاً تدريبياً بإنشاء ملف جديد هو mongo.js في جذر تطبيق الملاحظات الخلفي:

const mongoose = require('mongoose')

if (process.argv.length < 3) {
  console.log('give password as argument')
  process.exit(1)
}

const password = process.argv[2]

const url = `mongodb+srv://fullstack:${password}@cluster0.a5qfl.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0`

mongoose.set('strictQuery',false)

mongoose.connect(url, { family: 4 })

const noteSchema = new mongoose.Schema({
  content: String,
  important: Boolean,
})

const Note = mongoose.model('Note', noteSchema)

const note = new Note({
  content: 'HTML is easy',
  important: true,
})

note.save().then(result => {
  console.log('note saved!')
  mongoose.connection.close()
})

ملاحظة: حسب المنطقة التي اخترتها عند بناء عنقودك، قد يختلف MongoDB URI عن المثال أعلاه. ينبغي أن تتحقق من URI الصحيح الذي وُلِّد من MongoDB Atlas وتستخدمه.

يُنشأ الاتصال بقاعدة البيانات بالأمر:

mongoose.connect(url, { family: 4 })

تأخذ الدالة عنوان URL لقاعدة البيانات كمعامل أول، وكائناً يعرّف الإعدادات المطلوبة كمعامل ثانٍ. يدعم MongoDB Atlas عناوين IPv4 فقط، لذا نحدّد بالكائن { family: 4 } أن الاتصال ينبغي أن يستخدم IPv4 دائماً.

يفترض التطبيق التدريبي أنه سيُمرَّر إليه كلمة المرور من بيانات الاعتماد التي أنشأناها في MongoDB Atlas، كمعامل من سطر الأوامر. يمكننا الوصول إلى معامل سطر الأوامر هكذا:

const password = process.argv[2]

عندما تُشغَّل الشيفرة بالأمر node mongo.js yourPassword، ستضيف Mongo وثيقة جديدة إلى قاعدة البيانات.

ملاحظة: يرجى الانتباه إلى أن كلمة المرور هي كلمة المرور المُنشأة لمستخدم قاعدة البيانات، وليست كلمة مرور MongoDB Atlas الخاصة بك. كذلك، إذا أنشأت كلمة مرور تحتوي أحرفاً خاصة، فستحتاج إلى ترميز كلمة المرور تلك بصيغة URL.

يمكننا عرض الحالة الحالية لقاعدة البيانات من MongoDB Atlas عبر Browse collections، في تبويب Database.

زر browse collections في قواعد بيانات mongodb

كما يوضح العرض، أُضيفت الوثيقة المطابقة للملاحظة إلى مجموعة notes في قاعدة البيانات myFirstDatabase.

تبويب collections في mongodb مع قاعدة myFirstDatabase ومجموعة notes

لنحذف قاعدة البيانات الافتراضية test ونغيّر اسم قاعدة البيانات المشار إليها في نص الاتصال إلى noteApp بدلاً منها، بتعديل URI:

const url = `mongodb+srv://fullstack:${password}@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0`

لنشغّل شيفرتنا مرة أخرى:

تبويب collections في mongodb مع noteApp وnotes

تُخزَّن البيانات الآن في قاعدة البيانات الصحيحة. يقدّم العرض أيضاً وظيفة create database، التي يمكن استخدامها لإنشاء قواعد بيانات جديدة من الموقع. إنشاء قاعدة بيانات بهذه الطريقة ليس ضرورياً، لأن MongoDB Atlas ينشئ تلقائياً قاعدة بيانات جديدة عندما يحاول تطبيق الاتصال بقاعدة بيانات غير موجودة بعد.

المخطط

بعد إنشاء الاتصال بقاعدة البيانات، نعرّف المخطط لملاحظة والطراز (model) المطابق:

const noteSchema = new mongoose.Schema({
  content: String,
  important: Boolean,
})

const Note = mongoose.model('Note', noteSchema)

أولاً، نعرّف مخطط الملاحظة الذي يُخزَّن في المتغير noteSchema. يخبر المخطط Mongoose كيف ينبغي تخزين كائنات الملاحظات في قاعدة البيانات.

في تعريف طراز Note، المعامل الأول "Note" هو الاسم المفرد للطراز. سيكون اسم المجموعة هو الجمع بحروف صغيرة notes، لأن عُرف Mongoose هو تسمية المجموعات تلقائياً بصيغة الجمع (مثل notes) عندما يشير إليها المخطط بصيغة المفرد (مثل Note).

قواعد البيانات الوثائقية مثل Mongo بلا مخطط، أي أن قاعدة البيانات نفسها لا تهتم ببنية البيانات المخزّنة فيها. من الممكن تخزين وثائق بحقول مختلفة تماماً في المجموعة نفسها.

الفكرة وراء Mongoose هي أن البيانات المخزّنة في قاعدة البيانات تُمنَح مخططاً على مستوى التطبيق يعرّف شكل الوثائق المخزّنة في أي مجموعة معينة.

إنشاء الكائنات وحفظها

بعد ذلك، ينشئ التطبيق كائن ملاحظة جديداً بمساعدة طراز Note:

const note = new Note({
  content: 'HTML is Easy',
  important: false,
})

الطرازات دوال بانية تنشئ كائنات JavaScript جديدة بناءً على المعاملات المقدَّمة. وبما أن الكائنات تُنشأ بالدالة البانية للطراز، فإنها تملك جميع خصائص الطراز، ومنها دوال حفظ الكائن في قاعدة البيانات.

يتم حفظ الكائن في قاعدة البيانات بالدالة المسماة على نحو مناسب save، التي يمكن تزويدها بمعالج حدث عبر الدالة then:

note.save().then(result => {
  console.log('note saved!')
  mongoose.connection.close()
})

عندما يُحفظ الكائن في قاعدة البيانات، يُستدعى معالج الحدث المزوَّد إلى then. يغلق معالج الحدث اتصال قاعدة البيانات بالأمر mongoose.connection.close(). وإذا لم يُغلق الاتصال، يبقى الاتصال مفتوحاً حتى ينتهي البرنامج.

توجد نتيجة عملية الحفظ في المعامل result الخاص بمعالج الحدث. النتيجة ليست مثيرة للاهتمام كثيراً عندما نخزّن كائناً واحداً في قاعدة البيانات. يمكنك طباعة الكائن في الطرفية إذا أردت إلقاء نظرة أدق عليه أثناء تنفيذ تطبيقك أو أثناء تصحيح الأخطاء.

لنحفظ أيضاً بضع ملاحظات إضافية بتعديل البيانات في الشيفرة وتنفيذ البرنامج مرة أخرى.

ملاحظة: للأسف، توثيق Mongoose ليس متسقاً كثيراً، إذ تستخدم أجزاء منه دوال راجعة في أمثلتها وأجزاء أخرى أساليب مغايرة، لذا لا يُنصح بنسخ الشيفرة منه ولصقها مباشرة. كما لا يُنصح بخلط الوعود (promises) بالدوال الراجعة القديمة في الشيفرة نفسها.

جلب الكائنات من قاعدة البيانات

لنعلّق شيفرة توليد الملاحظات الجديدة ونستبدلها بما يلي:

Note.find({}).then(result => {
  result.forEach(note => {
    console.log(note)
  })
  mongoose.connection.close()
})

عند تنفيذ الشيفرة، يطبع البرنامج جميع الملاحظات المخزّنة في قاعدة البيانات:

مخرجات node mongo.js تعرض الملاحظات بصيغة JSON

تُسترجَع الكائنات من قاعدة البيانات بالدالة find الخاصة بطراز Note. معامل الدالة كائن يعبّر عن شروط البحث. وبما أن المعامل كائن فارغ{}، نحصل على جميع الملاحظات المخزّنة في مجموعة notes.

تتبع شروط البحث صيغة استعلام البحث في Mongo.

يمكننا قصر بحثنا على الملاحظات المهمة فقط هكذا:

Note.find({ important: true }).then(result => {
  // ...
})

تمرين 3.12.

3.12: قاعدة بيانات من سطر الأوامر

أنشئ قاعدة بيانات MongoDB سحابية لتطبيق دليل الهاتف باستخدام MongoDB Atlas.

أنشئ ملف mongo.js في مجلد المشروع، يمكن استخدامه لإضافة مدخلات إلى دليل الهاتف، ولعرض جميع المدخلات الموجودة فيه.

ملاحظة: لا تضمّن كلمة المرور في الملف الذي تُجري commit عليه وترفعه إلى GitHub!

ينبغي أن يعمل التطبيق كما يلي. تستخدم البرنامج بتمرير ثلاثة معاملات من سطر الأوامر (الأول هو كلمة المرور)، مثلاً:

node mongo.js yourpassword Anna 040-1234556

ونتيجة لذلك، سيطبع التطبيق:

added Anna number 040-1234556 to phonebook

سيُحفظ المدخل الجديد في دليل الهاتف في قاعدة البيانات. لاحظ أنه إذا كان الاسم يحتوي محارف مسافة بيضاء، فيجب وضعه بين علامتي اقتباس:

node mongo.js yourpassword "Arto Vihavainen" 045-1232456

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

node mongo.js yourpassword

فسيُفترض بالبرنامج أن يعرض جميع مدخلات دليل الهاتف:

phonebook:
Anna 040-1234556
Arto Vihavainen 045-1232456
Ada Lovelace 040-1231236

يمكنك الحصول على معاملات سطر الأوامر من المتغير process.argv.

ملاحظة: لا تغلق الاتصال في المكان الخطأ. فمثلاً، الشيفرة التالية لن تعمل:

Person
  .find({})
  .then(persons=> {
    // ...
  })

mongoose.connection.close()

في الشيفرة أعلاه، سيُنفَّذ الأمر mongoose.connection.close() فوراً بعد بدء عملية Person.find. وهذا يعني أن اتصال قاعدة البيانات سيُغلق فوراً، ولن يصل التنفيذ أبداً إلى النقطة التي تنتهي فيها عملية Person.find وتُستدعى فيها الدالة callback.

المكان الصحيح لإغلاق اتصال قاعدة البيانات هو نهاية الدالة الراجعة:

Person
  .find({})
  .then(persons=> {
    // ...
    mongoose.connection.close()
  })

ملاحظة: إذا عرّفت طرازاً باسم Person، فستسمّي mongoose المجموعة المرتبطة به تلقائياً people.

ربط الواجهة الخلفية بقاعدة بيانات

أصبح لدينا الآن ما يكفي من المعرفة لنبدأ استخدام Mongo في الواجهة الخلفية لتطبيق الملاحظات.

لنبدأ سريعاً بنسخ تعريفات Mongoose ولصقها في ملف index.js:

const mongoose = require('mongoose')

// لا تحفظ كلمة المرور الخاصة بك في GITHUB!!
const password = process.argv[2]
const url = `mongodb+srv://fullstack:${password}@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0`

mongoose.set('strictQuery',false)
mongoose.connect(url, { family: 4 })

const noteSchema = new mongoose.Schema({
  content: String,
  important: Boolean,
})

const Note = mongoose.model('Note', noteSchema)

لنغيّر معالج جلب جميع الملاحظات إلى الشكل التالي:

app.get('/api/notes', (request, response) => {
  Note.find({}).then(notes => {
    response.json(notes)
  })
})

لنشغّل الواجهة الخلفية بالأمر node --watch index.js yourpassword حتى نتحقق في المتصفح من أن الواجهة الخلفية تعرض بشكل صحيح جميع الملاحظات المحفوظة في قاعدة البيانات:

api/notes في المتصفح يعرض الملاحظات بصيغة JSON

يعمل التطبيق على نحو شبه مثالي. تفترض الواجهة الأمامية أن لكل كائن معرّفاً فريداً في الحقل id. كذلك لا نريد إعادة حقل الإصدار __v الخاص بـ mongo إلى الواجهة الأمامية.

إحدى طرق تنسيق الكائنات التي تعيدها Mongoose هي تعديل الدالة toJSON الخاصة بالمخطط، وهي تُستخدم على جميع نسخ الطرازات المُنتَجة بذلك المخطط. يمكن إجراء التعديل كما يلي:

noteSchema.set('toJSON', {
  transform: (document, returnedObject) => {
    returnedObject.id = returnedObject._id.toString()
    delete returnedObject._id
    delete returnedObject.__v
  }
})

رغم أن خاصية _id في كائنات Mongoose تبدو كنص، فهي في الحقيقة كائن. الدالة toJSON التي عرّفناها تحوّلها إلى نص من باب الاحتياط فقط. ولو لم نجري هذا التغيير، لسبّب لنا ضرراً أكبر في المستقبل بمجرد أن نبدأ كتابة الاختبارات.

لا حاجة إلى أي تغييرات في المعالج:

app.get('/api/notes', (request, response) => {
  Note.find({}).then(notes => {
    response.json(notes)
  })
})

تستخدم الشيفرة تلقائياً الدالة toJSON المعرَّفة عند تنسيق الملاحظات في الاستجابة.

نقل إعدادات قاعدة البيانات إلى وحدة خاصة بها

قبل أن نعيد هيكلة بقية الواجهة الخلفية لتستخدم قاعدة البيانات، لنستخرج الشيفرة الخاصة بـ Mongoose إلى وحدة خاصة بها.

لننشئ مجلداً جديداً للوحدة باسم models، ونضف ملفاً باسم note.js:

const mongoose = require('mongoose')

mongoose.set('strictQuery', false)

const url = process.env.MONGODB_URI

console.log('connecting to', url)
mongoose.connect(url, { family: 4 })
// highlight-start
  .then(result => {
    console.log('connected to MongoDB')
  })
  .catch(error => {
    console.log('error connecting to MongoDB:', error.message)
  })
// highlight-end

const noteSchema = new mongoose.Schema({
  content: String,
  important: Boolean,
})

noteSchema.set('toJSON', {
  transform: (document, returnedObject) => {
    returnedObject.id = returnedObject._id.toString()
    delete returnedObject._id
    delete returnedObject.__v
  }
})

module.exports = mongoose.model('Note', noteSchema)

هناك بعض التغييرات في الشيفرة مقارنةً بالسابق. أصبح عنوان URL لاتصال قاعدة البيانات يُمرَّر إلى التطبيق عبر متغير البيئة MONGODB_URI، إذ ليس من الجيد تضمينه مباشرة في الشيفرة:

const url = process.env.MONGODB_URI

هناك طرق كثيرة لتعريف قيمة متغير بيئة. على سبيل المثال، يمكننا تعريفه عند تشغيل التطبيق كما يلي:

MONGODB_URI="your_connection_string_here" npm run dev

سنتعلّم قريباً طريقة أكثر تطوراً لتعريف متغيرات البيئة.

تغيّرت طريقة إنشاء الاتصال قليلاً:

mongoose.connect(url, { family: 4 })
  .then(result => {
    console.log('connected to MongoDB')
  })
  .catch(error => {
    console.log('error connecting to MongoDB:', error.message)
  })

أصبحت الدالة المسؤولة عن إنشاء الاتصال تُمنَح دالتين للتعامل مع محاولة اتصال ناجحة وأخرى فاشلة. كلتا الدالتين تسجّلان فقط رسالة في الطرفية عن حالة النجاح:

مخرجات node عند اسم مستخدم أو كلمة مرور خاطئين

يختلف تعريف وحدات Node قليلاً عن طريقة تعريف وحدات ES6 في الجزء 2.

تُعرَّف الواجهة العامة للوحدة بإسناد قيمة إلى المتغير module.exports. سنسند القيمة لتكون طراز Note. أما الأمور الأخرى المعرَّفة داخل الوحدة، مثل المتغيرين mongoose و_url_، فلن تكون متاحة أو مرئية لمستخدمي الوحدة.

تحدث استيراد الوحدة بإضافة السطر التالي إلى index.js:

const Note = require('./models/note')

بهذه الطريقة سيُسنَد المتغير Note إلى الكائن نفسه الذي تعرّفه الوحدة.

تعريف متغيرات البيئة باستخدام مكتبة dotenv

طريقة أكثر تطوراً لتعريف متغيرات البيئة هي استخدام مكتبة dotenv. يمكنك تثبيت المكتبة بالأمر:

npm install dotenv

لاستخدام المكتبة، ننشئ ملف .env في جذر المشروع. تُعرَّف متغيرات البيئة داخل الملف، وقد يبدو هكذا:

MONGODB_URI=mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0
PORT=3001

أضفنا أيضاً منفذ الخادم المضمَّن في الشيفرة إلى متغير البيئة PORT.

ينبغي استثناء ملف .env من git فوراً لأننا لا نريد نشر أي معلومات سرية علناً على الإنترنت!

ملف .gitignore في vscode مع إضافة سطر .env

يمكن أخذ متغيرات البيئة المعرَّفة في ملف .env في الاستخدام بالتعليم require('dotenv').config()، ويمكنك الإشارة إليها في شيفرتك كما تشير إلى متغيرات البيئة العادية، بصيغة process.env.MONGODB_URI.

لنحمّل متغيرات البيئة في بداية ملف index.js لتكون متاحة في التطبيق كله. لنغيّر ملف index.js بالطريقة التالية:

require('dotenv').config()
const express = require('express')
const Note = require('./models/note')

const app = express()
// ..

const PORT = process.env.PORT
app.listen(PORT, () => {
  console.log(`Server running on port ${PORT}`)
})

من المهم أن يُستورَد dotenv قبل استيراد طراز note. هذا يضمن أن متغيرات البيئة من ملف .env متاحة عالمياً قبل استيراد الشيفرة من الوحدات الأخرى.

ملاحظة مهمة حول تعريف متغيرات البيئة في Fly.io وRender

مستخدمو Fly.io: بما أن GitHub لا يُستخدم مع Fly.io، فإن ملف .env يصل أيضاً إلى خوادم Fly.io عند نشر التطبيق. وبسبب ذلك، ستكون متغيرات البيئة المعرَّفة في الملف متاحة هناك.

غير أن خياراً أفضل هو منع نسخ ملف .env إلى Fly.io بإنشاء ملف .dockerignore في جذر المشروع بالمحتوى التالي

.env

وضبط قيمة متغير البيئة من سطر الأوامر بالأمر:

fly secrets set MONGODB_URI="mongodb+srv://fullstack:thepasswordishere@cluster0.a5qfl.mongodb.net/noteApp?retryWrites=true&w=majority&appName=Cluster0"

مستخدمو Render: عند استخدام Render، يُعطى عنوان url لقاعدة البيانات بتعريف متغير البيئة المناسب في لوحة التحكم:

المتصفح يعرض متغيرات بيئة render

اضبط عنوان URL الذي يبدأ بـ mongodb+srv://... فقط في حقل value.

استخدام قاعدة البيانات في معالجات المسارات

بعد ذلك، لنغيّر بقية وظائف الواجهة الخلفية لتستخدم قاعدة البيانات.

يتم إنشاء ملاحظة جديدة هكذا:

app.post('/api/notes', (request, response) => {
  const body = request.body

  if (!body.content) {
    return response.status(400).json({ error: 'content missing' })
  }

  const note = new Note({
    content: body.content,
    important: body.important || false,
  })

  note.save().then(savedNote => {
    response.json(savedNote)
  })
})

تُنشأ كائنات الملاحظات بالدالة البانية Note. تُرسَل الاستجابة داخل الدالة الراجعة لعملية save. وهذا يضمن ألا تُرسَل الاستجابة إلا إذا نجحت العملية. سنناقش معالجة الأخطاء بعد قليل.

المعامل savedNote في الدالة الراجعة هو الملاحظة الجديدة المنشأة والمحفوظة. البيانات المُعاد إرسالها في الاستجابة هي النسخة المنسَّقة التي تُنشأ تلقائياً بالدالة toJSON:

response.json(savedNote)

باستخدام دالة findById في Mongoose، يصبح جلب ملاحظة فردية كما يلي:

app.get('/api/notes/:id', (request, response) => {
  Note.findById(request.params.id).then(note => {
    response.json(note)
  })
})

التحقق من تكامل الواجهة الأمامية والخلفية

عندما تتوسّع الواجهة الخلفية، من الجيد اختبار الواجهة الخلفية أولاً بـ المتصفح أو Postman أو عميل REST في VS Code. بعد ذلك، لنجرّب إنشاء ملاحظة جديدة بعد أخذ قاعدة البيانات في الاستخدام:

عميل REST في VS Code يرسل طلب post

فقط بعد التحقق من أن كل شيء يعمل في الواجهة الخلفية، من الجيد اختبار أن الواجهة الأمامية تعمل مع الواجهة الخلفية. فاختبار الأمور حصراً عبر الواجهة الأمامية غير فعّال إطلاقاً.

من الجيد على الأرجح دمج الواجهة الأمامية والخلفية وظيفة واحدة في كل مرة. أولاً، يمكننا تنفيذ جلب جميع الملاحظات من قاعدة البيانات واختباره عبر نقطة نهاية الواجهة الخلفية في المتصفح. بعد ذلك، يمكننا التحقق من أن الواجهة الأمامية تعمل مع الواجهة الخلفية الجديدة. وبمجرد أن يبدو كل شيء يعمل، ننتقل إلى الميزة التالية.

بمجرد إدخال قاعدة بيانات إلى المعادلة، من المفيد فحص الحالة المحفوظة في قاعدة البيانات، مثلاً من لوحة التحكم في MongoDB Atlas. وفي كثير من الأحيان تكون برامج Node المساعدة الصغيرة مثل برنامج mongo.js الذي كتبناه سابقاً مفيدة جداً أثناء التطوير.

يمكنك إيجاد شيفرة تطبيقنا الحالي كاملةً في فرع part3-4 من مستودع GitHub هذا.

قسم مطوّر full stack الحقيقي

حان وقت التمارين مرة أخرى. ازداد تعقيد تطبيقنا الآن خطوة أخرى، إذ أصبح لدينا قاعدة بيانات إضافة إلى الواجهة الأمامية والخلفية. وهناك بالفعل مصادر كثيرة جداً محتملة للأخطاء.

لذا ينبغي أن نوسّع قسمنا مرة أخرى:

تطوير full stack صعب للغاية، ولهذا سأستخدم كل الوسائل الممكنة لتسهيله

  • سأبقي وحدة تحكم مطوّري المتصفح مفتوحة طوال الوقت
  • سأستخدم تبويب network في أدوات مطوّري المتصفح للتأكد من أن الواجهة الأمامية والخلفية تتواصلان كما أتوقع
  • سأراقب باستمرار حالة الخادم للتأكد من أن البيانات التي ترسلها الواجهة الأمامية إليه تُحفظ هناك كما أتوقع
  • سأراقب قاعدة البيانات؛ هل تُحفظ البيانات في الحالة المتوقعة
  • سأتقدم بخطوات صغيرة
  • سأكتب الكثير من عبارات console.log للتأكد من فهمي لكيفية تصرف الشيفرة وللمساعدة في تحديد المشاكل
  • إذا لم تعمل شيفرتي، لن أكتب المزيد من الشيفرة. بل أبدأ بحذف الشيفرة حتى تعمل أو أعود ببساطة إلى حالة كان فيها كل شيء يعمل
  • عندما أطلب المساعدة في قناة Discord الخاصة بالدورة أو في أي مكان آخر، سأصوغ أسئلتي بشكل صحيح، انظر هنا لكيفية طلب المساعدة

التمارين 3.13.-3.14.

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

3.13: قاعدة بيانات دليل الهاتف، الخطوة 1

غيّر جلب جميع مدخلات دليل الهاتف بحيث تُجلَب البيانات من قاعدة البيانات.

تحقق من أن الواجهة الأمامية تعمل بعد إجراء التغييرات.

في التمارين التالية، اكتب جميع الشيفرة الخاصة بـ Mongoose في وحدة خاصة بها، تماماً كما فعلنا في فصل إعدادات قاعدة البيانات في وحدة خاصة بها.

3.14: قاعدة بيانات دليل الهاتف، الخطوة 2

غيّر الواجهة الخلفية بحيث تُحفظ الأرقام الجديدة في قاعدة البيانات. تحقق من أن واجهتك الأمامية لا تزال تعمل بعد التغييرات.

في هذه المرحلة، يمكنك تجاهل ما إذا كان هناك بالفعل شخص في قاعدة البيانات بالاسم نفسه للشخص الذي تضيفه.

معالجة الأخطاء

إذا حاولنا زيارة عنوان URL لملاحظة بمعرّف غير موجود، مثل http://localhost:3001/api/notes/5c41c90e84d891c15dfa3431 حيث 5c41c90e84d891c15dfa3431 ليس معرّفاً مخزّناً في قاعدة البيانات، فستكون الاستجابة null.

لنغيّر هذا السلوك بحيث إذا لم توجد ملاحظة بالمعرّف المحدد، سيستجيب الخادم للطلب برمز حالة HTTP 404 not found. بالإضافة إلى ذلك، لننفّذ كتلة catch بسيطة للتعامل مع الحالات التي يُرفَض فيها الوعد الذي تعيده دالة findById:

app.get('/api/notes/:id', (request, response) => {
  Note.findById(request.params.id)
    .then(note => {
      // highlight-start
      if (note) {
        response.json(note)
      } else {
        response.status(404).end()
      }
      // highlight-end
    })
    // highlight-start
    .catch(error => {
      console.log(error)
      response.status(500).end()
    })
    // highlight-end
})

إذا لم يوجد كائن مطابق في قاعدة البيانات، ستكون قيمة note هي null وستُنفَّذ كتلة else. وينتج عن ذلك استجابة برمز الحالة 404 not found. أما إذا رُفض الوعد الذي تعيده دالة findById، فستحمل الاستجابة رمز الحالة 500 internal server error. وتعرض الطرفية معلومات أكثر تفصيلاً عن الخطأ.

إضافة إلى حالة الملاحظة غير الموجودة، هناك حالة خطأ أخرى تحتاج إلى معالجة. في هذه الحالة، نحاول جلب ملاحظة بنوع id خاطئ، أي id لا يطابق صيغة معرّف Mongo.

إذا أرسلنا الطلب التالي، فسنحصل على رسالة الخطأ المعروضة أدناه:

Method: GET
Path:   /api/notes/someInvalidId
Body:   {}
---
{ CastError: Cast to ObjectId failed for value "someInvalidId" at path "_id"
    at CastError (/Users/mluukkai/opetus/_fullstack/osa3-muisiinpanot/node_modules/mongoose/lib/error/cast.js:27:11)
    at ObjectId.cast (/Users/mluukkai/opetus/_fullstack/osa3-muisiinpanot/node_modules/mongoose/lib/schema/objectid.js:158:13)
    ...

عند إعطاء معرّف مشوّه كمعامل، ستطلق دالة findById خطأً يؤدي إلى رفض الوعد المُعاد. وسيؤدي ذلك إلى استدعاء الدالة الراجعة المعرَّفة في كتلة catch.

لنجرِ بعض التعديلات الصغيرة على الاستجابة في كتلة catch:

app.get('/api/notes/:id', (request, response) => {
  Note.findById(request.params.id)
    .then(note => {
      if (note) {
        response.json(note)
      } else {
        response.status(404).end() 
      }
    })
    .catch(error => {
      console.log(error)
      response.status(400).send({ error: 'malformatted id' })
    })
})

إذا كانت صيغة المعرّف غير صحيحة، فسننتهي في معالج الأخطاء المعرَّف في كتلة catch. ورمز الحالة المناسب لهذه الحالة هو 400 Bad Request لأن الحالة تطابق الوصف تماماً:

يشير رمز الحالة 400 (Bad Request) إلى أن الخادم لا يستطيع أو لا يريد معالجة الطلب بسبب أمر يُعتبر خطأ من العميل (مثل صيغة طلب مشوّهة، أو تأطير رسالة طلب غير صالح، أو توجيه طلب خادع).

أضفنا أيضاً بعض البيانات إلى الاستجابة لإلقاء الضوء على سبب الخطأ.

عند التعامل مع الوعود (Promises)، من الجيد دائماً تقريباً إضافة معالجة للأخطاء والاستثناءات. وإلا ستجد نفسك تتعامل مع أخطاء غريبة.

ليس فكرة سيئة أبداً طباعة الكائن الذي سبّب الاستثناء في الطرفية داخل معالج الأخطاء:

.catch(error => {
  console.log(error)
  response.status(400).send({ error: 'malformatted id' })
})

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

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

لقطة شاشة مثال تعرض شريحة صغيرة من المخرجات

نقل معالجة الأخطاء إلى وسيط

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

لنغيّر معالج المسار /api/notes/:id بحيث يمرّر الخطأ إلى الأمام بالدالة next. تُمرَّر الدالة next إلى المعالج كمعامل ثالث:

app.get('/api/notes/:id', (request, response, next) => {
  Note.findById(request.params.id)
    .then(note => {
      if (note) {
        response.json(note)
      } else {
        response.status(404).end()
      }
    })
    .catch(error => next(error))
})

يُمنَح الخطأ المُمرَّر إلى الأمام للدالة next كمعامل. وإذا استُدعيت next دون معامل، فسينتقل التنفيذ ببساطة إلى المسار أو الوسيط التالي. أما إذا استُدعيت الدالة next بمعامل، فسيستمر التنفيذ إلى وسيط معالج الأخطاء.

معالجات الأخطاء في Express وسيطات تُعرَّف بدالة تقبل أربعة معاملات. ويبدو معالج الأخطاء لدينا هكذا:

const errorHandler = (error, request, response, next) => {
  console.error(error.message)

  if (error.name === 'CastError') {
    return response.status(400).send({ error: 'malformatted id' })
  } 

  next(error)
}

// يجب أن يكون هذا آخر وسيط يُحمَّل، كما ينبغي تسجيل جميع المسارات قبله!
app.use(errorHandler)

يتحقق معالج الأخطاء مما إذا كان الخطأ استثناء CastError، وفي هذه الحالة نعرف أن الخطأ سبّبه معرّف كائن غير صالح في Mongo. في هذه الحالة، سيرسل معالج الأخطاء استجابة إلى المتصفح بكائن الاستجابة المُمرَّر كمعامل. وفي جميع حالات الأخطاء الأخرى، يمرّر الوسيط الخطأ إلى معالج الأخطاء الافتراضي في Express.

لاحظ أن وسيط معالجة الأخطاء يجب أن يكون آخر وسيط يُحمَّل، كما ينبغي تسجيل جميع المسارات قبل معالج الأخطاء!

ترتيب تحميل الوسيطات

ترتيب تنفيذ الوسيطات هو نفسه ترتيب تحميلها في Express بالدالة app.use. لهذا السبب، من المهم توخّي الحذر عند تعريف الوسيطات.

الترتيب الصحيح هو التالي:

app.use(express.static('dist'))
app.use(express.json())
app.use(requestLogger)

app.post('/api/notes', (request, response) => {
  const body = request.body
  // ...
})

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

// معالج الطلبات ذات نقطة النهاية غير المعروفة
app.use(unknownEndpoint)

const errorHandler = (error, request, response, next) => {
  // ...
}

// معالج الطلبات التي تؤدي إلى أخطاء
app.use(errorHandler)

ينبغي أن يكون وسيط محلل json من أوائل الوسيطات المحمَّلة في Express. لو كان الترتيب كالتالي:

app.use(requestLogger) // قيمة request.body غير معرّفة!

app.post('/api/notes', (request, response) => {
  // قيمة request.body غير معرّفة!
  const body = request.body
  // ...
})

app.use(express.json())

فلن تكون بيانات JSON المُرسَلة مع طلبات HTTP متاحة لوسيط المسجّل أو لمعالج مسار POST، لأن request.body ستكون undefined في تلك المرحلة.

من المهم أيضاً ألا يُحمَّل الوسيط الخاص بالتعامل مع المسارات غير المدعومة إلا بعد تعريف جميع نقاط النهاية، وقبل معالج الأخطاء مباشرة. فمثلاً، سيؤدي ترتيب التحميل التالي إلى مشكلة:

const unknownEndpoint = (request, response) => {
  response.status(404).send({ error: 'unknown endpoint' })
}

// معالج الطلبات ذات نقطة النهاية غير المعروفة
app.use(unknownEndpoint)

app.get('/api/notes', (request, response) => {
  // ...
})

الآن أصبحت معالجة نقاط النهاية غير المعروفة مرتَّبة قبل معالج طلبات HTTP. وبما أن معالج نقطة النهاية غير المعروفة يستجيب لجميع الطلبات بـ 404 unknown endpoint، فلن يُستدعى أي مسار أو وسيط بعد إرسال الاستجابة من وسيط نقطة النهاية غير المعروفة. والاستثناء الوحيد لذلك هو معالج الأخطاء الذي يجب أن يأتي في النهاية تماماً، بعد معالج نقاط النهاية غير المعروفة.

عمليات أخرى

لنضف بعض الوظائف الناقصة إلى تطبيقنا، ومنها حذف ملاحظة فردية وتحديثها.

أسهل طريقة لحذف ملاحظة من قاعدة البيانات هي الدالة findByIdAndDelete:

app.delete('/api/notes/:id', (request, response, next) => {
  Note.findByIdAndDelete(request.params.id)
    .then(result => {
      response.status(204).end()
    })
    .catch(error => next(error))
})

في كلتا حالتي حذف مورد بنجاح، تستجيب الواجهة الخلفية برمز الحالة 204 no content. والحالتان المختلفتان هما حذف ملاحظة موجودة، وحذف ملاحظة غير موجودة في قاعدة البيانات. ويمكن استخدام المعامل الراجع result للتحقق مما إذا كان المورد قد حُذف فعلاً، ويمكننا استخدام تلك المعلومة لإعادة رموز حالة مختلفة للحالتين إذا رأينا ذلك ضرورياً. ويُمرَّر أي استثناء يحدث إلى معالج الأخطاء.

لننفّذ وظيفة تحديث ملاحظة واحدة، بما يسمح بتغيير أهمية الملاحظة. يتم تحديث الملاحظة كما يلي:

app.put('/api/notes/:id', (request, response, next) => {
  const { content, important } = request.body

  Note.findById(request.params.id)
    .then(note => {
      if (!note) {
        return response.status(404).end()
      }

      note.content = content
      note.important = important

      return note.save().then((updatedNote) => {
        response.json(updatedNote)
      })
    })
    .catch(error => next(error))
})

تُجلَب الملاحظة المراد تحديثها أولاً من قاعدة البيانات باستخدام الدالة findById. وإذا لم يوجد كائن في قاعدة البيانات بالمعرّف المحدد، فستكون قيمة المتغير note هي null، ويستجيب الاستعلام برمز الحالة 404 Not Found.

وإذا وُجد كائن بالمعرّف المحدد، يُحدَّث حقلا content و_important_ بالبيانات المقدَّمة في الطلب، وتُحفظ الملاحظة المعدَّلة في قاعدة البيانات باستخدام الدالة save(). ويستجيب طلب HTTP بإرسال الملاحظة المحدَّثة في الاستجابة.

من النقاط الجديرة بالملاحظة أن الشيفرة تحتوي الآن على وعود متداخلة، أي أن داخل الدالة .then الخارجية تُعرَّف سلسلة وعود أخرى:

    .then(note => {
      if (!note) {
        return response.status(404).end()
      }

      note.content = content
      note.important = important

      // highlight-start
      return note.save().then((updatedNote) => {
        response.json(updatedNote)
      })
      // highlight-end

عادةً لا يُنصح بهذا لأنه قد يجعل الشيفرة صعبة القراءة. غير أن الحل يعمل في هذه الحالة لأنه يضمن ألا تُنفَّذ كتلة .then التي تلي الدالة save() إلا إذا وُجدت ملاحظة بالمعرّف المحدد في قاعدة البيانات واستُدعيت الدالة save(). وفي الجزء الرابع من الدورة، سنستكشف صيغة async/await التي تقدّم طريقة أسهل وأوضح للتعامل مع مثل هذه الحالات.

تقدّم Mongoose أيضاً الدالة findByIdAndUpdate، التي يمكن استخدامها لإيجاد وثيقة بـ id الخاص بها وتحديثها باستدعاء دالة واحد. غير أن هذا الأسلوب لا يلبي احتياجاتنا تماماً، لأننا نعرّف لاحقاً في هذا الجزء متطلبات معينة للبيانات المخزّنة في قاعدة البيانات، وfindByIdAndUpdate لا يدعم تحققات Mongoose من الصحة دعماً كاملاً. كما يشير توثيق Mongoose إلى أن الدالة save() هي الخيار الصحيح عموماً لتحديث وثيقة، لأنها توفّر تحققاً كاملاً.

بعد اختبار الواجهة الخلفية مباشرة بـ Postman أو عميل REST في VS Code، يمكننا التحقق من أنها تبدو تعمل. ويبدو أن الواجهة الأمامية تعمل أيضاً مع الواجهة الخلفية التي تستخدم قاعدة البيانات.

يمكنك إيجاد شيفرة تطبيقنا الحالي كاملةً في فرع part3-5 من مستودع GitHub هذا.

التمارين 3.15.-3.18.

3.15: قاعدة بيانات دليل الهاتف، الخطوة 3

غيّر الواجهة الخلفية بحيث ينعكس حذف مدخلات دليل الهاتف في قاعدة البيانات.

تحقق من أن الواجهة الأمامية لا تزال تعمل بعد إجراء التغييرات.

3.16: قاعدة بيانات دليل الهاتف، الخطوة 4

انقل معالجة الأخطاء في التطبيق إلى وسيط معالج أخطاء جديد.

3.17*: قاعدة بيانات دليل الهاتف، الخطوة 5

إذا حاول المستخدم إنشاء مدخل جديد في دليل الهاتف لشخص اسمه موجود بالفعل في دليل الهاتف، فستحاول الواجهة الأمامية تحديث رقم هاتف المدخل الموجود بإرسال طلب HTTP PUT إلى العنوان الفريد للمدخل.

عدّل الواجهة الخلفية لدعم هذا الطلب.

تحقق من أن الواجهة الأمامية تعمل بعد إجراء تغييراتك.

3.18*: قاعدة بيانات دليل الهاتف، الخطوة 6

حدّث أيضاً معالجة مساري HTTP GET api/persons/:id وinfo لتستخدم قاعدة البيانات، وتحقق من أنها تعمل مباشرة مع المتصفح أو Postman أو عميل REST في VS Code.

ينبغي أن يبدو فحص مدخل فردي في دليل الهاتف من المتصفح هكذا:

لقطة شاشة للمتصفح تعرض شخصاً واحداً عبر api/persons/their_id