chore: import codes
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
/**
|
||||
* 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 '@hykocx/zen/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' },
|
||||
]
|
||||
};
|
||||
Reference in New Issue
Block a user