fix: rebuild from files now works
This commit is contained in:
@@ -137,7 +137,7 @@ export class DatabaseConnection {
|
|||||||
id TEXT PRIMARY KEY,
|
id TEXT PRIMARY KEY,
|
||||||
project_id TEXT NOT NULL DEFAULT 'default',
|
project_id TEXT NOT NULL DEFAULT 'default',
|
||||||
title TEXT NOT NULL,
|
title TEXT NOT NULL,
|
||||||
slug TEXT NOT NULL UNIQUE,
|
slug TEXT NOT NULL,
|
||||||
excerpt TEXT,
|
excerpt TEXT,
|
||||||
status TEXT NOT NULL DEFAULT 'draft',
|
status TEXT NOT NULL DEFAULT 'draft',
|
||||||
author TEXT,
|
author TEXT,
|
||||||
@@ -211,6 +211,7 @@ export class DatabaseConnection {
|
|||||||
CREATE INDEX IF NOT EXISTS idx_sync_log_status ON sync_log(status);
|
CREATE INDEX IF NOT EXISTS idx_sync_log_status ON sync_log(status);
|
||||||
CREATE INDEX IF NOT EXISTS idx_post_links_source ON post_links(source_post_id);
|
CREATE INDEX IF NOT EXISTS idx_post_links_source ON post_links(source_post_id);
|
||||||
CREATE INDEX IF NOT EXISTS idx_post_links_target ON post_links(target_post_id);
|
CREATE INDEX IF NOT EXISTS idx_post_links_target ON post_links(target_post_id);
|
||||||
|
CREATE UNIQUE INDEX IF NOT EXISTS posts_project_slug_idx ON posts(project_id, slug);
|
||||||
`);
|
`);
|
||||||
|
|
||||||
// Check if project_id column exists in posts table, add if missing (migration)
|
// Check if project_id column exists in posts table, add if missing (migration)
|
||||||
@@ -267,6 +268,83 @@ export class DatabaseConnection {
|
|||||||
await this.localClient.execute("ALTER TABLE posts ADD COLUMN content TEXT");
|
await this.localClient.execute("ALTER TABLE posts ADD COLUMN content TEXT");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Migration: Update slug unique constraint to be project-scoped
|
||||||
|
// SQLite doesn't allow dropping column-level UNIQUE constraints, so we must recreate the table
|
||||||
|
// Check if the posts table has a column-level UNIQUE on slug (from the table definition)
|
||||||
|
const tableInfo = await this.localClient.execute(
|
||||||
|
"SELECT sql FROM sqlite_master WHERE type='table' AND name='posts'"
|
||||||
|
);
|
||||||
|
const tableSql = tableInfo.rows[0]?.sql as string || '';
|
||||||
|
const hasColumnLevelUnique = tableSql.includes('slug TEXT NOT NULL UNIQUE') ||
|
||||||
|
tableSql.includes('slug TEXT UNIQUE') ||
|
||||||
|
/slug\s+TEXT[^,]*UNIQUE/i.test(tableSql);
|
||||||
|
|
||||||
|
if (hasColumnLevelUnique) {
|
||||||
|
console.log('Migrating posts table to remove column-level UNIQUE constraint on slug...');
|
||||||
|
|
||||||
|
// Create new table without the UNIQUE constraint
|
||||||
|
await this.localClient.execute(`
|
||||||
|
CREATE TABLE IF NOT EXISTS posts_new (
|
||||||
|
id TEXT PRIMARY KEY,
|
||||||
|
project_id TEXT NOT NULL DEFAULT 'default',
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
slug TEXT NOT NULL,
|
||||||
|
excerpt TEXT,
|
||||||
|
content TEXT,
|
||||||
|
status TEXT NOT NULL DEFAULT 'draft',
|
||||||
|
author TEXT,
|
||||||
|
created_at INTEGER NOT NULL,
|
||||||
|
updated_at INTEGER NOT NULL,
|
||||||
|
published_at INTEGER,
|
||||||
|
file_path TEXT NOT NULL DEFAULT '',
|
||||||
|
sync_status TEXT NOT NULL DEFAULT 'pending',
|
||||||
|
synced_at INTEGER,
|
||||||
|
checksum TEXT,
|
||||||
|
tags TEXT,
|
||||||
|
categories TEXT,
|
||||||
|
published_title TEXT,
|
||||||
|
published_content TEXT,
|
||||||
|
published_tags TEXT,
|
||||||
|
published_categories TEXT,
|
||||||
|
published_excerpt TEXT
|
||||||
|
)
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Copy data
|
||||||
|
await this.localClient.execute(`
|
||||||
|
INSERT INTO posts_new
|
||||||
|
SELECT id, project_id, title, slug, excerpt, content, status, author,
|
||||||
|
created_at, updated_at, published_at, file_path, sync_status,
|
||||||
|
synced_at, checksum, tags, categories, published_title,
|
||||||
|
published_content, published_tags, published_categories, published_excerpt
|
||||||
|
FROM posts
|
||||||
|
`);
|
||||||
|
|
||||||
|
// Drop old table and rename new one
|
||||||
|
await this.localClient.execute('DROP TABLE posts');
|
||||||
|
await this.localClient.execute('ALTER TABLE posts_new RENAME TO posts');
|
||||||
|
|
||||||
|
// Recreate indexes
|
||||||
|
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_slug ON posts(slug)');
|
||||||
|
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_status ON posts(status)');
|
||||||
|
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_sync_status ON posts(sync_status)');
|
||||||
|
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at)');
|
||||||
|
await this.localClient.execute('CREATE INDEX IF NOT EXISTS idx_posts_project_id ON posts(project_id)');
|
||||||
|
await this.localClient.execute('CREATE UNIQUE INDEX IF NOT EXISTS posts_project_slug_idx ON posts(project_id, slug)');
|
||||||
|
|
||||||
|
console.log('Posts table migration complete');
|
||||||
|
} else {
|
||||||
|
// Just ensure the composite unique index exists
|
||||||
|
const compositeSlugIndex = await this.localClient.execute(
|
||||||
|
"SELECT name FROM sqlite_master WHERE type='index' AND name='posts_project_slug_idx' AND tbl_name='posts'"
|
||||||
|
);
|
||||||
|
if (compositeSlugIndex.rows.length === 0) {
|
||||||
|
await this.localClient.execute(
|
||||||
|
"CREATE UNIQUE INDEX posts_project_slug_idx ON posts(project_id, slug)"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Create FTS5 virtual table for full-text search
|
// Create FTS5 virtual table for full-text search
|
||||||
await this.localClient.execute(`
|
await this.localClient.execute(`
|
||||||
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
CREATE VIRTUAL TABLE IF NOT EXISTS posts_fts USING fts5(
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';
|
import { sqliteTable, text, integer, uniqueIndex } from 'drizzle-orm/sqlite-core';
|
||||||
|
|
||||||
// Projects table - stores blog projects/websites
|
// Projects table - stores blog projects/websites
|
||||||
export const projects = sqliteTable('projects', {
|
export const projects = sqliteTable('projects', {
|
||||||
@@ -20,7 +20,7 @@ export const posts = sqliteTable('posts', {
|
|||||||
projectId: text('project_id').notNull(),
|
projectId: text('project_id').notNull(),
|
||||||
id: text('id').primaryKey(),
|
id: text('id').primaryKey(),
|
||||||
title: text('title').notNull(),
|
title: text('title').notNull(),
|
||||||
slug: text('slug').notNull().unique(),
|
slug: text('slug').notNull(),
|
||||||
excerpt: text('excerpt'),
|
excerpt: text('excerpt'),
|
||||||
content: text('content'), // Draft body text (null/empty when published — content is in the file)
|
content: text('content'), // Draft body text (null/empty when published — content is in the file)
|
||||||
status: text('status', { enum: ['draft', 'published', 'archived'] }).notNull().default('draft'),
|
status: text('status', { enum: ['draft', 'published', 'archived'] }).notNull().default('draft'),
|
||||||
@@ -40,7 +40,10 @@ export const posts = sqliteTable('posts', {
|
|||||||
publishedTags: text('published_tags'),
|
publishedTags: text('published_tags'),
|
||||||
publishedCategories: text('published_categories'),
|
publishedCategories: text('published_categories'),
|
||||||
publishedExcerpt: text('published_excerpt'),
|
publishedExcerpt: text('published_excerpt'),
|
||||||
});
|
}, (table) => ({
|
||||||
|
// Composite unique index: slug must be unique within each project
|
||||||
|
projectSlugIdx: uniqueIndex('posts_project_slug_idx').on(table.projectId, table.slug),
|
||||||
|
}));
|
||||||
|
|
||||||
// Media table - stores metadata for images and other media
|
// Media table - stores metadata for images and other media
|
||||||
export const media = sqliteTable('media', {
|
export const media = sqliteTable('media', {
|
||||||
|
|||||||
@@ -238,6 +238,14 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
private async readSidecarFile(sidecarPath: string): Promise<MediaMetadata | null> {
|
private async readSidecarFile(sidecarPath: string): Promise<MediaMetadata | null> {
|
||||||
try {
|
try {
|
||||||
|
// Check if file exists first to avoid noisy errors
|
||||||
|
try {
|
||||||
|
await fs.access(sidecarPath);
|
||||||
|
} catch {
|
||||||
|
// File doesn't exist - this is expected when DB has stale paths
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
const content = await fs.readFile(sidecarPath, 'utf-8');
|
const content = await fs.readFile(sidecarPath, 'utf-8');
|
||||||
const lines = content.split('\n');
|
const lines = content.split('\n');
|
||||||
|
|
||||||
@@ -309,7 +317,7 @@ export class MediaEngine extends EventEmitter {
|
|||||||
|
|
||||||
return metadata as MediaMetadata;
|
return metadata as MediaMetadata;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to read sidecar file: ${sidecarPath}`, error);
|
console.error(`Failed to parse sidecar file: ${sidecarPath}`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -535,7 +543,16 @@ export class MediaEngine extends EventEmitter {
|
|||||||
execute: async (onProgress) => {
|
execute: async (onProgress) => {
|
||||||
const db = getDatabase().getLocal();
|
const db = getDatabase().getLocal();
|
||||||
|
|
||||||
onProgress(0, 'Scanning media directory...');
|
onProgress(0, 'Deleting existing media for project...');
|
||||||
|
|
||||||
|
// Delete all media for the current project - clean slate rebuild
|
||||||
|
const existingMedia = await db.select({ id: media.id }).from(media).where(eq(media.projectId, this.currentProjectId)).all();
|
||||||
|
if (existingMedia.length > 0) {
|
||||||
|
await db.delete(media).where(eq(media.projectId, this.currentProjectId));
|
||||||
|
console.log(`Deleted ${existingMedia.length} existing media record(s) for project ${this.currentProjectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(5, 'Scanning media directory...');
|
||||||
|
|
||||||
// Recursively find all .meta files in the media directory tree
|
// Recursively find all .meta files in the media directory tree
|
||||||
const metaFiles: string[] = [];
|
const metaFiles: string[] = [];
|
||||||
@@ -580,44 +597,26 @@ export class MediaEngine extends EventEmitter {
|
|||||||
const checksum = this.calculateChecksum(buffer);
|
const checksum = this.calculateChecksum(buffer);
|
||||||
const filename = path.basename(mediaFilePath);
|
const filename = path.basename(mediaFilePath);
|
||||||
|
|
||||||
const existing = await db.select().from(media).where(eq(media.id, metadata.id)).get();
|
// Insert fresh - we deleted all records at the start
|
||||||
|
await db.insert(media).values({
|
||||||
if (existing) {
|
id: metadata.id,
|
||||||
await db.update(media)
|
projectId: this.currentProjectId,
|
||||||
.set({
|
filename,
|
||||||
originalName: metadata.originalName,
|
originalName: metadata.originalName,
|
||||||
mimeType: metadata.mimeType,
|
mimeType: metadata.mimeType,
|
||||||
size: stats.size,
|
size: stats.size,
|
||||||
width: metadata.width,
|
width: metadata.width,
|
||||||
height: metadata.height,
|
height: metadata.height,
|
||||||
alt: metadata.alt,
|
alt: metadata.alt,
|
||||||
caption: metadata.caption,
|
caption: metadata.caption,
|
||||||
updatedAt: new Date(metadata.updatedAt),
|
filePath: mediaFilePath,
|
||||||
checksum,
|
sidecarPath,
|
||||||
tags: JSON.stringify(metadata.tags),
|
createdAt: new Date(metadata.createdAt),
|
||||||
})
|
updatedAt: new Date(metadata.updatedAt),
|
||||||
.where(eq(media.id, metadata.id));
|
syncStatus: 'pending',
|
||||||
} else {
|
checksum,
|
||||||
await db.insert(media).values({
|
tags: JSON.stringify(metadata.tags),
|
||||||
id: metadata.id,
|
});
|
||||||
projectId: this.currentProjectId,
|
|
||||||
filename,
|
|
||||||
originalName: metadata.originalName,
|
|
||||||
mimeType: metadata.mimeType,
|
|
||||||
size: stats.size,
|
|
||||||
width: metadata.width,
|
|
||||||
height: metadata.height,
|
|
||||||
alt: metadata.alt,
|
|
||||||
caption: metadata.caption,
|
|
||||||
filePath: mediaFilePath,
|
|
||||||
sidecarPath,
|
|
||||||
createdAt: new Date(metadata.createdAt),
|
|
||||||
updatedAt: new Date(metadata.updatedAt),
|
|
||||||
syncStatus: 'pending',
|
|
||||||
checksum,
|
|
||||||
tags: JSON.stringify(metadata.tags),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Media file not found for sidecar: ${sidecarPath}`, error);
|
console.error(`Media file not found for sidecar: ${sidecarPath}`, error);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import * as fs from 'fs/promises';
|
|||||||
import * as path from 'path';
|
import * as path from 'path';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
import matter from 'gray-matter';
|
import matter from 'gray-matter';
|
||||||
import { eq, and, desc, gte, lte, like } from 'drizzle-orm';
|
import { eq, and, desc, gte, lte, like, inArray } from 'drizzle-orm';
|
||||||
import { app } from 'electron';
|
import { app } from 'electron';
|
||||||
import { getDatabase } from '../database';
|
import { getDatabase } from '../database';
|
||||||
import { posts, Post, NewPost, postLinks } from '../database/schema';
|
import { posts, Post, NewPost, postLinks } from '../database/schema';
|
||||||
@@ -193,6 +193,14 @@ export class PostEngine extends EventEmitter {
|
|||||||
|
|
||||||
private async readPostFile(filePath: string): Promise<PostData | null> {
|
private async readPostFile(filePath: string): Promise<PostData | null> {
|
||||||
try {
|
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 content = await fs.readFile(filePath, 'utf-8');
|
||||||
const { data, content: body } = matter(content);
|
const { data, content: body } = matter(content);
|
||||||
const metadata = data as PostMetadata;
|
const metadata = data as PostMetadata;
|
||||||
@@ -213,7 +221,7 @@ export class PostEngine extends EventEmitter {
|
|||||||
categories: metadata.categories || [],
|
categories: metadata.categories || [],
|
||||||
};
|
};
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to read post file: ${filePath}`, error);
|
console.error(`Failed to parse post file: ${filePath}`, error);
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -834,7 +842,27 @@ export class PostEngine extends EventEmitter {
|
|||||||
const db = getDatabase().getLocal();
|
const db = getDatabase().getLocal();
|
||||||
const client = getDatabase().getLocalClient();
|
const client = getDatabase().getLocalClient();
|
||||||
|
|
||||||
onProgress(0, 'Scanning posts directory...');
|
onProgress(0, 'Deleting existing posts for project...');
|
||||||
|
|
||||||
|
// Delete all posts for the current project - clean slate rebuild
|
||||||
|
const existingPosts = await db.select({ id: posts.id }).from(posts).where(eq(posts.projectId, this.currentProjectId)).all();
|
||||||
|
if (existingPosts.length > 0) {
|
||||||
|
const postIds = existingPosts.map(p => p.id);
|
||||||
|
// Delete FTS entries first
|
||||||
|
if (client) {
|
||||||
|
for (const post of existingPosts) {
|
||||||
|
await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [post.id] });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Delete post links where source or target is in the posts being deleted
|
||||||
|
await db.delete(postLinks).where(inArray(postLinks.sourcePostId, postIds));
|
||||||
|
await db.delete(postLinks).where(inArray(postLinks.targetPostId, postIds));
|
||||||
|
// Delete posts
|
||||||
|
await db.delete(posts).where(eq(posts.projectId, this.currentProjectId));
|
||||||
|
console.log(`Deleted ${existingPosts.length} existing post(s) for project ${this.currentProjectId}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
onProgress(5, 'Scanning posts directory...');
|
||||||
|
|
||||||
// Recursively find all .md files in the posts directory tree
|
// Recursively find all .md files in the posts directory tree
|
||||||
const mdFiles: string[] = [];
|
const mdFiles: string[] = [];
|
||||||
@@ -863,6 +891,9 @@ export class PostEngine extends EventEmitter {
|
|||||||
|
|
||||||
onProgress(10, `Found ${mdFiles.length} post files`);
|
onProgress(10, `Found ${mdFiles.length} post files`);
|
||||||
|
|
||||||
|
// Track slugs to detect duplicates
|
||||||
|
const insertedSlugs = new Map<string, string>(); // slug -> filePath
|
||||||
|
|
||||||
for (let i = 0; i < mdFiles.length; i++) {
|
for (let i = 0; i < mdFiles.length; i++) {
|
||||||
const filePath = mdFiles[i];
|
const filePath = mdFiles[i];
|
||||||
const fileName = path.basename(filePath);
|
const fileName = path.basename(filePath);
|
||||||
@@ -872,33 +903,22 @@ export class PostEngine extends EventEmitter {
|
|||||||
const postData = await this.readPostFile(filePath);
|
const postData = await this.readPostFile(filePath);
|
||||||
|
|
||||||
if (postData) {
|
if (postData) {
|
||||||
const existing = await db.select().from(posts).where(eq(posts.id, postData.id)).get();
|
try {
|
||||||
const checksum = this.calculateChecksum(postData.content);
|
const projectId = postData.projectId || this.currentProjectId;
|
||||||
|
const slugKey = `${projectId}:${postData.slug}`;
|
||||||
|
|
||||||
if (existing) {
|
// Check for duplicate slugs
|
||||||
// Only update if no active draft content in DB (don't overwrite edits)
|
if (insertedSlugs.has(slugKey)) {
|
||||||
if (!existing.content) {
|
console.error(`Duplicate slug "${postData.slug}" found. File "${filePath}" duplicates "${insertedSlugs.get(slugKey)}". Skipping.`);
|
||||||
await db.update(posts)
|
continue;
|
||||||
.set({
|
|
||||||
title: postData.title,
|
|
||||||
slug: postData.slug,
|
|
||||||
excerpt: postData.excerpt,
|
|
||||||
content: null, // Content lives in the file, not DB
|
|
||||||
status: 'published', // Files on disk = published
|
|
||||||
author: postData.author,
|
|
||||||
updatedAt: postData.updatedAt,
|
|
||||||
publishedAt: postData.publishedAt,
|
|
||||||
filePath,
|
|
||||||
checksum,
|
|
||||||
tags: JSON.stringify(postData.tags),
|
|
||||||
categories: JSON.stringify(postData.categories),
|
|
||||||
})
|
|
||||||
.where(eq(posts.id, postData.id));
|
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
|
const checksum = this.calculateChecksum(postData.content);
|
||||||
|
|
||||||
|
// Insert fresh - we deleted all records at the start
|
||||||
await db.insert(posts).values({
|
await db.insert(posts).values({
|
||||||
id: postData.id,
|
id: postData.id,
|
||||||
projectId: postData.projectId || this.currentProjectId,
|
projectId,
|
||||||
title: postData.title,
|
title: postData.title,
|
||||||
slug: postData.slug,
|
slug: postData.slug,
|
||||||
excerpt: postData.excerpt,
|
excerpt: postData.excerpt,
|
||||||
@@ -914,15 +934,24 @@ export class PostEngine extends EventEmitter {
|
|||||||
tags: JSON.stringify(postData.tags),
|
tags: JSON.stringify(postData.tags),
|
||||||
categories: JSON.stringify(postData.categories),
|
categories: JSON.stringify(postData.categories),
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
// Update FTS index (use file content for search)
|
insertedSlugs.set(slugKey, filePath);
|
||||||
if (client) {
|
|
||||||
await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [postData.id] });
|
// Update FTS index (use file content for search)
|
||||||
await client.execute({
|
if (client) {
|
||||||
sql: 'INSERT INTO posts_fts (id, title, content, excerpt, tags, categories) VALUES (?, ?, ?, ?, ?, ?)',
|
await client.execute({ sql: 'DELETE FROM posts_fts WHERE id = ?', args: [postData.id] });
|
||||||
args: [postData.id, postData.title, postData.content, postData.excerpt || '', postData.tags.join(' '), postData.categories.join(' ')],
|
await client.execute({
|
||||||
});
|
sql: 'INSERT INTO posts_fts (id, title, content, excerpt, tags, categories) VALUES (?, ?, ?, ?, ?, ?)',
|
||||||
|
args: [postData.id, postData.title, postData.content, postData.excerpt || '', postData.tags.join(' '), postData.categories.join(' ')],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
// Handle constraint violations and other errors gracefully
|
||||||
|
if (error?.code === 'SQLITE_CONSTRAINT_UNIQUE') {
|
||||||
|
console.error(`Failed to insert post "${postData.title}" from ${filePath}: Unique constraint violation (likely slug conflict)`);
|
||||||
|
} else {
|
||||||
|
console.error(`Failed to process post from ${filePath}:`, error);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1241,6 +1241,9 @@ const code = 'example';
|
|||||||
mockDirent('other.txt'),
|
mockDirent('other.txt'),
|
||||||
] as any);
|
] as any);
|
||||||
|
|
||||||
|
// Mock access to allow file reads
|
||||||
|
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
// Mock readFile to return valid post content
|
// Mock readFile to return valid post content
|
||||||
vi.mocked(fs.readFile).mockImplementation(async (filePath: any) => {
|
vi.mocked(fs.readFile).mockImplementation(async (filePath: any) => {
|
||||||
if (filePath.includes('post1.md')) {
|
if (filePath.includes('post1.md')) {
|
||||||
@@ -1313,11 +1316,14 @@ Content 2`;
|
|||||||
expect(fs.mkdir).toHaveBeenCalled();
|
expect(fs.mkdir).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should update existing posts in database', async () => {
|
it('should delete all existing posts before inserting fresh from files', async () => {
|
||||||
const fs = await import('fs/promises');
|
const fs = await import('fs/promises');
|
||||||
|
|
||||||
vi.mocked(fs.readdir).mockResolvedValueOnce([mockDirent('existing.md')] as any);
|
vi.mocked(fs.readdir).mockResolvedValueOnce([mockDirent('existing.md')] as any);
|
||||||
|
|
||||||
|
// Mock access to allow file reads
|
||||||
|
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
vi.mocked(fs.readFile).mockResolvedValueOnce(`---
|
vi.mocked(fs.readFile).mockResolvedValueOnce(`---
|
||||||
id: existing-id
|
id: existing-id
|
||||||
projectId: default
|
projectId: default
|
||||||
@@ -1331,22 +1337,22 @@ categories: []
|
|||||||
---
|
---
|
||||||
Updated content`);
|
Updated content`);
|
||||||
|
|
||||||
// Mock that post exists in database
|
// Mock that project has existing posts to delete
|
||||||
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
vi.mocked(mockLocalDb.select).mockImplementation(() => {
|
||||||
const chain = createSelectChain();
|
const chain = createSelectChain();
|
||||||
chain.where = vi.fn().mockReturnValue({
|
chain.where = vi.fn().mockReturnValue({
|
||||||
...chain,
|
...chain,
|
||||||
get: vi.fn().mockResolvedValue({
|
all: vi.fn().mockResolvedValue([{ id: 'existing-id' }]),
|
||||||
id: 'existing-id',
|
get: vi.fn().mockResolvedValue(null),
|
||||||
title: 'Old Title',
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
return chain;
|
return chain;
|
||||||
});
|
});
|
||||||
|
|
||||||
await postEngine.rebuildDatabaseFromFiles();
|
await postEngine.rebuildDatabaseFromFiles();
|
||||||
|
|
||||||
expect(mockLocalDb.update).toHaveBeenCalled();
|
// Should delete existing posts first, then insert fresh
|
||||||
|
expect(mockLocalDb.delete).toHaveBeenCalled();
|
||||||
|
expect(mockLocalDb.insert).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should insert new posts not in database', async () => {
|
it('should insert new posts not in database', async () => {
|
||||||
@@ -1354,6 +1360,9 @@ Updated content`);
|
|||||||
|
|
||||||
vi.mocked(fs.readdir).mockResolvedValueOnce([mockDirent('new-post.md')] as any);
|
vi.mocked(fs.readdir).mockResolvedValueOnce([mockDirent('new-post.md')] as any);
|
||||||
|
|
||||||
|
// Mock access to allow file reads
|
||||||
|
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
vi.mocked(fs.readFile).mockResolvedValueOnce(`---
|
vi.mocked(fs.readFile).mockResolvedValueOnce(`---
|
||||||
id: new-post-id
|
id: new-post-id
|
||||||
projectId: default
|
projectId: default
|
||||||
@@ -1387,6 +1396,9 @@ New content`);
|
|||||||
|
|
||||||
vi.mocked(fs.readdir).mockResolvedValueOnce([mockDirent('fts-test.md')] as any);
|
vi.mocked(fs.readdir).mockResolvedValueOnce([mockDirent('fts-test.md')] as any);
|
||||||
|
|
||||||
|
// Mock access to allow file reads
|
||||||
|
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||||
|
|
||||||
vi.mocked(fs.readFile).mockResolvedValueOnce(`---
|
vi.mocked(fs.readFile).mockResolvedValueOnce(`---
|
||||||
id: fts-test-id
|
id: fts-test-id
|
||||||
projectId: default
|
projectId: default
|
||||||
|
|||||||
Reference in New Issue
Block a user