feat(backend): update collections, config and migration tools
Update Payload CMS configuration, collections (Audit, Posts), and add migration scripts/reports.
This commit is contained in:
207
apps/backend/scripts/migration/transformers.ts
Normal file
207
apps/backend/scripts/migration/transformers.ts
Normal file
@@ -0,0 +1,207 @@
|
||||
/**
|
||||
* Data Transformers
|
||||
* Story 1.3: Content Migration Script
|
||||
*
|
||||
* Transforms Webflow data to Payload CMS format
|
||||
*/
|
||||
|
||||
import type {
|
||||
PayloadCategory,
|
||||
PayloadPostData,
|
||||
PayloadPortfolioData,
|
||||
WebflowCategory,
|
||||
WebflowPost,
|
||||
WebflowPortfolioItem,
|
||||
} from './types'
|
||||
import { toSlug, splitColorToTextBackground, truncate, htmlToPlainText, parseDate } from './utils'
|
||||
import { htmlToLexical } from './lexicalConverter'
|
||||
|
||||
// ============================================================
|
||||
// CATEGORY TRANSFORMER
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Transform Webflow category to Payload CMS format
|
||||
*/
|
||||
export function transformCategory(
|
||||
webflowCategory: WebflowCategory,
|
||||
order: number = 0,
|
||||
): PayloadCategory {
|
||||
const { textColor, backgroundColor } = splitColorToTextBackground(
|
||||
webflowCategory.colorHex || '#ffffff',
|
||||
)
|
||||
|
||||
return {
|
||||
title: webflowCategory.name,
|
||||
nameEn: '', // Can be manually set later
|
||||
order,
|
||||
textColor,
|
||||
backgroundColor,
|
||||
slug: webflowCategory.slug || toSlug(webflowCategory.name),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform multiple categories
|
||||
*/
|
||||
export function transformCategories(
|
||||
webflowCategories: WebflowCategory[],
|
||||
): PayloadCategory[] {
|
||||
return webflowCategories.map((cat, index) => transformCategory(cat, index))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// POST TRANSFORMER
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Transform Webflow post to Payload CMS format
|
||||
*/
|
||||
export function transformPost(webflowPost: WebflowPost): PayloadPostData {
|
||||
// Generate excerpt from content if not provided
|
||||
const excerpt = webflowPost.excerpt || htmlToPlainText(webflowPost.content, 200)
|
||||
|
||||
// Convert HTML to Lexical JSON string format (for richText field storage)
|
||||
const lexicalContent = htmlToLexical(webflowPost.content || '')
|
||||
|
||||
return {
|
||||
title: webflowPost.title,
|
||||
slug: webflowPost.slug || toSlug(webflowPost.title),
|
||||
heroImage: undefined, // Will be set by media handler
|
||||
ogImage: undefined, // Will be set by media handler
|
||||
content: lexicalContent as any, // Lexical JSON string for richText field
|
||||
excerpt: truncate(excerpt, 200),
|
||||
publishedAt: parseDate(webflowPost.publishedDate),
|
||||
status: 'published',
|
||||
categories: [], // Will be resolved after categories are migrated
|
||||
meta: {
|
||||
title: webflowPost.seoTitle || webflowPost.title,
|
||||
description: webflowPost.seoDescription || excerpt,
|
||||
image: undefined, // Will be set by media handler
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform multiple posts
|
||||
*/
|
||||
export function transformPosts(webflowPosts: WebflowPost[]): PayloadPostData[] {
|
||||
return webflowPosts.map((post) => transformPost(post))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// PORTFOLIO TRANSFORMER
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Transform Webflow portfolio item to Payload CMS format
|
||||
*/
|
||||
export function transformPortfolio(
|
||||
webflowPortfolio: WebflowPortfolioItem,
|
||||
): PayloadPortfolioData {
|
||||
return {
|
||||
title: webflowPortfolio.name,
|
||||
slug: webflowPortfolio.slug || toSlug(webflowPortfolio.name),
|
||||
url: webflowPortfolio.websiteLink,
|
||||
image: undefined, // Will be set by media handler
|
||||
description: webflowPortfolio.description,
|
||||
websiteType: webflowPortfolio.websiteType || 'other',
|
||||
tags: parseTagsString(webflowPortfolio.tags),
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform multiple portfolio items
|
||||
*/
|
||||
export function transformPortfolios(
|
||||
webflowPortfolios: WebflowPortfolioItem[],
|
||||
): PayloadPortfolioData[] {
|
||||
return webflowPortfolios.map((item) => transformPortfolio(item))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// HELPER FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Parse comma-separated tags into array
|
||||
*/
|
||||
function parseTagsString(tagsString: string): Array<{ tag: string }> {
|
||||
if (!tagsString || typeof tagsString !== 'string') {
|
||||
return []
|
||||
}
|
||||
return tagsString
|
||||
.split(',')
|
||||
.map((tag) => tag.trim())
|
||||
.filter(Boolean)
|
||||
.map((tag) => ({ tag }))
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// VALIDATION FUNCTIONS
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* Validate transformed category
|
||||
*/
|
||||
export function validateCategory(category: PayloadCategory): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!category.title) {
|
||||
errors.push('Category title is required')
|
||||
}
|
||||
if (!category.slug) {
|
||||
errors.push('Category slug is required')
|
||||
}
|
||||
if (!category.textColor || !category.backgroundColor) {
|
||||
errors.push('Category colors are required')
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transformed post
|
||||
*/
|
||||
export function validatePost(post: PayloadPostData): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!post.title) {
|
||||
errors.push('Post title is required')
|
||||
}
|
||||
if (!post.slug) {
|
||||
errors.push('Post slug is required')
|
||||
}
|
||||
if (!post.content) {
|
||||
errors.push('Post content is required')
|
||||
}
|
||||
if (!post.publishedAt) {
|
||||
errors.push('Post published date is required')
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate transformed portfolio item
|
||||
*/
|
||||
export function validatePortfolio(
|
||||
portfolio: PayloadPortfolioData,
|
||||
): { valid: boolean; errors: string[] } {
|
||||
const errors: string[] = []
|
||||
|
||||
if (!portfolio.title) {
|
||||
errors.push('Portfolio title is required')
|
||||
}
|
||||
if (!portfolio.slug) {
|
||||
errors.push('Portfolio slug is required')
|
||||
}
|
||||
if (!portfolio.url) {
|
||||
errors.push('Portfolio URL is required')
|
||||
}
|
||||
if (!portfolio.websiteType) {
|
||||
errors.push('Portfolio website type is required')
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors }
|
||||
}
|
||||
Reference in New Issue
Block a user