UNPKG

@bernierllc/content-type-blog-post

Version:

Blog post content type with rich TipTap editor, SEO metadata, database storage, and web publishing

288 lines (245 loc) 8.44 kB
/* Copyright (c) 2025 Bernier LLC This file is licensed to the client under a limited-use license. The client may use and modify this code *only within the scope of the project it was delivered for*. Redistribution or use in other products or commercial offerings is not permitted without written consent from Bernier LLC. */ import { BlogPostMetadata } from '../src/types'; /** * Mock database client for testing */ export class MockDatabaseClient { private data: Map<string, any> = new Map(); private idCounter = 1; async query(sql: string, params?: any[]): Promise<any> { const sqlLower = sql.toLowerCase(); // Handle CREATE TABLE if (sqlLower.includes('create table')) { return { rows: [] }; } // Handle CREATE INDEX if (sqlLower.includes('create index')) { return { rows: [] }; } // Handle INSERT if (sqlLower.includes('insert into')) { return this.handleInsert(sql, params); } // Handle UPDATE if (sqlLower.includes('update') && sqlLower.includes('set')) { return this.handleUpdate(sql, params); } // Handle DELETE if (sqlLower.includes('delete from')) { return this.handleDelete(sql, params); } // Handle SELECT if (sqlLower.includes('select')) { return this.handleSelect(sql, params); } return { rows: [] }; } private handleInsert(sql: string, params?: any[]): any { const id = `test-id-${this.idCounter++}`; // Parse column names from SQL: INSERT INTO table (col1, col2, ...) VALUES const columnMatch = sql.match(/\(([^)]+)\)\s+VALUES/i); const columns = columnMatch ? columnMatch[1].split(',').map((c) => c.trim()) : []; // Create record with defaults const record: any = { id, title: undefined, slug: undefined, content: undefined, excerpt: undefined, meta_title: undefined, meta_description: undefined, keywords: undefined, og_image: undefined, canonical_url: undefined, author_name: undefined, author_email: undefined, author_avatar: undefined, author_bio: undefined, tags: undefined, categories: undefined, status: undefined, published_at: undefined, scheduled_for: undefined, featured_image: undefined, reading_time: undefined, word_count: undefined, created_at: new Date().toISOString(), updated_at: new Date().toISOString(), }; // Map params to columns if (params) { columns.forEach((col, index) => { if (index < params.length) { record[col] = params[index]; } }); } this.data.set(id, record); return { rows: [{ id }] }; } private handleUpdate(sql: string, params?: any[]): any { if (!params || params.length === 0) { return { rows: [] }; } // ID is always the last parameter const id = params[params.length - 1]; const record = this.data.get(id); if (!record) { return { rows: [] }; } // Parse column names from SET clause: SET col1 = $1, col2 = $2, ... // Use 's' flag to match across newlines in multiline SQL const setMatch = sql.match(/SET\s+(.+?)\s+WHERE/is); if (!setMatch) { return { rows: [] }; } const setPairs = setMatch[1].split(',').map((pair) => pair.trim()); // Update the record const updated = { ...record }; let paramIndex = 0; setPairs.forEach((pair) => { const [col, value] = pair.split('=').map((s) => s.trim()); // Check if value is a parameter placeholder ($1, $2, etc.) if (value.startsWith('$')) { // Parameterized value - use params array if (paramIndex < params.length - 1 && params[paramIndex] !== undefined) { updated[col] = params[paramIndex]; } paramIndex++; } else if (value === 'NOW()') { // NOW() function - use current timestamp updated[col] = new Date().toISOString(); } else { // Literal value - remove quotes and use directly const literalValue = value.replace(/^'|'$/g, ''); updated[col] = literalValue; } }); this.data.set(id, updated); return { rows: [] }; } private handleDelete(_sql: string, params?: any[]): any { if (!params || params.length === 0) { return { rows: [] }; } const id = params[0]; this.data.delete(id); return { rows: [] }; } private handleSelect(sql: string, params?: any[]): any { const sqlLower = sql.toLowerCase(); // Handle SELECT by ID if (sqlLower.includes('where id =')) { const id = params?.[0]; const record = this.data.get(id); return { rows: record ? [record] : [] }; } // Handle SELECT by slug if (sqlLower.includes('where slug =')) { const slug = params?.[0]; const record = Array.from(this.data.values()).find((r) => r.slug === slug); return { rows: record ? [record] : [] }; } // Handle SELECT slug LIKE (for uniqueness check) if (sqlLower.includes('where slug like')) { const slugPattern = params?.[0]?.replace('%', ''); const records = Array.from(this.data.values()).filter((r) => r.slug?.startsWith(slugPattern) ); return { rows: records }; } // Handle SELECT with filters (WHERE 1=1 AND ...) if (sqlLower.includes('where 1=1')) { let records = Array.from(this.data.values()); let paramIndex = 0; // Filter by status if (sqlLower.includes('status =') && params && params.length > paramIndex) { const status = params[paramIndex]; records = records.filter((r) => r.status === status); paramIndex++; } // Filter by tags (array overlap) if (sqlLower.includes('tags &&') && params && params.length > paramIndex) { const searchTags = params[paramIndex]; records = records.filter((r) => { const recordTags = r.tags || []; return searchTags.some((tag: string) => recordTags.includes(tag)); }); paramIndex++; } // Filter by categories (array overlap) if (sqlLower.includes('categories &&') && params && params.length > paramIndex) { const searchCategories = params[paramIndex]; records = records.filter((r) => { const recordCategories = r.categories || []; return searchCategories.some((cat: string) => recordCategories.includes(cat)); }); paramIndex++; } // Sort by created_at DESC if (sqlLower.includes('order by created_at desc')) { records.sort((a, b) => { return new Date(b.created_at).getTime() - new Date(a.created_at).getTime(); }); } // Handle OFFSET if (sqlLower.includes('offset') && params) { const offsetParamIndex = sqlLower.includes('limit') ? paramIndex + 1 : paramIndex; if (params.length > offsetParamIndex) { const offset = params[offsetParamIndex]; records = records.slice(offset); } } // Handle LIMIT (after OFFSET) if (sqlLower.includes('limit') && params && params.length > paramIndex) { const limit = params[paramIndex]; records = records.slice(0, limit); } return { rows: records }; } return { rows: [] }; } clear(): void { this.data.clear(); this.idCounter = 1; } } /** * Create sample blog post metadata for testing */ export function createSampleBlogPost(overrides?: Partial<BlogPostMetadata>): BlogPostMetadata { return { content: '<h1>Test Blog Post</h1><p>This is a test post with <strong>rich text</strong>.</p>', title: 'Test Blog Post', slug: 'test-blog-post', excerpt: 'This is a test post', seo: { metaTitle: 'Test Blog Post - Example Site', metaDescription: 'This is a comprehensive test blog post demonstrating rich text editing and SEO metadata.', keywords: ['test', 'blog', 'example'], ogImage: 'https://example.com/og-image.jpg', ogType: 'article', }, author: { name: 'Test Author', email: 'author@example.com', avatar: 'https://example.com/avatar.jpg', bio: 'Test author bio', }, tags: ['technology', 'tutorial'], categories: ['guides'], status: 'draft', createdAt: new Date(), updatedAt: new Date(), wordCount: 0, ...overrides, }; }