Blog/src/db/posts.ts

215 lines
6.1 KiB
TypeScript

import { db } from './db';
import { getOrCreateTag } from './tags';
import { getPostTags } from './tags';
// Load the blog post into a SQLite database
// This allows us to index and make queries against the metadata
// to support functions like search, filtering, and sorting
// as well as return either the post or the full AppShell with the post content
export function addToDatabase(filePath: string, data: { [key: string]: any }, content: string) {
if (!data) return;
// Use a transaction to ensure data consistency
const transaction = db.transaction(() => {
try {
// Extract common fields
const { title, date, readingTime, tags, excerpt } = data;
// Convert all values to strings or null explicitly (except tags)
// Ensure date is stored in ISO format for consistent sorting
const values = [
filePath ? String(filePath) : null,
title ? String(title) : null,
date ? (date instanceof Date ? date.toISOString().split('T')[0] : String(date)) : null,
readingTime ? String(readingTime) : null,
excerpt ? String(excerpt) : null,
content ? String(content) : null
];
// Query to insert or replace metadata (without tags)
const insertPost = db.query(`
INSERT OR REPLACE INTO posts
(path, title, date, reading_time, summary, content)
VALUES (?, ?, ?, ?, ?, ?)
`);
insertPost.run(...values);
// Get the post ID
const getPostId = db.query('SELECT id FROM posts WHERE path = ?');
const postResult = getPostId.get(filePath) as { id: number } | undefined;
if (!postResult) {
throw new Error(`Failed to retrieve post ID for ${filePath}`);
}
// Delete existing tag associations for this post
const deleteExistingTags = db.query('DELETE FROM post_tags WHERE post_id = ?');
deleteExistingTags.run(postResult.id);
// If tags exist, process them
if (tags && Array.isArray(tags)) {
// Insert into junction table
const insertPostTag = db.query('INSERT OR IGNORE INTO post_tags (post_id, tag_id) VALUES (?, ?)');
for (const tag of tags) {
const tagId = getOrCreateTag(String(tag));
insertPostTag.run(postResult.id, tagId);
}
}
} catch (error) {
console.error(`Failed to store ${filePath}:`, error);
throw error; // Re-throw to make the transaction fail
}
});
// Execute the transaction
try {
transaction();
} catch (error) {
console.error(`Transaction failed for ${filePath}:`, error);
}
}
// Returns the total number of posts
export function getNumOfPosts() {
const queryCount = db.query('SELECT COUNT(*) AS count FROM posts');
const numPosts = queryCount.get() as { count: number };
return numPosts.count;
}
// Helper function to get post data with tags
export function getPostWithTags(postPath: string) {
const getPost = db.query(`
SELECT * FROM posts WHERE path = ?
`);
const post = getPost.get(postPath) as any;
if (!post) return null;
// Get tags for this post
if (post.id) {
post.tags = getPostTags(post.id);
}
return post;
}
// Get recent posts
export function getRecentPosts(limit: number = 10, offset: number = 0) {
const query = db.query(`
SELECT * FROM posts
WHERE path NOT LIKE '%.md'
ORDER BY date DESC
LIMIT ? OFFSET ?
`);
const posts = query.all(limit, offset) as any[];
// Add tags to each post and clean up paths
return posts.map(post => ({
...post,
tags: getPostTags(post.id),
path: post.path.replace(/^.*\/content\//, '/').replace(/\.md$/, '')
}));
}
// Get all posts
export function getAllPosts() {
const query = db.query(`
SELECT * FROM posts
ORDER BY date DESC
`);
const posts = query.all() as any[];
// Add tags to each post
return posts.map(post => ({
...post,
tags: getPostTags(post.id)
}));
}
// Helper function to get a post by its path
export function getPostByPath(path: string) {
const query = db.query(`
SELECT * FROM posts
WHERE path = ?
`);
const post = query.get(path) as any;
if (!post) return null;
// Get tags for this post
post.tags = getPostTags(post.id);
return post;
}
// Helper function to calculate read time
export function calculateReadTime(content: string): number {
const wordsPerMinute = 200;
const words = content.split(/\s+/).length;
return Math.max(1, Math.ceil(words / wordsPerMinute));
}
// Helper function to format date for display
export function formatDate(dateString: string): string {
// Parse ISO date string (YYYY-MM-DD)
const date = new Date(dateString + 'T00:00:00');
return date.toLocaleDateString('en-US', {
year: 'numeric',
month: 'long',
day: 'numeric'
});
}
// Get adjacent posts (previous and next) chronologically for a given post
export function getAdjacentPosts(currentPostPath: string) {
const allPostsQuery = db.query(`
SELECT path, title, date
FROM posts
WHERE path NOT LIKE '%.md'
ORDER BY date DESC
`);
const allPosts = allPostsQuery.all() as any[];
// Find the current post index
const currentIndex = allPosts.findIndex(post => post.path === currentPostPath);
// If not found, return empty navigation
if (currentIndex === -1) {
return { previousPost: null, nextPost: null };
}
// Get previous post (newer post, which comes before current in reverse chronological order)
let previousPost = null;
if (currentIndex > 0) {
const prevPost = allPosts[currentIndex - 1];
// Clean up the path to match the URL structure
const cleanPath = prevPost.path.replace(/^.*\/content\//, '/').replace(/\.md$/, '');
previousPost = {
title: prevPost.title,
path: cleanPath
};
}
// Get next post (older post, which comes after current in reverse chronological order)
let nextPost = null;
if (currentIndex < allPosts.length - 1) {
const post = allPosts[currentIndex + 1];
// Clean up the path to match the URL structure
const cleanPath = post.path.replace(/^.*\/content\//, '/').replace(/\.md$/, '');
nextPost = {
title: post.title,
path: cleanPath
};
}
return { previousPost, nextPost };
}