215 lines
6.1 KiB
TypeScript
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 };
|
|
}
|