diff --git a/package.json b/package.json index 1fd85a812..d7053c3c7 100644 --- a/package.json +++ b/package.json @@ -17,11 +17,11 @@ "db:test:init": "NODE_ENV=test bun run scripts/test-db-init.ts", "db:test:seed": "NODE_ENV=test bun run scripts/test-db-seed.ts", "test:integration": "rm -f test-*.db* && NODE_ENV=test bun run db:test:init && bun run db:test:seed && playwright test", - "test": "bun test src/", - "test:watch": "bun test src/ --watch", - "test:coverage": "bun test src/ --coverage", - "test:coverage:lcov": "bun test src/ --coverage --coverage-reporter=lcov", - "test:ci": "bun test src/ --coverage --coverage-reporter=lcov" + "test": "bun test src/ --preload ./src/test-setup.ts", + "test:watch": "bun test src/ --watch --preload ./src/test-setup.ts", + "test:coverage": "bun test src/ --coverage --preload ./src/test-setup.ts", + "test:coverage:lcov": "bun test src/ --coverage --coverage-reporter=lcov --preload ./src/test-setup.ts", + "test:ci": "bun test src/ --coverage --coverage-reporter=lcov --preload ./src/test-setup.ts" }, "devDependencies": { "@ai-sdk/anthropic": "^2.0.38", diff --git a/src/app.d.ts b/src/app.d.ts index 91a7137bd..5118a3231 100644 --- a/src/app.d.ts +++ b/src/app.d.ts @@ -42,4 +42,17 @@ declare global { } } +// Declare environment variables +declare module '$env/static/private' { + export const DB_PATH: string + export const GITHUB_CLIENT_ID: string + export const GITHUB_CLIENT_SECRET: string + export const GITHUB_AUTHORIZATION_CALLBACK_URL: string + export const ANTHROPIC_API_KEY: string | undefined + export const YOUTUBE_API_KEY: string | undefined + export const GITHUB_TOKEN: string | undefined + export const BULK_IMPORT_API_KEY: string | undefined + export const SEED_DATABASE: string | undefined +} + export {} diff --git a/src/lib/admin/types.ts b/src/lib/admin/types.ts index c6764ede3..6032ba48e 100644 --- a/src/lib/admin/types.ts +++ b/src/lib/admin/types.ts @@ -96,8 +96,7 @@ export const CONTENT_TYPE_ICONS: Record = { video: 'video', library: 'package', announcement: 'megaphone', - collection: 'folder', - event: 'calendar' + collection: 'folder' } as const // Common action types diff --git a/src/lib/admin/utils.ts b/src/lib/admin/utils.ts index dabd8e7fc..869485f0e 100644 --- a/src/lib/admin/utils.ts +++ b/src/lib/admin/utils.ts @@ -1,58 +1,3 @@ -import { fail, redirect } from '@sveltejs/kit' -import { superValidate, message } from 'sveltekit-superforms' -import { zod4 } from 'sveltekit-superforms/adapters' -import type { z } from 'zod/v4' - -/** - * Common pattern for handling form submissions in admin pages - */ -export async function handleFormAction({ - request, - schema, - onSuccess, - successMessage, - redirectTo, - errorMessage = 'Operation failed. Please try again.' -}: { - request: Request - schema: T - onSuccess: (data: z.infer) => Promise | void - successMessage?: string - redirectTo?: string - errorMessage?: string -}) { - const form = await superValidate(request, zod4(schema)) - - if (!form.valid) { - return fail(400, { form }) - } - - try { - await onSuccess(form.data) - - if (successMessage) { - message(form, { type: 'success', text: successMessage }) - } - } catch (error) { - console.error('Form action error:', error) - - const errorText = error instanceof Error ? error.message : errorMessage - - message(form, { type: 'error', text: errorText }) - if (successMessage) { - return fail(500, { form }) - } else { - return fail(500, { form, error: errorText }) - } - } - - if (redirectTo) { - redirect(303, redirectTo) - } - - return { form } -} - /** * Generate a URL-safe slug from a string */ diff --git a/src/lib/schema/content.ts b/src/lib/schema/content.ts index 84873f1bb..07ec8bf33 100644 --- a/src/lib/schema/content.ts +++ b/src/lib/schema/content.ts @@ -1,8 +1,15 @@ import { z } from 'zod/v4' +import { tagSchema } from './tags' export const typeSchema = z.enum(['video', 'library', 'announcement', 'collection', 'recipe']) export const statusSchema = z.enum(['draft', 'published', 'archived']) +// For form submissions, we accept either tag IDs (strings) or full tag objects +const tagInputSchema = z.union([ + z.string(), // Tag ID + tagSchema // Full tag object +]) + const baseContentSchema = z.object({ id: z.string(), title: z.string().min(1, 'Title is required'), @@ -10,7 +17,7 @@ const baseContentSchema = z.object({ description: z.string().min(1, 'Description is required'), status: statusSchema, type: typeSchema, - tags: z.array(z.string()).min(1, 'At least one tag is required'), + tags: z.array(tagInputSchema).min(1, 'At least one tag is required'), author_id: z.string().optional(), created_at: z.string(), updated_at: z.string(), diff --git a/src/lib/server/db/utils.ts b/src/lib/server/db/utils.ts index 5e5b9e965..50f5f57b7 100644 --- a/src/lib/server/db/utils.ts +++ b/src/lib/server/db/utils.ts @@ -1,10 +1,11 @@ -import { db } from './initiate' +import type { Database } from 'bun:sqlite' /** * Checks if the database has any data in the content table. + * @param db - The database instance to check * @returns {boolean} True if data exists, false otherwise */ -export function hasData(): boolean { +export function hasData(db: Database): boolean { try { const result = db.prepare('SELECT COUNT(*) as count FROM content').get() as { count: number } return result.count > 0 diff --git a/src/lib/server/services/AnnouncementService.ts b/src/lib/server/services/AnnouncementService.ts index 7dfe451dd..c7db80be6 100644 --- a/src/lib/server/services/AnnouncementService.ts +++ b/src/lib/server/services/AnnouncementService.ts @@ -1,4 +1,4 @@ -import type { Database } from 'better-sqlite3' +import type { Database } from 'bun:sqlite' export interface PlacementLocation { id: string diff --git a/src/lib/server/services/content.test.ts b/src/lib/server/services/content.test.ts index 277d7c71b..8c4413a0a 100644 --- a/src/lib/server/services/content.test.ts +++ b/src/lib/server/services/content.test.ts @@ -107,18 +107,18 @@ describe('ContentService', () => { test('should sort by latest by default', () => { const content = contentService.getFilteredContent() for (let i = 1; i < content.length; i++) { - expect(new Date(content[i].published_at) <= new Date(content[i - 1].published_at)).toBe( - true - ) + const currentDate = content[i].published_at ? new Date(content[i].published_at) : new Date(0) + const previousDate = content[i - 1].published_at ? new Date(content[i - 1].published_at) : new Date(0) + expect(currentDate <= previousDate).toBe(true) } }) test('should sort by oldest when specified', () => { const content = contentService.getFilteredContent({ sort: 'oldest' }) for (let i = 1; i < content.length; i++) { - expect(new Date(content[i].published_at) >= new Date(content[i - 1].published_at)).toBe( - true - ) + const currentDate = content[i].published_at ? new Date(content[i].published_at) : new Date(0) + const previousDate = content[i - 1].published_at ? new Date(content[i - 1].published_at) : new Date(0) + expect(currentDate >= previousDate).toBe(true) } }) }) @@ -202,8 +202,10 @@ describe('ContentService', () => { slug: 'new-test-content', type: 'recipe' as const, body: 'Test body', + rendered_body: '

Test body

', description: 'Test description', status: 'draft' as const, + published_at: null, tags: ['tag1'] } @@ -232,8 +234,10 @@ describe('ContentService', () => { slug: 'content-with-author', type: 'recipe' as const, body: 'Test body', + rendered_body: '

Test body

', description: 'Test description', status: 'published' as const, + published_at: new Date().toISOString(), tags: [] } @@ -253,9 +257,10 @@ describe('ContentService', () => { title: 'Multi-tag Content', slug: 'multi-tag-content', type: 'library' as const, - body: 'Test body', description: 'Test description', status: 'published' as const, + published_at: new Date().toISOString(), + metadata: {}, tags: ['tag1', 'tag2'] } @@ -275,8 +280,10 @@ describe('ContentService', () => { slug: 'searchable-content', type: 'recipe' as const, body: 'Content with searchable tags', + rendered_body: '

Content with searchable tags

', description: 'Description', status: 'published' as const, + published_at: new Date().toISOString(), tags: ['tag1', 'tag2'] } @@ -302,7 +309,10 @@ describe('ContentService', () => { type: existing.type, status: existing.status, body: existing.body, - description: 'Updated description' + rendered_body: existing.rendered_body || '', + published_at: existing.published_at, + description: 'Updated description', + tags: [] } contentService.updateContent(updates) @@ -322,8 +332,11 @@ describe('ContentService', () => { slug: existing.slug, type: existing.type, body: existing.body, + rendered_body: existing.rendered_body || '', + published_at: existing.published_at, description: existing.description, - status: 'published' as const + status: 'published' as const, + tags: [] } contentService.updateContent(updates) @@ -343,6 +356,8 @@ describe('ContentService', () => { type: existing.type, status: existing.status, body: existing.body, + rendered_body: existing.rendered_body || '', + published_at: existing.published_at, description: existing.description, tags: ['tag2', 'tag3'] } @@ -365,8 +380,11 @@ describe('ContentService', () => { slug: existing.slug, type: existing.type, body: existing.body, + rendered_body: existing.rendered_body || '', + published_at: existing.published_at, description: existing.description, - status: 'published' as const + status: 'published' as const, + tags: [] } contentService.updateContent(updates) diff --git a/src/lib/server/services/content.ts b/src/lib/server/services/content.ts index b484a7958..c71bb78ff 100644 --- a/src/lib/server/services/content.ts +++ b/src/lib/server/services/content.ts @@ -16,6 +16,29 @@ export class ContentService { private searchService?: SearchService ) {} + /** + * Normalize tags to IDs - accepts either tag IDs (strings) or full Tag objects + */ + private normalizeTagsToIds(tags: (string | Tag)[]): string[] { + return tags.map(tag => typeof tag === 'string' ? tag : tag.id) + } + + /** + * Normalize tags to slugs for search - accepts either tag IDs (strings) or full Tag objects + */ + private normalizeTagsToSlugs(tags: (string | Tag)[]): string[] { + return tags.map(tag => { + if (typeof tag === 'string') { + // It's a tag ID, need to look up the slug + const result = this.db.prepare('SELECT slug FROM tags WHERE id = ?').get(tag) as { slug: string } | null + return result?.slug || '' + } else { + // It's a full tag object + return tag.slug + } + }).filter(Boolean) + } + getContentById(id: string): ContentWithAuthor | null { if (!id) { console.error('Invalid content ID:', id) @@ -117,15 +140,14 @@ export class ContentService { // Assign tags to each child content childContent.tags = childTags || [] - childContent.children = [] // Ensure all children have empty children arrays // Add to the children collection childrenContent.push(childContent) } } - // Set the children on the parent content - content.children = childrenContent + // Set the children on the parent content (expanded from IDs to full objects) + content.children = childrenContent as any } else { // Empty children array content.children = [] @@ -163,7 +185,7 @@ export class ContentService { const searchQuery = filters.search.trim() // Pass status to search service - if status is 'all' or undefined, don't filter by status in search const searchStatus = filters.status && filters.status !== 'all' ? filters.status : undefined - const searchResults = this.searchService.search({ + const searchResults = this.searchService!.search({ query: searchQuery, status: searchStatus }) @@ -259,7 +281,7 @@ export class ContentService { const searchQuery = filters.search.trim() // Pass status to search service - if status is 'all' or undefined, don't filter by status in search const searchStatus = filters.status && filters.status !== 'all' ? filters.status : undefined - const searchResults = this.searchService.search({ + const searchResults = this.searchService!.search({ query: searchQuery, status: searchStatus }) @@ -369,15 +391,16 @@ export class ContentService { } if (data.tags && data.tags.length > 0) { + const tagIds = this.normalizeTagsToIds(data.tags) const insertTagStmt = this.db.prepare( `INSERT INTO content_to_tags (content_id, tag_id) VALUES (?, ?)` ) - for (const tag of data.tags) { + for (const tagId of tagIds) { try { - insertTagStmt.run(id, tag) + insertTagStmt.run(id, tagId) } catch (error) { - console.error('Failed to add tag relationship:', { contentId: id, tagId: tag, error }) + console.error('Failed to add tag relationship:', { contentId: id, tagId, error }) throw error } } @@ -386,18 +409,7 @@ export class ContentService { // Add to search index regardless of status if (this.searchService) { // Get tag slugs for search index - let tagSlugs: string[] = [] - if (data.tags && data.tags.length > 0) { - const tagQuery = this.db.prepare(` - SELECT slug FROM tags WHERE id = ? - `) - tagSlugs = data.tags - .map((tagId) => { - const tag = tagQuery.get(tagId) as { slug: string } | null - return tag?.slug || '' - }) - .filter(Boolean) - } + const tagSlugs = data.tags ? this.normalizeTagsToSlugs(data.tags) : [] this.searchService.add({ id, @@ -406,7 +418,7 @@ export class ContentService { tags: tagSlugs, type: data.type, status: data.status, - created_at: data.created_at || new Date().toISOString(), + created_at: new Date().toISOString(), published_at: data.status === 'published' ? new Date().toISOString() : '', likes: 0, saves: 0, @@ -461,12 +473,13 @@ export class ContentService { this.db.prepare('DELETE FROM content_to_tags WHERE content_id = ?').run(data.id) if (data.tags && data.tags.length > 0) { + const tagIds = this.normalizeTagsToIds(data.tags) const insertTagStmt = this.db.prepare( `INSERT INTO content_to_tags (content_id, tag_id) VALUES (?, ?)` ) - for (const tag of data.tags) { - insertTagStmt.run(data.id, tag) + for (const tagId of tagIds) { + insertTagStmt.run(data.id, tagId) } } @@ -485,8 +498,8 @@ export class ContentService { if (this.searchService) { const updatedContent = this.getContentById(data.id) if (updatedContent) { - // Get tag slugs for search index - const tagSlugs = updatedContent.tags?.map((tag) => tag.slug) || [] + // Extract slugs from tags for search index + const tagSlugs = updatedContent.tags ? this.normalizeTagsToSlugs(updatedContent.tags) : [] this.searchService.update(data.id, { id: updatedContent.id, @@ -505,7 +518,7 @@ export class ContentService { } } - getContentBySlug(slug: string, any_status?: boolean): Content | null { + getContentBySlug(slug: string, any_status?: boolean): ContentWithAuthor | null { if (!slug) { console.error('Invalid slug:', slug) return null diff --git a/src/lib/server/services/events.test.ts b/src/lib/server/services/events.test.ts index 49e5a7bf7..16c7b6663 100644 --- a/src/lib/server/services/events.test.ts +++ b/src/lib/server/services/events.test.ts @@ -44,14 +44,14 @@ describe('EventsService', () => { } }) }) - ) + ) as any const events = await eventsService.fetchUpcomingEventsFromAPI() expect(events).toBeDefined() expect(Array.isArray(events)).toBe(true) if (events.length > 0) { expect(events[0].slug).toBe('test-event') - expect(events[0].title).toBe('Test Event') + expect(events[0].name).toBe('Test Event') } }) @@ -62,7 +62,7 @@ describe('EventsService', () => { ok: false, statusText: 'Not Found' }) - ) + ) as any const events = await eventsService.fetchUpcomingEventsFromAPI() expect(events).toBeDefined() @@ -71,7 +71,7 @@ describe('EventsService', () => { test('should handle network errors', async () => { // Mock network error - global.fetch = mock(() => Promise.reject(new Error('Network error'))) + global.fetch = mock(() => Promise.reject(new Error('Network error'))) as any const events = await eventsService.fetchUpcomingEventsFromAPI() expect(events).toBeDefined() @@ -100,7 +100,7 @@ describe('EventsService', () => { } }) }) - }) + }) as any // First call should fetch const events1 = await eventsService.fetchUpcomingEventsFromAPI() @@ -137,7 +137,7 @@ describe('EventsService', () => { } }) }) - ) + ) as any const events = await eventsService.fetchPastEventsFromAPI() expect(events).toBeDefined() @@ -153,7 +153,7 @@ describe('EventsService', () => { ok: true, json: () => Promise.resolve({ events: { edges: [] } }) }) - ) + ) as any const events = await eventsService.fetchPastEventsFromAPI() expect(events).toBeDefined() @@ -176,12 +176,12 @@ describe('EventsService', () => { url: 'https://guild.host/events/single-event' }) }) - ) + ) as any const event = await eventsService.fetchEventFromAPI('single-event') expect(event).toBeDefined() expect(event?.slug).toBe('single-event') - expect(event?.title).toBe('Single Event') + expect(event?.name).toBe('Single Event') }) test('should return null for 404 response', async () => { @@ -191,14 +191,14 @@ describe('EventsService', () => { status: 404, statusText: 'Not Found' }) - ) + ) as any const event = await eventsService.fetchEventFromAPI('non-existent') expect(event).toBeNull() }) test('should return null for other errors', async () => { - global.fetch = mock(() => Promise.reject(new Error('API error'))) + global.fetch = mock(() => Promise.reject(new Error('API error'))) as any const event = await eventsService.fetchEventFromAPI('error-event') expect(event).toBeNull() @@ -227,7 +227,7 @@ describe('EventsService', () => { } }) }) - ) + ) as any const events = await noCacheService.fetchUpcomingEventsFromAPI() expect(events).toBeDefined() diff --git a/src/lib/server/services/external-content.ts b/src/lib/server/services/external-content.ts index 73bbec66c..8c5f33ad9 100644 --- a/src/lib/server/services/external-content.ts +++ b/src/lib/server/services/external-content.ts @@ -71,35 +71,95 @@ export class ExternalContentService { } if (existing) { - this.contentService.updateContent({ - id: existing.id, - title: data.title, - slug: existing.slug, - description: data.description || existing.description, - type: data.type, - status: existing.status, - body: data.body || existing.body || '', - metadata: JSON.stringify(metadata), - tags: data.tags || [] - }) + // Update based on content type + if (data.type === 'recipe') { + const existingBody = existing.type === 'recipe' ? existing.body || '' : '' + const existingRenderedBody = existing.type === 'recipe' ? existing.rendered_body || '' : '' + + this.contentService.updateContent({ + id: existing.id, + type: 'recipe', + title: data.title, + slug: existing.slug, + description: data.description || existing.description, + status: existing.status, + published_at: existing.published_at, + body: data.body || existingBody, + rendered_body: existingRenderedBody, + metadata: metadata, + tags: data.tags || [] + }) + } else if (data.type === 'video') { + this.contentService.updateContent({ + id: existing.id, + type: 'video', + title: data.title, + slug: existing.slug, + description: data.description || existing.description, + status: existing.status, + published_at: existing.published_at, + metadata: metadata, + tags: data.tags || [] + }) + } else if (data.type === 'library') { + this.contentService.updateContent({ + id: existing.id, + type: 'library', + title: data.title, + slug: existing.slug, + description: data.description || existing.description, + status: existing.status, + published_at: existing.published_at, + metadata: metadata, + tags: data.tags || [] + }) + } return existing.id } else { - const contentId = this.contentService.addContent({ - title: data.title, - type: data.type, - slug, - description: data.description || '', - body: data.body || '', - metadata, - status: 'draft', - tags: data.tags || [], - // Use the original published date for both created_at and published_at - // This ensures proper chronological ordering - created_at: data.publishedAt || new Date().toISOString(), - published_at: data.publishedAt || new Date().toISOString(), - author_id: data.author_id - }) + // Add new content based on type + let contentId: string + + if (data.type === 'recipe') { + contentId = this.contentService.addContent({ + type: 'recipe', + title: data.title, + slug, + description: data.description || '', + body: data.body || '', + rendered_body: '', + metadata, + status: 'draft', + tags: data.tags || [], + published_at: data.publishedAt || new Date().toISOString(), + author_id: data.author_id + }) + } else if (data.type === 'video') { + contentId = this.contentService.addContent({ + type: 'video', + title: data.title, + slug, + description: data.description || '', + metadata, + status: 'draft', + tags: data.tags || [], + published_at: data.publishedAt || new Date().toISOString(), + author_id: data.author_id + }) + } else { + // library + contentId = this.contentService.addContent({ + type: 'library', + title: data.title, + slug, + description: data.description || '', + metadata, + status: 'draft', + tags: data.tags || [], + published_at: data.publishedAt || new Date().toISOString(), + author_id: data.author_id + }) + } return contentId } @@ -152,12 +212,7 @@ export class ExternalContentService { * Generate a unique slug for external content */ private generateSlug(data: ExternalContentData): string { - // For events, use the external ID directly as it's usually already a slug - if (data.type === 'event' && data.source.externalId.match(/^[a-z0-9-]+$/)) { - return data.source.externalId - } - - // For other content, generate from title + // Generate from title const titleSlug = data.title .toLowerCase() .replace(/[^a-z0-9]+/g, '-') diff --git a/src/lib/server/services/llm.ts b/src/lib/server/services/llm.ts index 754ecc9d4..0b1416cb2 100644 --- a/src/lib/server/services/llm.ts +++ b/src/lib/server/services/llm.ts @@ -25,8 +25,7 @@ Write only the description, no additional text.` const { text } = await generateText({ model: this.model, prompt, - temperature: 0.7, - maxTokens: 100 + temperature: 0.7 }) return text.trim() @@ -72,8 +71,7 @@ Return only a comma-separated list of tag names, nothing else.` const { text } = await generateText({ model: this.model, prompt, - temperature: 0.3, // Lower temperature for more consistent tag selection - maxTokens: 100 + temperature: 0.3 // Lower temperature for more consistent tag selection }) // Parse the response and validate against available tags @@ -113,8 +111,7 @@ Return only the slug, nothing else.` const { text } = await generateText({ model: this.model, prompt, - temperature: 0.3, - maxTokens: 50 + temperature: 0.3 }) return text @@ -155,8 +152,7 @@ Return only the improved content in markdown format.` const { text } = await generateText({ model: this.model, prompt, - temperature: 0.6, - maxTokens: 2000 + temperature: 0.6 }) return text.trim() diff --git a/src/lib/server/services/metadata.test.ts b/src/lib/server/services/metadata.test.ts index fb5715a49..4780e2ab3 100644 --- a/src/lib/server/services/metadata.test.ts +++ b/src/lib/server/services/metadata.test.ts @@ -58,9 +58,19 @@ describe('MetadataService', () => { test('should return empty object for content with no metadata', () => { const content = { id: 'test-id', - type: 'library', + type: 'library' as const, title: 'Test', slug: 'test', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: null } @@ -72,9 +82,19 @@ describe('MetadataService', () => { test('should return parsed metadata from string', () => { const content = { id: 'test-id', - type: 'library', + type: 'library' as const, title: 'Test', slug: 'test', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: JSON.stringify({ github: { stars: 100 } }) } @@ -86,9 +106,19 @@ describe('MetadataService', () => { test('should return metadata object directly', () => { const content = { id: 'test-id', - type: 'library', + type: 'library' as const, title: 'Test', slug: 'test', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: { github: { stars: 200 } } } @@ -103,9 +133,19 @@ describe('MetadataService', () => { const content = { id: 'test-id', - type: 'library', + type: 'library' as const, title: 'Test', slug: 'test', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: { updated_at: twoDaysAgo, @@ -124,9 +164,19 @@ describe('MetadataService', () => { const content = { id: 'test-id', - type: 'library', + type: 'library' as const, title: 'Test', slug: 'test', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: { updated_at: oneHourAgo, @@ -160,9 +210,19 @@ describe('MetadataService', () => { const content = { id: 'test-id', - type: 'library', + type: 'library' as const, title: 'Test Library', slug: 'test-library', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: { github: { repoUrl: 'https://github.com/sveltejs/svelte' } @@ -192,9 +252,19 @@ describe('MetadataService', () => { const content = { id: 'test-id', - type: 'video', + type: 'video' as const, title: 'Test Video', slug: 'test-video', + description: 'Test description', + tags: [], + published_at: new Date().toISOString(), + created_at: new Date().toISOString(), + updated_at: new Date().toISOString(), + likes: 0, + saves: 0, + liked: false, + saved: false, + views: 0, status: 'published' as const, metadata: { videoId: 'dQw4w9WgXcQ' } } diff --git a/src/lib/server/services/moderation.test.ts b/src/lib/server/services/moderation.test.ts index 7b09d59d4..815a765eb 100644 --- a/src/lib/server/services/moderation.test.ts +++ b/src/lib/server/services/moderation.test.ts @@ -54,7 +54,7 @@ describe('ModerationService', () => { const testItems = [ { id: 'item1', - type: 'content', + type: 'content' as const, status: ModerationStatus.PENDING, data: JSON.stringify({ title: 'Pending Content 1', @@ -69,7 +69,7 @@ describe('ModerationService', () => { }, { id: 'item2', - type: 'content', + type: 'content' as const, status: ModerationStatus.APPROVED, data: JSON.stringify({ title: 'Approved Content', @@ -143,7 +143,7 @@ describe('ModerationService', () => { describe('addToModerationQueue', () => { test('should add new item to queue', () => { const newItem = { - type: 'content', + type: 'content' as const, data: JSON.stringify({ title: 'New Content', type: 'recipe', diff --git a/src/lib/types/content.ts b/src/lib/types/content.ts index 6520b8007..4831e37d9 100644 --- a/src/lib/types/content.ts +++ b/src/lib/types/content.ts @@ -14,10 +14,14 @@ export type UpdateContent = z.infer export type CreateContent = z.infer // Content with author information -export type ContentWithAuthor = Content & { +// Note: Schema has tags as string[] (tag IDs for forms), +// but runtime DB results have full Tag objects - type assertions used where needed +// The 'children' property is overridden from string[] to ContentWithAuthor[] for expanded collections +export type ContentWithAuthor = Omit & { author_id?: string author_username?: string author_name?: string + children?: string[] | ContentWithAuthor[] // Can be IDs or expanded objects } // Content filtering options diff --git a/src/lib/types/roles.ts b/src/lib/types/roles.ts deleted file mode 100644 index 0209ea3d9..000000000 --- a/src/lib/types/roles.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { roleSchema, createRoleSchema, updateRoleSchema } from '$lib/schema/roles' -import type { z } from 'zod' - -export type Role = z.infer -export type CreateRole = z.infer -export type UpdateRole = z.infer diff --git a/src/lib/ui/ContentCard.svelte b/src/lib/ui/ContentCard.svelte index d91b02781..e850cd3de 100644 --- a/src/lib/ui/ContentCard.svelte +++ b/src/lib/ui/ContentCard.svelte @@ -177,7 +177,7 @@ {#if content.type === 'recipe'} {:else if content.type === 'collection'} - + 0 && typeof content.children[0] !== 'string' ? content.children : []} /> {:else if content.type === 'video'}