Menu
CoddyTech

ترتيب النصوص

جزء من قسم واجهات REST API في رحلة Node.js على Coddy. الدرس 15 من 25.

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

const sorted = cities.toSorted((a, b) => a.name.localeCompare(b.name));

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

challenge icon

التحدي

سهل

واصل العمل على متعقّب المهام في routes/tasks.js. في مسار القائمة، بعد عوامل التصفية الحالية، اجعل ?sort=title يعرض المهام مرتبة أبجديًا حسب العنوان. من دون هذه القيمة، حافظ على السلوك الحالي. لا تغيّر ترتيب المهام المخزّنة.

جرّب GET /api/tasks?sort=title، ثم GET /api/tasks. يجب أن تظل القائمة الثانية بترتيبها الأصلي. يجب أن تظل التصفية باستخدام ?done=false&sort=title تعرض المهام غير المكتملة فقط.

جرّب بنفسك

import { Router } from 'express';
import { z } from 'zod';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';

const router = Router();

// مهمة كاملة كما يرسلها PUT. ترفض .strict() الحقول التي لا تُدرجها.
const Task = z
    .object({
        title: z.string().trim().min(1).max(100),
        done: z.boolean(),
    })
    .strict();
const NewTask = Task.partial({ done: true });
const TaskChanges = Task.partial();

// تكون القيمة true إذا كانت هناك مهمة أخرى تحمل هذا العنوان بالفعل، بغضّ النظر عن حالة الأحرف.
const titleTaken = (title, exceptId) =>
    tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());

router.get('/', (req, res) => {
    const { done, q } = req.query;
    let list = tasks;
    if (done !== undefined) {
        if (done !== 'true' && done !== 'false') {
            return sendError(res, 400, 'invalid_query', 'done must be true or false');
        }
        list = list.filter((t) => t.done === (done === 'true'));
    }
    if (q !== undefined) {
        const words = String(q).toLowerCase();
        list = list.filter((t) => t.title.toLowerCase().includes(words));
    }
    res.json(list);
});

router.get('/:id', (req, res) => {
    const task = tasks.find((t) => t.id === Number(req.params.id));
    if (!task) {
        return sendError(res, 404, 'not_found', 'Task not found');
    }
    res.json(task);
});

router.post('/', (req, res) => {
    const result = NewTask.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    const { title, done = false } = result.data;
    if (titleTaken(title)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    const task = { id: nextId(), title, done, createdAt: new Date().toISOString() };
    tasks.push(task);
    res.status(201).location(`/api/tasks/${task.id}`).json(task);
});

router.patch('/:id', (req, res) => {
    const task = tasks.find((t) => t.id === Number(req.params.id));
    if (!task) {
        return sendError(res, 404, 'not_found', 'Task not found');
    }
    const result = TaskChanges.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    if (result.data.title !== undefined && titleTaken(result.data.title, task.id)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    Object.assign(task, result.data);
    res.json(task);
});

router.put('/:id', (req, res) => {
    const index = tasks.findIndex((t) => t.id === Number(req.params.id));
    if (index === -1) {
        return sendError(res, 404, 'not_found', 'Task not found');
    }
    const result = Task.safeParse(req.body);
    if (!result.success) {
        return sendInvalid(res, result.error);
    }
    const { id, createdAt } = tasks[index];
    if (titleTaken(result.data.title, id)) {
        return sendError(res, 409, 'conflict', 'A task with this title already exists');
    }
    tasks[index] = { id, ...result.data, createdAt };
    res.json(tasks[index]);
});

router.delete('/:id', (req, res) => {
    const index = tasks.findIndex((t) => t.id === Number(req.params.id));
    if (index === -1) {
        return sendError(res, 404, 'not_found', 'Task not found');
    }
    tasks.splice(index, 1);
    res.status(204).end();
});

export default router;

جميع دروس واجهات REST API

تدرّب بنفسك: Playground لـ Express