feat: better conflict resolution management
This commit is contained in:
@@ -11,16 +11,31 @@ import { getMacroConfigMap, type MacroConfig } from '../config/macroConfig';
|
||||
export type PostAnalysisStatus = 'new' | 'update' | 'conflict' | 'content-duplicate';
|
||||
export type MediaAnalysisStatus = 'new' | 'update' | 'conflict' | 'content-duplicate' | 'missing';
|
||||
|
||||
/** How to resolve a slug conflict during import */
|
||||
export type ImportConflictResolution = 'ignore' | 'overwrite' | 'import';
|
||||
|
||||
export interface AnalyzedPost {
|
||||
wxrPost: WxrPost;
|
||||
status: PostAnalysisStatus;
|
||||
contentHash: string;
|
||||
markdownPreview: string;
|
||||
/** How to resolve conflict (only relevant when status is 'conflict'). Default is 'ignore'. */
|
||||
conflictResolution?: ImportConflictResolution;
|
||||
existingPost?: {
|
||||
id: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
checksum: string | null;
|
||||
/** Date the existing post was created/published */
|
||||
pubDate: string | null;
|
||||
/** Excerpt from existing post */
|
||||
excerpt: string | null;
|
||||
/** Author of the existing post */
|
||||
author: string | null;
|
||||
/** Tags of the existing post */
|
||||
tags: string[];
|
||||
/** Categories of the existing post */
|
||||
categories: string[];
|
||||
};
|
||||
}
|
||||
|
||||
@@ -205,6 +220,13 @@ export class ImportAnalysisEngine {
|
||||
slug: posts.slug,
|
||||
title: posts.title,
|
||||
checksum: posts.checksum,
|
||||
excerpt: posts.excerpt,
|
||||
author: posts.author,
|
||||
publishedAt: posts.publishedAt,
|
||||
createdAt: posts.createdAt,
|
||||
status: posts.status,
|
||||
tags: posts.tags,
|
||||
categories: posts.categories,
|
||||
})
|
||||
.from(posts)
|
||||
.where(eq(posts.projectId, this.currentProjectId))
|
||||
@@ -261,9 +283,9 @@ export class ImportAnalysisEngine {
|
||||
|
||||
// Analyze posts
|
||||
const analyzedPosts = this.analyzePostItems(wxrData.posts, slugToPost, checksumToPost);
|
||||
|
||||
|
||||
this.onProgress?.('Analyzing pages...', `${wxrData.pages.length} pages to analyze`);
|
||||
|
||||
|
||||
const analyzedPages = this.analyzePostItems(wxrData.pages, slugToPost, checksumToPost);
|
||||
|
||||
this.onProgress?.('Analyzing media files...', `${wxrData.media.length} media files to analyze`);
|
||||
@@ -307,8 +329,8 @@ export class ImportAnalysisEngine {
|
||||
|
||||
private analyzePostItems(
|
||||
wxrPosts: WxrPost[],
|
||||
slugToPost: Map<string, { id: string; slug: string; title: string; checksum: string | null }>,
|
||||
checksumToPost: Map<string, { id: string; slug: string; title: string; checksum: string | null }>,
|
||||
slugToPost: Map<string, { id: string; slug: string; title: string; checksum: string | null; excerpt: string | null; author: string | null; publishedAt: Date | null; createdAt: Date; status: string; tags: string | null; categories: string | null }>,
|
||||
checksumToPost: Map<string, { id: string; slug: string; title: string; checksum: string | null; excerpt: string | null; author: string | null; publishedAt: Date | null; createdAt: Date; status: string; tags: string | null; categories: string | null }>,
|
||||
): AnalyzedPost[] {
|
||||
return wxrPosts.map(wxrPost => {
|
||||
const markdown = this.convertToMarkdown(wxrPost.content);
|
||||
@@ -327,25 +349,44 @@ export class ImportAnalysisEngine {
|
||||
} else {
|
||||
status = 'conflict';
|
||||
}
|
||||
const existingDate = existingBySlug.publishedAt || existingBySlug.createdAt;
|
||||
const existingTags = existingBySlug.tags ? JSON.parse(existingBySlug.tags) : [];
|
||||
const existingCategories = existingBySlug.categories ? JSON.parse(existingBySlug.categories) : [];
|
||||
existingPost = {
|
||||
id: existingBySlug.id,
|
||||
title: existingBySlug.title,
|
||||
slug: existingBySlug.slug,
|
||||
checksum: existingBySlug.checksum,
|
||||
pubDate: existingDate ? existingDate.toISOString() : null,
|
||||
excerpt: existingBySlug.excerpt,
|
||||
author: existingBySlug.author,
|
||||
tags: existingTags,
|
||||
categories: existingCategories,
|
||||
};
|
||||
} else if (existingByHash) {
|
||||
status = 'content-duplicate';
|
||||
const existingDate = existingByHash.publishedAt || existingByHash.createdAt;
|
||||
const existingTagsByHash = existingByHash.tags ? JSON.parse(existingByHash.tags) : [];
|
||||
const existingCategoriesByHash = existingByHash.categories ? JSON.parse(existingByHash.categories) : [];
|
||||
existingPost = {
|
||||
id: existingByHash.id,
|
||||
title: existingByHash.title,
|
||||
slug: existingByHash.slug,
|
||||
checksum: existingByHash.checksum,
|
||||
pubDate: existingDate ? existingDate.toISOString() : null,
|
||||
excerpt: existingByHash.excerpt,
|
||||
author: existingByHash.author,
|
||||
tags: existingTagsByHash,
|
||||
categories: existingCategoriesByHash,
|
||||
};
|
||||
} else {
|
||||
status = 'new';
|
||||
}
|
||||
|
||||
return { wxrPost, status, contentHash, markdownPreview, existingPost };
|
||||
// For conflicts, default resolution is 'ignore'
|
||||
const conflictResolution = status === 'conflict' ? 'ignore' as const : undefined;
|
||||
|
||||
return { wxrPost, status, contentHash, markdownPreview, existingPost, conflictResolution };
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@ import { getDatabase } from '../database';
|
||||
import { posts, Post, NewPost, postLinks } from '../database/schema';
|
||||
import { taskManager, Task } from './TaskManager';
|
||||
import { stemText, stemQuery, SupportedLanguage } from './stemmer';
|
||||
import { readPostFile as readPostFileShared, type PostFileData } from './postFileUtils';
|
||||
|
||||
export interface PostData {
|
||||
id: string;
|
||||
@@ -275,38 +276,13 @@ export class PostEngine extends EventEmitter {
|
||||
}
|
||||
|
||||
private async readPostFile(filePath: string): Promise<PostData | null> {
|
||||
try {
|
||||
// Check if file exists first to avoid noisy errors
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
// File doesn't exist - this is expected when DB has stale paths
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content: body } = matter(content);
|
||||
const metadata = data as PostMetadata;
|
||||
|
||||
return {
|
||||
id: metadata.id,
|
||||
projectId: metadata.projectId || this.currentProjectId,
|
||||
title: metadata.title,
|
||||
slug: metadata.slug,
|
||||
excerpt: metadata.excerpt,
|
||||
content: body,
|
||||
status: metadata.status,
|
||||
author: metadata.author,
|
||||
createdAt: new Date(metadata.createdAt),
|
||||
updatedAt: new Date(metadata.updatedAt),
|
||||
publishedAt: metadata.publishedAt ? new Date(metadata.publishedAt) : undefined,
|
||||
tags: metadata.tags || [],
|
||||
categories: metadata.categories || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse post file: ${filePath}`, error);
|
||||
return null;
|
||||
}
|
||||
const data = await readPostFileShared(filePath);
|
||||
if (!data) return null;
|
||||
|
||||
return {
|
||||
...data,
|
||||
projectId: data.projectId || this.currentProjectId,
|
||||
};
|
||||
}
|
||||
|
||||
async createPost(data: Partial<PostData>): Promise<PostData> {
|
||||
|
||||
@@ -68,8 +68,12 @@ export {
|
||||
type AnalyzedTag,
|
||||
type PostAnalysisStatus,
|
||||
type MediaAnalysisStatus,
|
||||
type ImportConflictResolution,
|
||||
} from './ImportAnalysisEngine';
|
||||
export {
|
||||
ImportDefinitionEngine,
|
||||
type ImportDefinitionData,
|
||||
} from './ImportDefinitionEngine';
|
||||
} from './ImportDefinitionEngine';export {
|
||||
readPostFile,
|
||||
type PostFileData,
|
||||
} from './postFileUtils';
|
||||
78
src/main/engine/postFileUtils.ts
Normal file
78
src/main/engine/postFileUtils.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
/**
|
||||
* Shared utilities for reading and parsing post markdown files.
|
||||
* Used by PostEngine for editing.
|
||||
*/
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
import matter from 'gray-matter';
|
||||
|
||||
export interface PostFileData {
|
||||
id: string;
|
||||
projectId?: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
excerpt?: string;
|
||||
content: string;
|
||||
status: 'draft' | 'published' | 'archived';
|
||||
author?: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
publishedAt?: Date;
|
||||
tags: string[];
|
||||
categories: string[];
|
||||
}
|
||||
|
||||
interface PostFileMetadata {
|
||||
id: string;
|
||||
projectId?: string;
|
||||
title: string;
|
||||
slug: string;
|
||||
excerpt?: string;
|
||||
status: 'draft' | 'published' | 'archived';
|
||||
author?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
publishedAt?: string;
|
||||
tags?: string[];
|
||||
categories?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Read and parse a post markdown file with YAML frontmatter.
|
||||
* @param filePath Absolute path to the .md file
|
||||
* @returns Parsed post data or null if file doesn't exist or can't be parsed
|
||||
*/
|
||||
export async function readPostFile(filePath: string): Promise<PostFileData | null> {
|
||||
try {
|
||||
// Check if file exists first to avoid noisy errors
|
||||
try {
|
||||
await fs.access(filePath);
|
||||
} catch {
|
||||
// File doesn't exist
|
||||
return null;
|
||||
}
|
||||
|
||||
const fileContent = await fs.readFile(filePath, 'utf-8');
|
||||
const { data, content: body } = matter(fileContent);
|
||||
const metadata = data as PostFileMetadata;
|
||||
|
||||
return {
|
||||
id: metadata.id,
|
||||
projectId: metadata.projectId,
|
||||
title: metadata.title,
|
||||
slug: metadata.slug,
|
||||
excerpt: metadata.excerpt,
|
||||
content: body,
|
||||
status: metadata.status,
|
||||
author: metadata.author,
|
||||
createdAt: new Date(metadata.createdAt),
|
||||
updatedAt: new Date(metadata.updatedAt),
|
||||
publishedAt: metadata.publishedAt ? new Date(metadata.publishedAt) : undefined,
|
||||
tags: metadata.tags || [],
|
||||
categories: metadata.categories || [],
|
||||
};
|
||||
} catch (error) {
|
||||
console.error(`Failed to parse post file: ${filePath}`, error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user