81172bda94
Update all references across source files, documentation, and configuration to reflect the new package scope and name. This includes updating `.npmrc` registry config, install instructions, module examples, and all import path comments throughout the codebase.
473 lines
16 KiB
JavaScript
473 lines
16 KiB
JavaScript
/**
|
|
* Posts Module - API Routes
|
|
*/
|
|
|
|
import {
|
|
createPost,
|
|
getPostById,
|
|
getPostBySlug,
|
|
getPosts,
|
|
searchPosts,
|
|
updatePost,
|
|
deletePost
|
|
} from './crud.js';
|
|
|
|
import {
|
|
createCategory,
|
|
getCategoryById,
|
|
getCategories,
|
|
getActiveCategories,
|
|
updateCategory,
|
|
deleteCategory
|
|
} from './categories/crud.js';
|
|
|
|
import {
|
|
uploadImage,
|
|
deleteFile,
|
|
generateBlogFilePath,
|
|
generateUniqueFilename,
|
|
validateUpload,
|
|
FILE_TYPE_PRESETS,
|
|
FILE_SIZE_LIMITS
|
|
} from '@zen/core/storage';
|
|
|
|
import { getPostsConfig, getPostType } from './config.js';
|
|
|
|
// ============================================================================
|
|
// Config
|
|
// ============================================================================
|
|
|
|
async function handleGetConfig() {
|
|
try {
|
|
const config = getPostsConfig();
|
|
return { success: true, config };
|
|
} catch (error) {
|
|
return { success: false, error: error.message || 'Failed to get config' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Posts (admin)
|
|
// ============================================================================
|
|
|
|
async function handleGetPosts(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!getPostType(postType)) return { success: false, error: `Unknown post type: ${postType}` };
|
|
|
|
const id = url.searchParams.get('id');
|
|
if (id) {
|
|
const post = await getPostById(postType, parseInt(id));
|
|
if (!post) return { success: false, error: 'Post not found' };
|
|
return { success: true, post };
|
|
}
|
|
|
|
const page = parseInt(url.searchParams.get('page')) || 1;
|
|
const limit = parseInt(url.searchParams.get('limit')) || 20;
|
|
const search = url.searchParams.get('search') || '';
|
|
const category_id = url.searchParams.get('category_id') || null;
|
|
const sortBy = url.searchParams.get('sortBy') || 'created_at';
|
|
const sortOrder = url.searchParams.get('sortOrder') || 'DESC';
|
|
const withRelations = url.searchParams.get('withRelations') === 'true';
|
|
|
|
const result = await getPosts(postType, {
|
|
page,
|
|
limit,
|
|
search,
|
|
category_id: category_id ? parseInt(category_id) : null,
|
|
sortBy,
|
|
sortOrder,
|
|
withRelations
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
posts: result.posts,
|
|
total: result.pagination.total,
|
|
totalPages: result.pagination.totalPages,
|
|
page: result.pagination.page,
|
|
limit: result.pagination.limit
|
|
};
|
|
} catch (error) {
|
|
console.error('[Posts] Error GET posts:', error);
|
|
return { success: false, error: error.message || 'Failed to fetch posts' };
|
|
}
|
|
}
|
|
|
|
async function handleCreatePost(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
|
|
const body = await request.json();
|
|
const postData = body.post || body;
|
|
if (!postData || Object.keys(postData).length === 0) {
|
|
return { success: false, error: 'Post data is required' };
|
|
}
|
|
|
|
const post = await createPost(postType, postData);
|
|
return { success: true, post, message: 'Post created successfully' };
|
|
} catch (error) {
|
|
console.error('[Posts] Error creating post:', error);
|
|
return { success: false, error: error.message || 'Failed to create post' };
|
|
}
|
|
}
|
|
|
|
async function handleUpdatePost(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const body = await request.json();
|
|
const postType = url.searchParams.get('type');
|
|
const id = url.searchParams.get('id') || body.id;
|
|
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!id) return { success: false, error: 'Post ID is required' };
|
|
|
|
const updates = body.post || (({ id: _i, ...rest }) => rest)(body);
|
|
if (!updates || Object.keys(updates).length === 0) {
|
|
return { success: false, error: 'Update data is required' };
|
|
}
|
|
|
|
const typeConfig = getPostType(postType);
|
|
|
|
// Handle old image cleanup
|
|
const existing = await getPostById(postType, parseInt(id));
|
|
if (!existing) return { success: false, error: 'Post not found' };
|
|
|
|
const post = await updatePost(postType, parseInt(id), updates);
|
|
|
|
// Clean up replaced images
|
|
if (typeConfig) {
|
|
for (const field of typeConfig.fields.filter(f => f.type === 'image')) {
|
|
const oldKey = existing._data?.[field.name] || null;
|
|
const newKey = updates[field.name];
|
|
if (oldKey && newKey !== undefined && newKey !== oldKey) {
|
|
try {
|
|
await deleteFile(oldKey);
|
|
} catch (err) {
|
|
console.warn(`[Posts] Error deleting old image ${oldKey}:`, err.message);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return { success: true, post, message: 'Post updated successfully' };
|
|
} catch (error) {
|
|
console.error('[Posts] Error updating post:', error);
|
|
return { success: false, error: error.message || 'Failed to update post' };
|
|
}
|
|
}
|
|
|
|
async function handleDeletePost(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
const id = url.searchParams.get('id');
|
|
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!id) return { success: false, error: 'Post ID is required' };
|
|
|
|
const deleted = await deletePost(postType, parseInt(id));
|
|
if (!deleted) return { success: false, error: 'Post not found' };
|
|
return { success: true, message: 'Post deleted successfully' };
|
|
} catch (error) {
|
|
console.error('[Posts] Error deleting post:', error);
|
|
return { success: false, error: 'Failed to delete post' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Image upload (admin)
|
|
// ============================================================================
|
|
|
|
async function handleUploadImage(request) {
|
|
try {
|
|
const formData = await request.formData();
|
|
const file = formData.get('file');
|
|
|
|
if (!file) return { success: false, error: 'No file provided' };
|
|
|
|
const validation = validateUpload({
|
|
filename: file.name,
|
|
size: file.size,
|
|
allowedTypes: FILE_TYPE_PRESETS.IMAGES,
|
|
maxSize: FILE_SIZE_LIMITS.IMAGE
|
|
});
|
|
|
|
if (!validation.valid) {
|
|
return { success: false, error: validation.errors.join(', ') };
|
|
}
|
|
|
|
const uniqueFilename = generateUniqueFilename(file.name);
|
|
const key = generateBlogFilePath(Date.now(), uniqueFilename);
|
|
const buffer = Buffer.from(await file.arrayBuffer());
|
|
|
|
const uploadResult = await uploadImage({
|
|
key,
|
|
body: buffer,
|
|
contentType: file.type,
|
|
metadata: { originalName: file.name }
|
|
});
|
|
|
|
if (!uploadResult.success) {
|
|
return { success: false, error: uploadResult.error || 'Upload failed' };
|
|
}
|
|
|
|
return { success: true, key: uploadResult.data.key };
|
|
} catch (error) {
|
|
console.error('[Posts] Error uploading image:', error);
|
|
return { success: false, error: error.message || 'Upload failed' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Categories (admin)
|
|
// ============================================================================
|
|
|
|
async function handleGetCategories(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
|
|
const id = url.searchParams.get('id');
|
|
if (id) {
|
|
const category = await getCategoryById(postType, parseInt(id));
|
|
if (!category) return { success: false, error: 'Category not found' };
|
|
return { success: true, category };
|
|
}
|
|
|
|
const page = parseInt(url.searchParams.get('page')) || 1;
|
|
const limit = parseInt(url.searchParams.get('limit')) || 20;
|
|
const search = url.searchParams.get('search') || '';
|
|
const is_active = url.searchParams.get('is_active');
|
|
const sortBy = url.searchParams.get('sortBy') || 'title';
|
|
const sortOrder = url.searchParams.get('sortOrder') || 'ASC';
|
|
|
|
const result = await getCategories(postType, {
|
|
page,
|
|
limit,
|
|
search,
|
|
is_active: is_active === 'true' ? true : is_active === 'false' ? false : null,
|
|
sortBy,
|
|
sortOrder
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
categories: result.categories,
|
|
total: result.pagination.total,
|
|
totalPages: result.pagination.totalPages,
|
|
page: result.pagination.page,
|
|
limit: result.pagination.limit
|
|
};
|
|
} catch (error) {
|
|
console.error('[Posts] Error GET categories:', error);
|
|
return { success: false, error: error.message || 'Failed to fetch categories' };
|
|
}
|
|
}
|
|
|
|
async function handleCreateCategory(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
|
|
const body = await request.json();
|
|
const categoryData = body.category || body;
|
|
if (!categoryData || Object.keys(categoryData).length === 0) {
|
|
return { success: false, error: 'Category data is required' };
|
|
}
|
|
|
|
const category = await createCategory(postType, categoryData);
|
|
return { success: true, category, message: 'Category created successfully' };
|
|
} catch (error) {
|
|
console.error('[Posts] Error creating category:', error);
|
|
return { success: false, error: error.message || 'Failed to create category' };
|
|
}
|
|
}
|
|
|
|
async function handleUpdateCategory(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const body = await request.json();
|
|
const postType = url.searchParams.get('type');
|
|
const id = url.searchParams.get('id') || body.id;
|
|
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!id) return { success: false, error: 'Category ID is required' };
|
|
|
|
const updates = body.category || (({ id: _i, ...rest }) => rest)(body);
|
|
if (!updates || Object.keys(updates).length === 0) {
|
|
return { success: false, error: 'Update data is required' };
|
|
}
|
|
|
|
const existing = await getCategoryById(postType, parseInt(id));
|
|
if (!existing) return { success: false, error: 'Category not found' };
|
|
|
|
const category = await updateCategory(postType, parseInt(id), updates);
|
|
return { success: true, category, message: 'Category updated successfully' };
|
|
} catch (error) {
|
|
console.error('[Posts] Error updating category:', error);
|
|
return { success: false, error: error.message || 'Failed to update category' };
|
|
}
|
|
}
|
|
|
|
async function handleDeleteCategory(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
const id = url.searchParams.get('id');
|
|
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!id) return { success: false, error: 'Category ID is required' };
|
|
|
|
await deleteCategory(postType, parseInt(id));
|
|
return { success: true, message: 'Category deleted successfully' };
|
|
} catch (error) {
|
|
console.error('[Posts] Error deleting category:', error);
|
|
if (error.message.includes('Cannot delete')) {
|
|
return { success: false, error: error.message };
|
|
}
|
|
return { success: false, error: 'Failed to delete category' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Relation search (admin — used by RelationSelector picker)
|
|
// ============================================================================
|
|
|
|
async function handleSearchPosts(request) {
|
|
try {
|
|
const url = new URL(request.url);
|
|
const postType = url.searchParams.get('type');
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!getPostType(postType)) return { success: false, error: `Unknown post type: ${postType}` };
|
|
|
|
const q = url.searchParams.get('q') || '';
|
|
const limit = parseInt(url.searchParams.get('limit')) || 20;
|
|
|
|
const posts = await searchPosts(postType, q, limit);
|
|
return { success: true, posts };
|
|
} catch (error) {
|
|
console.error('[Posts] Error searching posts:', error);
|
|
return { success: false, error: error.message || 'Failed to search posts' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Public API
|
|
// ============================================================================
|
|
|
|
async function handlePublicGetConfig() {
|
|
try {
|
|
const config = getPostsConfig();
|
|
return { success: true, config };
|
|
} catch (error) {
|
|
return { success: false, error: error.message || 'Failed to get config' };
|
|
}
|
|
}
|
|
|
|
async function handlePublicGetPosts(request, params) {
|
|
try {
|
|
const postType = params?.type;
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
if (!getPostType(postType)) return { success: false, error: `Unknown post type: ${postType}` };
|
|
|
|
const url = new URL(request.url);
|
|
const page = parseInt(url.searchParams.get('page')) || 1;
|
|
const limit = parseInt(url.searchParams.get('limit')) || 20;
|
|
const category_id = url.searchParams.get('category_id') || null;
|
|
const sortBy = url.searchParams.get('sortBy') || 'created_at';
|
|
const sortOrder = (url.searchParams.get('sortOrder') || 'DESC').toUpperCase();
|
|
const withRelations = url.searchParams.get('withRelations') === 'true';
|
|
|
|
const result = await getPosts(postType, {
|
|
page,
|
|
limit,
|
|
category_id: category_id ? parseInt(category_id) : null,
|
|
sortBy,
|
|
sortOrder,
|
|
withRelations
|
|
});
|
|
|
|
return {
|
|
success: true,
|
|
posts: result.posts,
|
|
total: result.pagination.total,
|
|
totalPages: result.pagination.totalPages,
|
|
page: result.pagination.page,
|
|
limit: result.pagination.limit
|
|
};
|
|
} catch (error) {
|
|
console.error('[Posts] Error public GET posts:', error);
|
|
return { success: false, error: error.message || 'Failed to fetch posts' };
|
|
}
|
|
}
|
|
|
|
async function handlePublicGetPostBySlug(request, params) {
|
|
try {
|
|
const postType = params?.type;
|
|
const slug = params?.slug;
|
|
if (!postType || !slug) return { success: false, error: 'Post type and slug are required' };
|
|
|
|
const post = await getPostBySlug(postType, slug);
|
|
if (!post) return { success: false, error: 'Post not found' };
|
|
return { success: true, post };
|
|
} catch (error) {
|
|
console.error('[Posts] Error public GET post by slug:', error);
|
|
return { success: false, error: error.message || 'Failed to fetch post' };
|
|
}
|
|
}
|
|
|
|
async function handlePublicGetCategories(request, params) {
|
|
try {
|
|
const postType = params?.type;
|
|
if (!postType) return { success: false, error: 'Post type is required' };
|
|
|
|
const categories = await getActiveCategories(postType);
|
|
return { success: true, categories };
|
|
} catch (error) {
|
|
console.error('[Posts] Error public GET categories:', error);
|
|
return { success: false, error: error.message || 'Failed to fetch categories' };
|
|
}
|
|
}
|
|
|
|
// ============================================================================
|
|
// Route Definitions
|
|
// ============================================================================
|
|
|
|
export default {
|
|
routes: [
|
|
// Admin config
|
|
{ path: '/admin/posts/config', method: 'GET', handler: handleGetConfig, auth: 'admin' },
|
|
|
|
// Admin posts
|
|
{ path: '/admin/posts/posts', method: 'GET', handler: handleGetPosts, auth: 'admin' },
|
|
{ path: '/admin/posts/posts', method: 'POST', handler: handleCreatePost, auth: 'admin' },
|
|
{ path: '/admin/posts/posts', method: 'PUT', handler: handleUpdatePost, auth: 'admin' },
|
|
{ path: '/admin/posts/posts', method: 'DELETE', handler: handleDeletePost, auth: 'admin' },
|
|
|
|
// Admin image upload
|
|
{ path: '/admin/posts/upload-image', method: 'POST', handler: handleUploadImage, auth: 'admin' },
|
|
|
|
// Admin relation search (for RelationSelector picker)
|
|
{ path: '/admin/posts/search', method: 'GET', handler: handleSearchPosts, auth: 'admin' },
|
|
|
|
// Admin categories
|
|
{ path: '/admin/posts/categories', method: 'GET', handler: handleGetCategories, auth: 'admin' },
|
|
{ path: '/admin/posts/categories', method: 'POST', handler: handleCreateCategory, auth: 'admin' },
|
|
{ path: '/admin/posts/categories', method: 'PUT', handler: handleUpdateCategory, auth: 'admin' },
|
|
{ path: '/admin/posts/categories', method: 'DELETE', handler: handleDeleteCategory, auth: 'admin' },
|
|
|
|
// Public
|
|
{ path: '/posts/config', method: 'GET', handler: handlePublicGetConfig, auth: 'public' },
|
|
{ path: '/posts/:type', method: 'GET', handler: handlePublicGetPosts, auth: 'public' },
|
|
{ path: '/posts/:type/:slug', method: 'GET', handler: handlePublicGetPostBySlug, auth: 'public' },
|
|
{ path: '/posts/:type/categories', method: 'GET', handler: handlePublicGetCategories, auth: 'public' },
|
|
]
|
|
};
|