import fs from 'fs'; import path from 'path'; import { IStorage } from './storage'; import { User, Author, Work, Translation, Tag, Bookmark, Comment, Like, Collection, CollectionItem, TimelineEvent, BlogPost, ReadingProgress, InsertUser, InsertAuthor, InsertWork, InsertTranslation, InsertTag, InsertBookmark, InsertComment, InsertLike, InsertCollection, InsertCollectionItem, InsertTimelineEvent, InsertBlogPost, InsertReadingProgress } from '../shared/schema'; export class JsonStorage implements IStorage { private data: { users: Map; authors: Map; works: Map; translations: Map; tags: Map; workTagsRelations: Map>; blogPostTagsRelations: Map>; bookmarks: Map; comments: Map; likes: Map; collections: Map; collectionItems: Map; timelineEvents: Map; blogPosts: Map; readingProgresses: Map; }; private counters: { userId: number; authorId: number; workId: number; translationId: number; tagId: number; bookmarkId: number; commentId: number; likeId: number; collectionId: number; collectionItemId: number; timelineEventId: number; blogPostId: number; readingProgressId: number; }; private dataFilePath: string; constructor(dataFilePath: string = path.join(process.cwd(), 'data', 'sampleData.json')) { this.dataFilePath = dataFilePath; this.data = { users: new Map(), authors: new Map(), works: new Map(), translations: new Map(), tags: new Map(), workTagsRelations: new Map>(), blogPostTagsRelations: new Map>(), bookmarks: new Map(), comments: new Map(), likes: new Map(), collections: new Map(), collectionItems: new Map(), timelineEvents: new Map(), blogPosts: new Map(), readingProgresses: new Map(), }; this.counters = { userId: 1, authorId: 1, workId: 1, translationId: 1, tagId: 1, bookmarkId: 1, commentId: 1, likeId: 1, collectionId: 1, collectionItemId: 1, timelineEventId: 1, blogPostId: 1, readingProgressId: 1, }; this.loadDataFromFile(); } private loadDataFromFile() { try { console.log(`Loading data from ${this.dataFilePath}`); if (!fs.existsSync(this.dataFilePath)) { console.error(`Data file not found: ${this.dataFilePath}`); return; } const jsonData = JSON.parse(fs.readFileSync(this.dataFilePath, 'utf8')); // Load users if (jsonData.users) { jsonData.users.forEach((user: User) => { this.data.users.set(user.id, { ...user, createdAt: new Date(user.createdAt) }); this.counters.userId = Math.max(this.counters.userId, user.id + 1); }); } // Load authors if (jsonData.authors) { jsonData.authors.forEach((author: Author) => { this.data.authors.set(author.id, { ...author, createdAt: new Date(author.createdAt) }); this.counters.authorId = Math.max(this.counters.authorId, author.id + 1); }); } // Load works if (jsonData.works) { jsonData.works.forEach((work: Work) => { this.data.works.set(work.id, { ...work, createdAt: new Date(work.createdAt) }); this.counters.workId = Math.max(this.counters.workId, work.id + 1); }); } // Load translations if (jsonData.translations) { jsonData.translations.forEach((translation: Translation) => { this.data.translations.set(translation.id, { ...translation, createdAt: new Date(translation.createdAt) }); this.counters.translationId = Math.max(this.counters.translationId, translation.id + 1); }); } // Load tags if (jsonData.tags) { jsonData.tags.forEach((tag: Tag) => { this.data.tags.set(tag.id, { ...tag, createdAt: new Date(tag.createdAt) }); this.counters.tagId = Math.max(this.counters.tagId, tag.id + 1); }); } // Load work-tag relations if (jsonData.workTagsRelations) { for (const [workId, tagIds] of Object.entries(jsonData.workTagsRelations)) { this.data.workTagsRelations.set( parseInt(workId), new Set((tagIds as number[]).map(id => id)) ); } } // Load blog post-tag relations if (jsonData.blogPostTagsRelations) { for (const [postId, tagIds] of Object.entries(jsonData.blogPostTagsRelations)) { this.data.blogPostTagsRelations.set( parseInt(postId), new Set((tagIds as number[]).map(id => id)) ); } } // Load bookmarks if (jsonData.bookmarks) { jsonData.bookmarks.forEach((bookmark: Bookmark) => { this.data.bookmarks.set(bookmark.id, { ...bookmark, createdAt: new Date(bookmark.createdAt) }); this.counters.bookmarkId = Math.max(this.counters.bookmarkId, bookmark.id + 1); }); } // Load comments if (jsonData.comments) { jsonData.comments.forEach((comment: Comment) => { this.data.comments.set(comment.id, { ...comment, createdAt: new Date(comment.createdAt) }); this.counters.commentId = Math.max(this.counters.commentId, comment.id + 1); }); } // Load likes if (jsonData.likes) { jsonData.likes.forEach((like: Like) => { this.data.likes.set(like.id, { ...like, createdAt: new Date(like.createdAt) }); this.counters.likeId = Math.max(this.counters.likeId, like.id + 1); }); } // Load collections if (jsonData.collections) { jsonData.collections.forEach((collection: Collection) => { this.data.collections.set(collection.id, { ...collection, createdAt: new Date(collection.createdAt) }); this.counters.collectionId = Math.max(this.counters.collectionId, collection.id + 1); }); } // Load collection items if (jsonData.collectionItems) { jsonData.collectionItems.forEach((item: CollectionItem) => { this.data.collectionItems.set(item.id, { ...item, createdAt: new Date(item.createdAt) }); this.counters.collectionItemId = Math.max(this.counters.collectionItemId, item.id + 1); }); } // Load timeline events if (jsonData.timelineEvents) { jsonData.timelineEvents.forEach((event: TimelineEvent) => { this.data.timelineEvents.set(event.id, { ...event, createdAt: new Date(event.createdAt) }); this.counters.timelineEventId = Math.max(this.counters.timelineEventId, event.id + 1); }); } // Load blog posts if (jsonData.blogPosts) { jsonData.blogPosts.forEach((post: BlogPost) => { this.data.blogPosts.set(post.id, { ...post, createdAt: new Date(post.createdAt), publishedAt: post.publishedAt ? new Date(post.publishedAt) : null }); this.counters.blogPostId = Math.max(this.counters.blogPostId, post.id + 1); }); } // Load reading progresses if (jsonData.readingProgresses) { jsonData.readingProgresses.forEach((progress: ReadingProgress) => { const key = `${progress.userId}-${progress.workId}-${progress.translationId || 0}`; this.data.readingProgresses.set(key, { ...progress, lastReadAt: new Date(progress.lastReadAt) }); this.counters.readingProgressId = Math.max(this.counters.readingProgressId, progress.id + 1); }); } console.log('Data loaded successfully'); console.log(`Users: ${this.data.users.size}`); console.log(`Authors: ${this.data.authors.size}`); console.log(`Works: ${this.data.works.size}`); console.log(`Translations: ${this.data.translations.size}`); } catch (error) { console.error('Error loading data from file:', error); } } private saveDataToFile() { try { const jsonData = { users: Array.from(this.data.users.values()), authors: Array.from(this.data.authors.values()), works: Array.from(this.data.works.values()), translations: Array.from(this.data.translations.values()), tags: Array.from(this.data.tags.values()), workTagsRelations: Object.fromEntries( Array.from(this.data.workTagsRelations.entries()).map( ([workId, tagIds]) => [workId, Array.from(tagIds)] ) ), blogPostTagsRelations: Object.fromEntries( Array.from(this.data.blogPostTagsRelations.entries()).map( ([postId, tagIds]) => [postId, Array.from(tagIds)] ) ), bookmarks: Array.from(this.data.bookmarks.values()), comments: Array.from(this.data.comments.values()), likes: Array.from(this.data.likes.values()), collections: Array.from(this.data.collections.values()), collectionItems: Array.from(this.data.collectionItems.values()), timelineEvents: Array.from(this.data.timelineEvents.values()), blogPosts: Array.from(this.data.blogPosts.values()), readingProgresses: Array.from(this.data.readingProgresses.values()) }; fs.writeFileSync(this.dataFilePath, JSON.stringify(jsonData, null, 2), 'utf8'); console.log(`Data saved to ${this.dataFilePath}`); } catch (error) { console.error('Error saving data to file:', error); } } // User methods async getUser(id: number): Promise { return this.data.users.get(id); } async getUserByUsername(username: string): Promise { return Array.from(this.data.users.values()).find(user => user.username === username); } async createUser(insertUser: InsertUser): Promise { const id = this.counters.userId++; const now = new Date(); const user: User = { ...insertUser, id, role: 'user', createdAt: now }; this.data.users.set(id, user); this.saveDataToFile(); return user; } // Author methods async getAuthors(limit: number = 100, offset: number = 0): Promise { return Array.from(this.data.authors.values()) .sort((a, b) => a.name.localeCompare(b.name)) .slice(offset, offset + limit); } async getAuthorById(id: number): Promise { return this.data.authors.get(id); } async getAuthorBySlug(slug: string): Promise { return Array.from(this.data.authors.values()).find(author => author.slug === slug); } async createAuthor(insertAuthor: InsertAuthor): Promise { const id = this.counters.authorId++; const now = new Date(); const author: Author = { ...insertAuthor, id, createdAt: now }; this.data.authors.set(id, author); this.saveDataToFile(); return author; } // Work methods async getWorks(limit: number = 100, offset: number = 0): Promise { return Array.from(this.data.works.values()) .sort((a, b) => a.title.localeCompare(b.title)) .slice(offset, offset + limit); } async getWorkById(id: number): Promise { return this.data.works.get(id); } async getWorkBySlug(slug: string): Promise { return Array.from(this.data.works.values()).find(work => work.slug === slug); } async getWorksByAuthorId(authorId: number): Promise { return Array.from(this.data.works.values()).filter(work => work.authorId === authorId); } async getWorksByTags(tagIds: number[]): Promise { const tagSet = new Set(tagIds); const workIds = new Set(); this.data.workTagsRelations.forEach((tags, workId) => { for (const tagId of tags) { if (tagSet.has(tagId)) { workIds.add(workId); break; } } }); return Array.from(workIds).map(id => this.data.works.get(id)!).filter(Boolean); } async createWork(insertWork: InsertWork): Promise { const id = this.counters.workId++; const now = new Date(); const work: Work = { ...insertWork, id, createdAt: now }; this.data.works.set(id, work); this.saveDataToFile(); return work; } // Translation methods async getTranslations(workId: number): Promise { return Array.from(this.data.translations.values()).filter( translation => translation.workId === workId ); } async getTranslationById(id: number): Promise { return this.data.translations.get(id); } async createTranslation(insertTranslation: InsertTranslation): Promise { const id = this.counters.translationId++; const now = new Date(); const translation: Translation = { ...insertTranslation, id, createdAt: now }; this.data.translations.set(id, translation); this.saveDataToFile(); return translation; } // Tag methods async getTags(): Promise { return Array.from(this.data.tags.values()); } async getTagsByType(type: string): Promise { return Array.from(this.data.tags.values()).filter(tag => tag.type === type); } async createTag(insertTag: InsertTag): Promise { const id = this.counters.tagId++; const now = new Date(); const tag: Tag = { ...insertTag, id, createdAt: now }; this.data.tags.set(id, tag); this.saveDataToFile(); return tag; } async addTagToWork(workId: number, tagId: number): Promise { if (!this.data.workTagsRelations.has(workId)) { this.data.workTagsRelations.set(workId, new Set()); } this.data.workTagsRelations.get(workId)!.add(tagId); this.saveDataToFile(); } async getWorkTags(workId: number): Promise { const tagIds = this.data.workTagsRelations.get(workId) || new Set(); return Array.from(tagIds).map(id => this.data.tags.get(id)!).filter(Boolean); } async getBlogPostTags(postId: number): Promise { const tagIds = this.data.blogPostTagsRelations.get(postId) || new Set(); return Array.from(tagIds).map(id => this.data.tags.get(id)!).filter(Boolean); } async addTagToBlogPost(postId: number, tagId: number): Promise { console.log(`Adding tag ${tagId} to blog post ${postId}`); if (!this.data.blogPostTagsRelations.has(postId)) { console.log(`Creating new tag set for blog post ${postId}`); this.data.blogPostTagsRelations.set(postId, new Set()); } this.data.blogPostTagsRelations.get(postId)!.add(tagId); console.log(`After adding tag, blog post ${postId} has tags:`, Array.from(this.data.blogPostTagsRelations.get(postId) || new Set())); this.saveDataToFile(); } // Bookmark methods async getBookmarksByUserId(userId: number): Promise { return Array.from(this.data.bookmarks.values()).filter(bookmark => bookmark.userId === userId); } async getBookmarkByUserAndWork(userId: number, workId: number): Promise { return Array.from(this.data.bookmarks.values()).find( bookmark => bookmark.userId === userId && bookmark.workId === workId ); } async createBookmark(insertBookmark: InsertBookmark): Promise { const id = this.counters.bookmarkId++; const now = new Date(); const bookmark: Bookmark = { ...insertBookmark, id, createdAt: now }; this.data.bookmarks.set(id, bookmark); this.saveDataToFile(); return bookmark; } async deleteBookmark(id: number): Promise { this.data.bookmarks.delete(id); this.saveDataToFile(); } // Comment methods async getCommentsByWorkId(workId: number): Promise { return Array.from(this.data.comments.values()).filter( comment => comment.workId === workId ); } async getCommentsByTranslationId(translationId: number): Promise { return Array.from(this.data.comments.values()).filter( comment => comment.translationId === translationId ); } async getCommentsByUserId(userId: number): Promise { return Array.from(this.data.comments.values()).filter( comment => comment.userId === userId ); } async getCommentsByEntityId(entityType: string, entityId: number): Promise { return Array.from(this.data.comments.values()).filter( comment => comment.entityType === entityType && comment.entityId === entityId ); } async createComment(insertComment: InsertComment): Promise { const id = this.counters.commentId++; const now = new Date(); const comment: Comment = { ...insertComment, id, createdAt: now }; this.data.comments.set(id, comment); this.saveDataToFile(); return comment; } // Like methods async createLike(insertLike: InsertLike): Promise { const id = this.counters.likeId++; const now = new Date(); const like: Like = { ...insertLike, id, createdAt: now }; this.data.likes.set(id, like); this.saveDataToFile(); return like; } async deleteLike(id: number): Promise { this.data.likes.delete(id); this.saveDataToFile(); } async getLikesByEntity(entityType: string, entityId: number): Promise { return Array.from(this.data.likes.values()).filter( like => like.entityType === entityType && like.entityId === entityId ); } async getLikesByUserId(userId: number): Promise { return Array.from(this.data.likes.values()).filter( like => like.userId === userId ); } // Collection methods async getCollections(limit: number = 100, offset: number = 0): Promise { return Array.from(this.data.collections.values()) .sort((a, b) => a.title.localeCompare(b.title)) .slice(offset, offset + limit); } async getCollectionsByUserId(userId: number): Promise { return Array.from(this.data.collections.values()).filter( collection => collection.userId === userId ); } async getCollectionById(id: number): Promise { return this.data.collections.get(id); } async getCollectionBySlug(slug: string): Promise { return Array.from(this.data.collections.values()).find( collection => collection.slug === slug ); } async createCollection(insertCollection: InsertCollection): Promise { const id = this.counters.collectionId++; const now = new Date(); const collection: Collection = { ...insertCollection, id, createdAt: now }; this.data.collections.set(id, collection); this.saveDataToFile(); return collection; } async addWorkToCollection(insertCollectionItem: InsertCollectionItem): Promise { const id = this.counters.collectionItemId++; const now = new Date(); const collectionItem: CollectionItem = { ...insertCollectionItem, id, createdAt: now }; this.data.collectionItems.set(id, collectionItem); this.saveDataToFile(); return collectionItem; } async getCollectionItems(collectionId: number): Promise { return Array.from(this.data.collectionItems.values()).filter( item => item.collectionId === collectionId ); } // Timeline events async getTimelineEventsByAuthorId(authorId: number): Promise { return Array.from(this.data.timelineEvents.values()) .filter(event => event.authorId === authorId) .sort((a, b) => a.year - b.year); } async createTimelineEvent(insertEvent: InsertTimelineEvent): Promise { const id = this.counters.timelineEventId++; const now = new Date(); const event: TimelineEvent = { ...insertEvent, id, createdAt: now }; this.data.timelineEvents.set(id, event); this.saveDataToFile(); return event; } // Blog posts async getBlogPosts(limit: number = 100, offset: number = 0): Promise { return Array.from(this.data.blogPosts.values()) .sort((a, b) => { const dateA = a.publishedAt || a.createdAt; const dateB = b.publishedAt || b.createdAt; return dateB.getTime() - dateA.getTime(); // newest first }) .slice(offset, offset + limit); } async getBlogPostById(id: number): Promise { return this.data.blogPosts.get(id); } async getBlogPostBySlug(slug: string): Promise { return Array.from(this.data.blogPosts.values()).find( post => post.slug === slug ); } async createBlogPost(insertPost: InsertBlogPost): Promise { const id = this.counters.blogPostId++; const now = new Date(); const post: BlogPost = { ...insertPost, id, createdAt: now }; this.data.blogPosts.set(id, post); console.log('Created blog post with ID:', id); this.saveDataToFile(); return post; } // Reading progress async getReadingProgress(userId: number, workId: number, translationId?: number): Promise { const key = `${userId}-${workId}-${translationId || 0}`; return this.data.readingProgresses.get(key); } async updateReadingProgress(insertProgress: InsertReadingProgress): Promise { const key = `${insertProgress.userId}-${insertProgress.workId}-${insertProgress.translationId || 0}`; if (this.data.readingProgresses.has(key)) { const existing = this.data.readingProgresses.get(key)!; const updated: ReadingProgress = { ...existing, progress: insertProgress.progress ?? existing.progress, lastReadAt: new Date() }; this.data.readingProgresses.set(key, updated); this.saveDataToFile(); return updated; } else { const id = this.counters.readingProgressId++; const progress: ReadingProgress = { id, workId: insertProgress.workId, progress: insertProgress.progress ?? 0, userId: insertProgress.userId, translationId: insertProgress.translationId ?? null, lastReadAt: new Date() }; this.data.readingProgresses.set(key, progress); this.saveDataToFile(); return progress; } } // Search and filter methods async searchWorks(query: string, limit: number = 10): Promise { const searchTerms = query.toLowerCase().split(/\s+/); const works = Array.from(this.data.works.values()); const scoredWorks = works.map(work => { let score = 0; const title = work.title.toLowerCase(); const description = work.description?.toLowerCase() || ''; searchTerms.forEach(term => { if (title.includes(term)) score += 10; if (description.includes(term)) score += 5; }); return { work, score }; }); return scoredWorks .filter(item => item.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map(item => item.work); } async searchAuthors(query: string, limit: number = 10): Promise { const searchTerms = query.toLowerCase().split(/\s+/); const authors = Array.from(this.data.authors.values()); const scoredAuthors = authors.map(author => { let score = 0; const name = author.name.toLowerCase(); const biography = author.biography?.toLowerCase() || ''; const country = author.country?.toLowerCase() || ''; searchTerms.forEach(term => { if (name.includes(term)) score += 10; if (country.includes(term)) score += 7; if (biography.includes(term)) score += 3; }); return { author, score }; }); return scoredAuthors .filter(item => item.score > 0) .sort((a, b) => b.score - a.score) .slice(0, limit) .map(item => item.author); } async filterWorks(params: { language?: string, type?: string, yearStart?: number, yearEnd?: number, tags?: number[] }): Promise { let filteredWorks = Array.from(this.data.works.values()); if (params.language) { filteredWorks = filteredWorks.filter(work => work.language === params.language); } if (params.type) { filteredWorks = filteredWorks.filter(work => work.type === params.type); } if (params.yearStart) { filteredWorks = filteredWorks.filter(work => work.year! >= params.yearStart!); } if (params.yearEnd) { filteredWorks = filteredWorks.filter(work => work.year! <= params.yearEnd!); } if (params.tags && params.tags.length > 0) { const taggedWorkIds = new Set(); this.data.workTagsRelations.forEach((tagIds, workId) => { for (const tagId of params.tags!) { if (tagIds.has(tagId)) { taggedWorkIds.add(workId); break; } } }); filteredWorks = filteredWorks.filter(work => taggedWorkIds.has(work.id)); } return filteredWorks; } }