Etiketler
Coddy'nin Node.js Journey'sinin REST API'leri bölümünün bir parçası. Ders 24 / 25.
Görevler office veya money gibi etiketler alır ve pano uygulaması, her etiketin görev sayısını gösteren bir menü ister. Bu adım üç şey ekler: liste için bir şema kuralı, bir filtre ve etiketleri sayan bir rota. Aşağıda gösterilen şekilde bir nesneye sayım yapmak yeni.
// şemada genres: z.array(z.string()).max(3).optional()
const counts = {};
for (const book of books) {
for (const genre of book.genres) {
counts[genre] = (counts[genre] ?? 0) + 1;
}
}
// { poetry: 2, drama: 1 }Object.entries(counts), bu nesneyi bir listeye dönüştürmeye hazır [["poetry", 2], ...] çiftlerine dönüştürür. Bunun gibi bir özet, kendine ait URL'si olan bağımsız bir kaynaktır.
optional(), bu alanın bulunmamasına izin verirken mevcut olduğunda değerini yine de denetler. Object.entries(counts).map(([genre, count]) => ({ genre, count })) ile giriş çiftlerini nesnelere dönüştür: köşeli ayraçlar her çifti açar; parantezler bir nesne döndürür.
Görev
KolayPano uygulaması, görevleri office veya money gibi etiketlere göre gruplandırmak ve her etiketi, onu taşıyan görev sayısıyla birlikte gösteren bir menü sunmak istiyor. API’nize etiketleri ekleyin.
routes/tasks.jsdosyasındaTaskşemasına isteğe bağlı birlabelsalanı ekleyin: kırpma işleminden sonra her biri 1 ila 20 karakter olan, en fazla 5 etiketten oluşan bir liste. ŞemalarıTasküzerinden oluşturulduğu için POST ve PATCH de bu alanı alır. Geçersiz bir liste, alışılmış 400invalid_bodyhatasına yol açar. Geçersiz listelere"labels": "office"(liste değil, metin),"labels": [""]ve altı etiket dahildir.- Kaydedilen her görevin bir
labelslistesi olur:- Bir POST veya PUT isteğinde
labelsgönderilmezse[]kaydedin. PUT görevin tamamını değiştirdiği için, labels olmadan yapılan bir PUT etiketleri temizler. - PATCH,
labelsalanını yalnızca gönderildiğinde değiştirir.
- Bir POST veya PUT isteğinde
store.jsdosyasında başlangıç görevlerine etiketler ekleyin: 1. ve 2. görevler için["office"], 3. görev için["money"].- Yine
routes/tasks.jsdosyasında liste rotasına bir filtre ekleyin:GET /api/v1/tasks?label=officeyalnızcalabelslistesinde tam olarakofficebulunan görevleri döndürür. Önceden olduğu gibi diğer filtrelerle ve sayfalarla birlikte çalışır. server.jsdosyasında, yönlendiricinin bağlandığı yereGET /api/v1/labelsekleyin ve buradastore.jsdosyasındantasksöğesini içe aktarın. Kullanımda olan her etiketi, onu taşıyan görev sayısıyla birlikte döndürür; etiketleri A'dan Z'ye sıralar. Başlangıç görevleriyle sonuç şöyledir:[ { "label": "money", "count": 1 }, { "label": "office", "count": 2 } ]
Deneyin:
GET /api/v1/labelsyukarıdaki listeyi döndürür./api/v1/tasksadresine POST{ "title": "Order chairs", "labels": ["office"] }gönderin: 201 alırsınız ve yeni görevde"labels": ["office"]bulunur.GET /api/v1/labelsartıkofficeiçin"count": 3gösterir.- POST
{ "title": "Water the plants" }gönderin: yeni görevde"labels": []bulunur. GET /api/v1/tasks?label=moneyyalnızca 3. görevi döndürür.GET /api/v1/tasks?label=nope,"items": []ve"total": 0döndürür.PUT /api/v1/tasks/2adresine{ "title": "Book the meeting room", "done": false }gönderin; 2. görev"labels": []ile döndürülür.
Kendin dene
import { Router } from 'express';
import { z } from 'zod';
import { config } from '../config.js';
import { sendError, sendInvalid } from '../errors.js';
import { nextId, tasks } from '../store.js';
const router = Router();
// PUT tarafından gönderildiği şekliyle eksiksiz bir görev. .strict(), listelemediği alanları reddeder.
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 SORT_FIELDS = ['title', 'createdAt'];
// Büyük-küçük harf fark etmeksizin başka bir görev zaten bu başlığa sahipse true.
const titleTaken = (title, exceptId) =>
tasks.some((t) => t.id !== exceptId && t.title.toLowerCase() === title.toLowerCase());
router.get('/', (req, res) => {
const { done, q, sort } = 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 (sort !== undefined) {
const field = String(sort).startsWith('-') ? String(sort).slice(1) : String(sort);
if (!SORT_FIELDS.includes(field)) {
return sendError(
res,
400,
'invalid_query',
'sort must be title or createdAt, with - in front to reverse',
);
}
const direction = String(sort).startsWith('-') ? -1 : 1;
list = list.toSorted((a, b) => a[field].localeCompare(b[field]) * direction);
}
const page = req.query.page === undefined ? 1 : Number(req.query.page);
const limit = req.query.limit === undefined ? config.pageSize : Number(req.query.limit);
if (!Number.isInteger(page) || page < 1) {
return sendError(res, 400, 'invalid_query', 'page must be a whole number from 1');
}
if (!Number.isInteger(limit) || limit < 1 || limit > 50) {
return sendError(res, 400, 'invalid_query', 'limit must be a whole number from 1 to 50');
}
const start = (page - 1) * limit;
res.json({
items: list.slice(start, start + limit),
page,
total: list.length,
pages: Math.ceil(list.length / limit),
});
});
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/v1/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'leri bölümündeki tüm dersler
1Kaynaklar ve Fiiller
Kaynaklar ve URL'lerPOST ile Oluşturma4Tarayıcılar, Sürümler ve Yapılandırma
Origin NedirBaşka Bir Origin'e İzin VermeCORSAPI'yi Çağıran Bir SayfaKendi başına pratik yap: Express playground