/** * 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' }, ] };