الترتيب
جزء من قسم واجهات REST API في رحلة Node.js على Coddy. الدرس 11 من 18.
يريد العميل عرض أحدث المهام أولًا، أو ترتيب القائمة من A إلى Z. من الأعراف الشائعة استخدام ?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العناصر من الألف إلى الياء، ويعرض?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;