chore: import codes

This commit is contained in:
2026-04-12 12:50:14 -04:00
parent 4bcb4898e8
commit 65ae3c6788
241 changed files with 48834 additions and 1 deletions
+472
View File
@@ -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' },
]
};