diff --git a/src/main/engine/ApplyValidationDataService.ts b/src/main/engine/ApplyValidationDataService.ts new file mode 100644 index 0000000..d6026af --- /dev/null +++ b/src/main/engine/ApplyValidationDataService.ts @@ -0,0 +1,121 @@ +import type { PostData } from './PostEngine'; + +function resolvePostCreatedAt(post: { createdAt: Date | string }): Date { + if (post.createdAt instanceof Date) { + return post.createdAt; + } + + const parsed = new Date(post.createdAt); + return Number.isNaN(parsed.getTime()) ? new Date() : parsed; +} + +export function buildApplyValidationArchives(posts: PostData[]): { + allCategories: Set; + allTags: Set; + years: Map; + yearMonths: Map; + yearMonthDays: Map; +} { + const allCategories = new Set(); + const allTags = new Set(); + const years = new Map(); + const yearMonths = new Map(); + const yearMonthDays = new Map(); + + for (const post of posts) { + for (const category of post.categories || []) allCategories.add(category); + for (const tag of post.tags || []) allTags.add(tag); + + const createdAt = resolvePostCreatedAt(post); + const updatedAt = post.updatedAt; + const year = createdAt.getFullYear(); + const month = String(createdAt.getMonth() + 1).padStart(2, '0'); + const day = String(createdAt.getDate()).padStart(2, '0'); + const ymKey = `${year}/${month}`; + const ymdKey = `${year}/${month}/${day}`; + + if (!years.has(year) || updatedAt > years.get(year)!) { + years.set(year, updatedAt); + } + if (!yearMonths.has(ymKey) || updatedAt > yearMonths.get(ymKey)!) { + yearMonths.set(ymKey, updatedAt); + } + if (!yearMonthDays.has(ymdKey) || updatedAt > yearMonthDays.get(ymdKey)!) { + yearMonthDays.set(ymdKey, updatedAt); + } + } + + return { + allCategories, + allTags, + years, + yearMonths, + yearMonthDays, + }; +} + +export function selectRequestedPosts(params: { + publishedPosts: PostData[]; + requestedPostIds: Set; + requestedPageSlugs: Set; +}): { + requestedSinglePosts: PostData[]; + requestedPagePosts: PostData[]; +} { + const requestedSinglePosts = params.publishedPosts.filter((post) => params.requestedPostIds.has(post.id)); + const requestedPagePosts = params.publishedPosts.filter((post) => { + if (!params.requestedPageSlugs.has(post.slug)) { + return false; + } + const categories = Array.isArray(post.categories) ? post.categories : []; + return categories.includes('page'); + }); + + return { + requestedSinglePosts, + requestedPagePosts, + }; +} + +export function buildRequestedArchiveMaps(params: { + requestedYears: Set; + requestedYearMonths: Set; + requestedYearMonthDays: Set; + years: Map; + yearMonths: Map; + yearMonthDays: Map; +}): { + requestedYearsMap: Map; + requestedYearMonthsMap: Map; + requestedYearMonthDaysMap: Map; +} { + const requestedYearsMap = new Map(); + for (const year of params.requestedYears) { + const lastmod = params.years.get(year); + if (lastmod) { + requestedYearsMap.set(year, lastmod); + } + } + + const requestedYearMonthsMap = new Map(); + for (const ym of params.requestedYearMonths) { + const lastmod = params.yearMonths.get(ym); + if (lastmod) { + requestedYearMonthsMap.set(ym, lastmod); + } + } + + const requestedYearMonthDaysMap = new Map(); + for (const ymd of params.requestedYearMonthDays) { + const lastmod = params.yearMonthDays.get(ymd); + if (lastmod) { + requestedYearMonthDaysMap.set(ymd, lastmod); + } + } + + return { + requestedYearsMap, + requestedYearMonthsMap, + requestedYearMonthDaysMap, + }; +} diff --git a/src/main/engine/BlogGenerationEngine.ts b/src/main/engine/BlogGenerationEngine.ts index 0e29c96..e681674 100644 --- a/src/main/engine/BlogGenerationEngine.ts +++ b/src/main/engine/BlogGenerationEngine.ts @@ -39,6 +39,11 @@ import { generateSinglePostPages, generateTagPages, } from './RoutePageGenerationService'; +import { + buildApplyValidationArchives, + buildRequestedArchiveMaps, + selectRequestedPosts, +} from './ApplyValidationDataService'; const DEFAULT_MAX_POSTS_PER_PAGE = 50; const MIN_MAX_POSTS_PER_PAGE = 1; @@ -568,34 +573,7 @@ export class BlogGenerationEngine { const { publishedPosts, publishedListPosts } = await loadPublishedGenerationSets(this.postEngine, listExcludedCategories); const generationPostIndex = buildGenerationPostIndex(publishedListPosts); - const allCategories = new Set(); - const allTags = new Set(); - const years = new Map(); - const yearMonths = new Map(); - const yearMonthDays = new Map(); - - for (const post of publishedListPosts) { - for (const category of post.categories || []) allCategories.add(category); - for (const tag of post.tags || []) allTags.add(tag); - - const createdAt = resolvePostCreatedAt(post); - const updatedAt = post.updatedAt; - const year = createdAt.getFullYear(); - const month = String(createdAt.getMonth() + 1).padStart(2, '0'); - const day = String(createdAt.getDate()).padStart(2, '0'); - const ymKey = `${year}/${month}`; - const ymdKey = `${year}/${month}/${day}`; - - if (!years.has(year) || updatedAt > years.get(year)!) { - years.set(year, updatedAt); - } - if (!yearMonths.has(ymKey) || updatedAt > yearMonths.get(ymKey)!) { - yearMonths.set(ymKey, updatedAt); - } - if (!yearMonthDays.has(ymdKey) || updatedAt > yearMonthDays.get(ymdKey)!) { - yearMonthDays.set(ymdKey, updatedAt); - } - } + const { allCategories, allTags, years, yearMonths, yearMonthDays } = buildApplyValidationArchives(publishedListPosts); const targetedPlan = buildTargetedValidationPlan({ initialPlan: missingPathPlan, @@ -629,38 +607,20 @@ export class BlogGenerationEngine { // no-op for applyValidation }; - const requestedSinglePosts = publishedPosts.filter((post) => targetedPlan.requestedPostIds.has(post.id)); - const requestedPagePosts = publishedPosts.filter((post) => { - if (!targetedPlan.requestedPageSlugs.has(post.slug)) { - return false; - } - const categories = Array.isArray(post.categories) ? post.categories : []; - return categories.includes('page'); + const { requestedSinglePosts, requestedPagePosts } = selectRequestedPosts({ + publishedPosts, + requestedPostIds: targetedPlan.requestedPostIds, + requestedPageSlugs: targetedPlan.requestedPageSlugs, }); - const requestedYearsMap = new Map(); - for (const year of targetedPlan.requestedYears) { - const lastmod = years.get(year); - if (lastmod) { - requestedYearsMap.set(year, lastmod); - } - } - - const requestedYearMonthsMap = new Map(); - for (const ym of targetedPlan.requestedYearMonths) { - const lastmod = yearMonths.get(ym); - if (lastmod) { - requestedYearMonthsMap.set(ym, lastmod); - } - } - - const requestedYearMonthDaysMap = new Map(); - for (const ymd of targetedPlan.requestedYearMonthDays) { - const lastmod = yearMonthDays.get(ymd); - if (lastmod) { - requestedYearMonthDaysMap.set(ymd, lastmod); - } - } + const { requestedYearsMap, requestedYearMonthsMap, requestedYearMonthDaysMap } = buildRequestedArchiveMaps({ + requestedYears: targetedPlan.requestedYears, + requestedYearMonths: targetedPlan.requestedYearMonths, + requestedYearMonthDays: targetedPlan.requestedYearMonthDays, + years, + yearMonths, + yearMonthDays, + }); onProgress( 48, diff --git a/tests/engine/ApplyValidationDataService.test.ts b/tests/engine/ApplyValidationDataService.test.ts new file mode 100644 index 0000000..2e754f8 --- /dev/null +++ b/tests/engine/ApplyValidationDataService.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from 'vitest'; +import type { PostData } from '../../src/main/engine/PostEngine'; +import { + buildApplyValidationArchives, + buildRequestedArchiveMaps, + selectRequestedPosts, +} from '../../src/main/engine/ApplyValidationDataService'; + +function makePost(overrides: Partial = {}): PostData { + const createdAt = overrides.createdAt ?? new Date('2025-01-15T10:00:00.000Z'); + return { + id: overrides.id ?? 'post-1', + projectId: overrides.projectId ?? 'project', + title: overrides.title ?? 'Title', + slug: overrides.slug ?? 'title', + excerpt: overrides.excerpt, + content: overrides.content ?? 'Body', + status: overrides.status ?? 'published', + author: overrides.author, + createdAt, + updatedAt: overrides.updatedAt ?? createdAt, + publishedAt: overrides.publishedAt, + tags: overrides.tags ?? [], + categories: overrides.categories ?? [], + }; +} + +describe('ApplyValidationDataService', () => { + it('builds category tag and date archives from list posts', () => { + const posts = [ + makePost({ id: '1', categories: ['news'], tags: ['t1'], createdAt: new Date('2025-01-15T00:00:00.000Z') }), + makePost({ id: '2', categories: ['page'], tags: [], createdAt: new Date('2025-02-20T00:00:00.000Z') }), + ]; + + const result = buildApplyValidationArchives(posts); + + expect(result.allCategories.has('news')).toBe(true); + expect(result.allCategories.has('page')).toBe(true); + expect(result.allTags.has('t1')).toBe(true); + expect(result.years.has(2025)).toBe(true); + expect(result.yearMonths.has('2025/01')).toBe(true); + expect(result.yearMonths.has('2025/02')).toBe(true); + expect(result.yearMonthDays.has('2025/01/15')).toBe(true); + expect(result.yearMonthDays.has('2025/02/20')).toBe(true); + }); + + it('selects requested single/page posts and resolves requested date maps', () => { + const publishedPosts = [ + makePost({ id: 'a', slug: 'post-a', categories: ['news'], createdAt: new Date('2025-01-15T00:00:00.000Z') }), + makePost({ id: 'b', slug: 'about', categories: ['page'], createdAt: new Date('2025-01-10T00:00:00.000Z') }), + ]; + + const selected = selectRequestedPosts({ + publishedPosts, + requestedPostIds: new Set(['a']), + requestedPageSlugs: new Set(['about']), + }); + + expect(selected.requestedSinglePosts.map((p) => p.id)).toEqual(['a']); + expect(selected.requestedPagePosts.map((p) => p.id)).toEqual(['b']); + + const archives = buildApplyValidationArchives(publishedPosts); + const requested = buildRequestedArchiveMaps({ + requestedYears: new Set([2025]), + requestedYearMonths: new Set(['2025/01']), + requestedYearMonthDays: new Set(['2025/01/15']), + years: archives.years, + yearMonths: archives.yearMonths, + yearMonthDays: archives.yearMonthDays, + }); + + expect(requested.requestedYearsMap.has(2025)).toBe(true); + expect(requested.requestedYearMonthsMap.has('2025/01')).toBe(true); + expect(requested.requestedYearMonthDaysMap.has('2025/01/15')).toBe(true); + }); +});