This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
export { handleCreateArticle } from './handleCreateArticle.mjs';
|
||||
export { handleDeleteArticle } from './handleDeleteArticle.mjs';
|
||||
export { handleListArticles } from './handleListArticles.mjs';
|
||||
export { handleUpdateArticle } from './handleUpdateArticle.mjs';
|
||||
@@ -0,0 +1,20 @@
|
||||
import { readFile, rename, writeFile } from 'node:fs/promises';
|
||||
import { paths } from '../../config/paths.mjs';
|
||||
import { httpError } from '../../shared/http/httpError.mjs';
|
||||
|
||||
export async function readArticles() {
|
||||
const raw = await readFile(paths.articlesFile, 'utf8');
|
||||
const articles = JSON.parse(raw);
|
||||
|
||||
if (!Array.isArray(articles)) {
|
||||
throw httpError(500, 'Article database must be a JSON array');
|
||||
}
|
||||
|
||||
return articles;
|
||||
}
|
||||
|
||||
export async function writeArticles(articles) {
|
||||
const temporaryFile = `${paths.articlesFile}.${process.pid}.tmp`;
|
||||
await writeFile(temporaryFile, `${JSON.stringify(articles, null, 2)}\n`, 'utf8');
|
||||
await rename(temporaryFile, paths.articlesFile);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import { httpError } from '../../shared/http/httpError.mjs';
|
||||
|
||||
export function normalizeArticle(input) {
|
||||
if (!input || typeof input !== 'object' || Array.isArray(input)) {
|
||||
throw httpError(400, 'Article payload must be an object');
|
||||
}
|
||||
|
||||
const article = {
|
||||
...input,
|
||||
slug: readRequiredString(input, 'slug'),
|
||||
title: readRequiredString(input, 'title'),
|
||||
date: readRequiredString(input, 'date'),
|
||||
author: readRequiredString(input, 'author'),
|
||||
categories: normalizeCategories(input.categories),
|
||||
cover: readRequiredString(input, 'cover'),
|
||||
excerpt: readRequiredString(input, 'excerpt'),
|
||||
contentHtml: readRequiredString(input, 'contentHtml', { preserveWhitespace: true }),
|
||||
readingMinutes: normalizeReadingMinutes(input.readingMinutes),
|
||||
};
|
||||
|
||||
if (/[/?#]/.test(article.slug)) {
|
||||
throw httpError(400, 'Slug must not contain "/", "?" or "#"');
|
||||
}
|
||||
|
||||
if (Number.isNaN(new Date(article.date).getTime())) {
|
||||
throw httpError(400, 'Date must be a valid date string');
|
||||
}
|
||||
|
||||
return article;
|
||||
}
|
||||
|
||||
export function assertUniqueSlug(articles, slug, exceptSlug) {
|
||||
const duplicate = articles.find((article) => article.slug === slug && article.slug !== exceptSlug);
|
||||
|
||||
if (duplicate) {
|
||||
throw httpError(409, `Slug "${slug}" is already used`);
|
||||
}
|
||||
}
|
||||
|
||||
function readRequiredString(input, key, options = {}) {
|
||||
const value = input[key];
|
||||
|
||||
if (typeof value !== 'string') {
|
||||
throw httpError(400, `"${key}" must be a string`);
|
||||
}
|
||||
|
||||
const normalized = options.preserveWhitespace ? value : value.trim();
|
||||
if (!normalized) {
|
||||
throw httpError(400, `"${key}" is required`);
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function normalizeCategories(value) {
|
||||
const categories = Array.isArray(value)
|
||||
? value
|
||||
: String(value || '')
|
||||
.split(',')
|
||||
.map((category) => category.trim());
|
||||
|
||||
const normalized = categories.filter(Boolean);
|
||||
if (normalized.length === 0) {
|
||||
throw httpError(400, 'At least one category is required');
|
||||
}
|
||||
|
||||
return [...new Set(normalized)];
|
||||
}
|
||||
|
||||
function normalizeReadingMinutes(value) {
|
||||
const minutes = Number(value);
|
||||
|
||||
if (!Number.isInteger(minutes) || minutes < 1) {
|
||||
throw httpError(400, '"readingMinutes" must be a positive integer');
|
||||
}
|
||||
|
||||
return minutes;
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
import { readJsonBody } from '../../shared/http/readJsonBody.mjs';
|
||||
import { sendJson } from '../../shared/http/sendJson.mjs';
|
||||
import { readArticles, writeArticles } from './articleRepository.mjs';
|
||||
import { assertUniqueSlug, normalizeArticle } from './articleValidator.mjs';
|
||||
|
||||
export async function handleCreateArticle(request, response) {
|
||||
const payload = await readJsonBody(request);
|
||||
const article = normalizeArticle(payload.article || payload);
|
||||
const articles = await readArticles();
|
||||
|
||||
assertUniqueSlug(articles, article.slug);
|
||||
articles.unshift(article);
|
||||
await writeArticles(articles);
|
||||
|
||||
return sendJson(response, { article }, 201);
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
import { httpError } from '../../shared/http/httpError.mjs';
|
||||
import { sendJson } from '../../shared/http/sendJson.mjs';
|
||||
import { readArticles, writeArticles } from './articleRepository.mjs';
|
||||
|
||||
export async function handleDeleteArticle(_request, response, slug) {
|
||||
const articles = await readArticles();
|
||||
const index = articles.findIndex((article) => article.slug === slug);
|
||||
|
||||
if (index === -1) {
|
||||
throw httpError(404, `Article "${slug}" was not found`);
|
||||
}
|
||||
|
||||
const [article] = articles.splice(index, 1);
|
||||
await writeArticles(articles);
|
||||
|
||||
return sendJson(response, { article });
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { sendJson } from '../../shared/http/sendJson.mjs';
|
||||
import { readArticles } from './articleRepository.mjs';
|
||||
|
||||
export async function handleListArticles(_request, response) {
|
||||
return sendJson(response, { articles: await readArticles() });
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { httpError } from '../../shared/http/httpError.mjs';
|
||||
import { readJsonBody } from '../../shared/http/readJsonBody.mjs';
|
||||
import { sendJson } from '../../shared/http/sendJson.mjs';
|
||||
import { readArticles, writeArticles } from './articleRepository.mjs';
|
||||
import { assertUniqueSlug, normalizeArticle } from './articleValidator.mjs';
|
||||
|
||||
export async function handleUpdateArticle(request, response, currentSlug) {
|
||||
const payload = await readJsonBody(request);
|
||||
const articles = await readArticles();
|
||||
const index = articles.findIndex((article) => article.slug === currentSlug);
|
||||
|
||||
if (index === -1) {
|
||||
throw httpError(404, `Article "${currentSlug}" was not found`);
|
||||
}
|
||||
|
||||
const article = normalizeArticle(payload.article || payload);
|
||||
assertUniqueSlug(articles, article.slug, currentSlug);
|
||||
articles[index] = article;
|
||||
await writeArticles(articles);
|
||||
|
||||
return sendJson(response, { article });
|
||||
}
|
||||
Reference in New Issue
Block a user