Menu
CoddyTech

מיון

חלק מהיחידה REST APIs במסלול ה-Node.js של Coddy. שיעור 16 מתוך 25.

לקוח רוצה שהמשימות החדשות ביותר יוצגו ראשונות, או שהרשימה תהיה מסודרת מ־A עד Z. מוסכמה נפוצה היא ?sort=field, עם - לפני השדה כדי להפוך את הסדר:

// GET /api/books?sort=-year
const field = sort.startsWith('-') ? sort.slice(1) : sort;   // 'year'
const direction = sort.startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => (a[field] - b[field]) * direction);

בדוק תחילה ש־field הוא אחד מהשדות שאתה מתיר, והשב 400 עבור כל שדה אחר, משום ששדה שאין בספרים לא מספק סדר שימושי. toSorted מחזירה עותק ממוין. sort מסדרת מחדש את המערך עצמו, וכששום מסנן לא יצר עותק, המערך הזה הוא המאגר: כל בקשה מאוחרת יותר תראה את הסדר החדש. הדוגמה הזאת מבצעת חיסור, שפועל רק עם מספרים. כדי להשוות בין שתי מחרוזות, השתמש ב־a.localeCompare(b): כמו חיסור, היא מחזירה מספר שלילי, 0 או מספר חיובי. תאריכי ISO הם מחרוזות שממוינות בסדר כרונולוגי.

challenge icon

אתגר

קל

החליפו את המיון לפי כותרת בלבד שכתבתם בשיעור מיון טקסט בטיפול בשדה ובכיוון שמופיעים כאן.

לקוחות רוצים שהמשימות החדשות ביותר יופיעו ראשונות, או שהרשימה תהיה מסודרת מא׳ עד ת׳. תנו לרשימה להתמיין בעצמה באמצעות ?sort=.

  1. ב-routes/tasks.js, בנתיב הרשימה, קראו את sort מתוך req.query. בלעדיו, הרשימה נשארת בסדר שבו היא נשמרה.
  2. ?sort=title ממיין לפי כותרת מא׳ עד ת׳. ?sort=createdAt ממיין לפי מועד היצירה, מהישן ביותר לחדש ביותר. סימן - בתחילת הערך הופך את הסדר: ?sort=-title ממיין מת׳ עד א׳, ו-?sort=-createdAt מציג תחילה את החדשות ביותר.
  3. שני השדות הם טקסט. createdAt הוא תאריך בפורמט ISO, כגון "2026-03-03T10:30:00.000Z", שמתמיין נכון כטקסט. לכן השוו באמצעות localeCompare, ולא באמצעות חיסור.
  4. כל שדה אחר, כמו ?sort=color, מחזיר סטטוס 400:
    { "error": { "code": "invalid_query", "message": "sort must be title or createdAt, with - in front to reverse" } }
  5. בצעו את המיון אחרי הסינון, כדי ש-?done=false&sort=-title יחזיר את המשימות הפתוחות בסדר מת׳ עד א׳.
  6. מיינו עותק באמצעות toSorted. sort משנה את הסדר של המערך השמור עצמו, וכל בקשה מאוחרת יותר תראה את הסדר החדש.

נסו זאת: GET /api/tasks?sort=title מחזיר את המשימות 2, 3, 1 ("Book...", "Send...", "Write..."). GET /api/tasks?sort=-createdAt מחזיר את 3, 2, 1. לאחר מכן, GET /api/tasks ללא פרמטרים מחזיר שוב את 1, 2, 3. GET /api/tasks?sort=color מחזיר סטטוס 400 עם הקוד invalid_query.

נסו בעצמכם

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();

// ערך אמת כאשר למשימה אחרת כבר יש כותרת כזו, ללא תלות באותיות גדולות או קטנות.
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));
    }
    if (req.query.sort === 'title') {
        list = list.toSorted((a, b) => a.title.localeCompare(b.title));
    }
    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 APIs

תרגלו בעצמכם: Express אונליין