מיון
חלק מהיחידה REST APIs במסלול ה-Node.js של Coddy. שיעור 11 מתוך 18.
לקוח רוצה שהמשימות החדשות ביותר יופיעו ראשונות, או את הרשימה מא׳ עד ת׳. מוסכמה נפוצה היא ?sort=field, עם - בתחילת הערך כדי להפוך את הסדר:
// GET /api/books?sort=-year
const field = sort.replace(/^-/, ''); // 'year'
const direction = sort.startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => (a[field] - b[field]) * direction);בדוק תחילה את field מול השדות שאתה מתיר, והחזר 400 עבור כל שדה אחר, מכיוון ששדה שאינו קיים בספרים לא מספק סדר שימושי. toSorted מחזירה עותק ממוין. sort משנה את סדר המערך עצמו, וכאשר שום מסנן לא יצר עותק, המערך הזה הוא המאגר: כל בקשה מאוחרת יותר תראה את הסדר החדש. השווה מחרוזות באמצעות a.localeCompare(b). תאריכי ISO הם מחרוזות שממוינות בסדר כרונולוגי.
אתגר
קל?sort=titleמציג בסדר אלפביתי מ־A עד Z, ואילו?sort=createdAtמציג מהישן לחדש; סימן-בתחילת הערך הופך את הסדר.- כל שדה אחר מחזיר תשובת 400 עם הקוד
invalid_query. - המיון לעולם אינו משנה את הסדר השמור.
שלח GET /api/tasks?sort=-createdAt, לאחר מכן GET /api/tasks, ולאחר מכן GET /api/tasks?sort=color.
נסו בעצמכם
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;