tercul-frontend/server/jsonStorage.ts
mukimovd 07e354075e Populate the platform with sample literary data and analysis features
Adds sample author data, text content, analysis results and annotation entities to JSON storage.

Replit-Commit-Author: Agent
Replit-Commit-Session-Id: cbacfb18-842a-4116-a907-18c0105ad8ec
Replit-Commit-Screenshot-Url: https://storage.googleapis.com/screenshot-production-us-central1/39b5c689-6e8a-4d5a-9792-69cc81a56534/32264298-034a-445f-8b51-86b6180c466c.jpg
2025-05-08 00:13:10 +00:00

854 lines
27 KiB
TypeScript

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, AnalysisResult, InsertAnalysisResult,
Annotation, InsertAnnotation
} from '../shared/schema';
export class JsonStorage implements IStorage {
private data: {
users: Map<number, User>;
authors: Map<number, Author>;
works: Map<number, Work>;
translations: Map<number, Translation>;
tags: Map<number, Tag>;
workTagsRelations: Map<number, Set<number>>;
blogPostTagsRelations: Map<number, Set<number>>;
bookmarks: Map<number, Bookmark>;
comments: Map<number, Comment>;
likes: Map<number, Like>;
collections: Map<number, Collection>;
collectionItems: Map<number, CollectionItem>;
timelineEvents: Map<number, TimelineEvent>;
blogPosts: Map<number, BlogPost>;
readingProgresses: Map<string, ReadingProgress>;
analysisResults: Map<number, AnalysisResult>;
annotations: Map<number, Annotation>;
};
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;
analysisResultId: number;
annotationId: number;
};
private dataFilePath: string;
constructor(dataFilePath: string = path.join(process.cwd(), 'data', 'sampleData.json')) {
this.dataFilePath = dataFilePath;
this.data = {
users: new Map<number, User>(),
authors: new Map<number, Author>(),
works: new Map<number, Work>(),
translations: new Map<number, Translation>(),
tags: new Map<number, Tag>(),
workTagsRelations: new Map<number, Set<number>>(),
blogPostTagsRelations: new Map<number, Set<number>>(),
bookmarks: new Map<number, Bookmark>(),
comments: new Map<number, Comment>(),
likes: new Map<number, Like>(),
collections: new Map<number, Collection>(),
collectionItems: new Map<number, CollectionItem>(),
timelineEvents: new Map<number, TimelineEvent>(),
blogPosts: new Map<number, BlogPost>(),
readingProgresses: new Map<string, ReadingProgress>(),
analysisResults: new Map<number, AnalysisResult>(),
annotations: new Map<number, Annotation>(),
};
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,
analysisResultId: 1,
annotationId: 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);
});
}
// Load analysis results
if (jsonData.analysisResults) {
jsonData.analysisResults.forEach((result: AnalysisResult) => {
this.data.analysisResults.set(result.id, {
...result,
createdAt: new Date(result.createdAt)
});
this.counters.analysisResultId = Math.max(this.counters.analysisResultId, result.id + 1);
});
}
// Load annotations
if (jsonData.annotations) {
jsonData.annotations.forEach((annotation: Annotation) => {
this.data.annotations.set(annotation.id, {
...annotation,
createdAt: new Date(annotation.createdAt)
});
this.counters.annotationId = Math.max(this.counters.annotationId, annotation.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}`);
console.log(`Analysis Results: ${this.data.analysisResults.size}`);
console.log(`Annotations: ${this.data.annotations.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()),
analysisResults: Array.from(this.data.analysisResults.values()),
annotations: Array.from(this.data.annotations.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<User | undefined> {
return this.data.users.get(id);
}
async getUserByUsername(username: string): Promise<User | undefined> {
return Array.from(this.data.users.values()).find(user => user.username === username);
}
async createUser(insertUser: InsertUser): Promise<User> {
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<Author[]> {
return Array.from(this.data.authors.values())
.sort((a, b) => a.name.localeCompare(b.name))
.slice(offset, offset + limit);
}
async getAuthorById(id: number): Promise<Author | undefined> {
return this.data.authors.get(id);
}
async getAuthorBySlug(slug: string): Promise<Author | undefined> {
return Array.from(this.data.authors.values()).find(author => author.slug === slug);
}
async createAuthor(insertAuthor: InsertAuthor): Promise<Author> {
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<Work[]> {
return Array.from(this.data.works.values())
.sort((a, b) => a.title.localeCompare(b.title))
.slice(offset, offset + limit);
}
async getWorkById(id: number): Promise<Work | undefined> {
return this.data.works.get(id);
}
async getWorkBySlug(slug: string): Promise<Work | undefined> {
return Array.from(this.data.works.values()).find(work => work.slug === slug);
}
async getWorksByAuthorId(authorId: number): Promise<Work[]> {
return Array.from(this.data.works.values()).filter(work => work.authorId === authorId);
}
async getWorksByTags(tagIds: number[]): Promise<Work[]> {
const tagSet = new Set(tagIds);
const workIds = new Set<number>();
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<Work> {
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<Translation[]> {
return Array.from(this.data.translations.values()).filter(
translation => translation.workId === workId
);
}
async getTranslationById(id: number): Promise<Translation | undefined> {
return this.data.translations.get(id);
}
async createTranslation(insertTranslation: InsertTranslation): Promise<Translation> {
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<Tag[]> {
return Array.from(this.data.tags.values());
}
async getTagsByType(type: string): Promise<Tag[]> {
return Array.from(this.data.tags.values()).filter(tag => tag.type === type);
}
async createTag(insertTag: InsertTag): Promise<Tag> {
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<void> {
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<Tag[]> {
const tagIds = this.data.workTagsRelations.get(workId) || new Set<number>();
return Array.from(tagIds).map(id => this.data.tags.get(id)!).filter(Boolean);
}
async getBlogPostTags(postId: number): Promise<Tag[]> {
const tagIds = this.data.blogPostTagsRelations.get(postId) || new Set<number>();
return Array.from(tagIds).map(id => this.data.tags.get(id)!).filter(Boolean);
}
async addTagToBlogPost(postId: number, tagId: number): Promise<void> {
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<Bookmark[]> {
return Array.from(this.data.bookmarks.values()).filter(bookmark => bookmark.userId === userId);
}
async getBookmarkByUserAndWork(userId: number, workId: number): Promise<Bookmark | undefined> {
return Array.from(this.data.bookmarks.values()).find(
bookmark => bookmark.userId === userId && bookmark.workId === workId
);
}
async createBookmark(insertBookmark: InsertBookmark): Promise<Bookmark> {
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<void> {
this.data.bookmarks.delete(id);
this.saveDataToFile();
}
// Comment methods
async getCommentsByWorkId(workId: number): Promise<Comment[]> {
return Array.from(this.data.comments.values()).filter(
comment => comment.workId === workId
);
}
async getCommentsByTranslationId(translationId: number): Promise<Comment[]> {
return Array.from(this.data.comments.values()).filter(
comment => comment.translationId === translationId
);
}
async getCommentsByUserId(userId: number): Promise<Comment[]> {
return Array.from(this.data.comments.values()).filter(
comment => comment.userId === userId
);
}
async getCommentsByEntityId(entityType: string, entityId: number): Promise<Comment[]> {
return Array.from(this.data.comments.values()).filter(
comment => comment.entityType === entityType && comment.entityId === entityId
);
}
async createComment(insertComment: InsertComment): Promise<Comment> {
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<Like> {
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<void> {
this.data.likes.delete(id);
this.saveDataToFile();
}
async getLikesByEntity(entityType: string, entityId: number): Promise<Like[]> {
return Array.from(this.data.likes.values()).filter(
like => like.entityType === entityType && like.entityId === entityId
);
}
async getLikesByUserId(userId: number): Promise<Like[]> {
return Array.from(this.data.likes.values()).filter(
like => like.userId === userId
);
}
// Collection methods
async getCollections(limit: number = 100, offset: number = 0): Promise<Collection[]> {
return Array.from(this.data.collections.values())
.sort((a, b) => a.title.localeCompare(b.title))
.slice(offset, offset + limit);
}
async getCollectionsByUserId(userId: number): Promise<Collection[]> {
return Array.from(this.data.collections.values()).filter(
collection => collection.userId === userId
);
}
async getCollectionById(id: number): Promise<Collection | undefined> {
return this.data.collections.get(id);
}
async getCollectionBySlug(slug: string): Promise<Collection | undefined> {
return Array.from(this.data.collections.values()).find(
collection => collection.slug === slug
);
}
async createCollection(insertCollection: InsertCollection): Promise<Collection> {
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<CollectionItem> {
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<CollectionItem[]> {
return Array.from(this.data.collectionItems.values()).filter(
item => item.collectionId === collectionId
);
}
// Timeline events
async getTimelineEventsByAuthorId(authorId: number): Promise<TimelineEvent[]> {
return Array.from(this.data.timelineEvents.values())
.filter(event => event.authorId === authorId)
.sort((a, b) => a.year - b.year);
}
async createTimelineEvent(insertEvent: InsertTimelineEvent): Promise<TimelineEvent> {
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<BlogPost[]> {
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<BlogPost | undefined> {
return this.data.blogPosts.get(id);
}
async getBlogPostBySlug(slug: string): Promise<BlogPost | undefined> {
return Array.from(this.data.blogPosts.values()).find(
post => post.slug === slug
);
}
async createBlogPost(insertPost: InsertBlogPost): Promise<BlogPost> {
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<ReadingProgress | undefined> {
const key = `${userId}-${workId}-${translationId || 0}`;
return this.data.readingProgresses.get(key);
}
async updateReadingProgress(insertProgress: InsertReadingProgress): Promise<ReadingProgress> {
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<Work[]> {
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<Author[]> {
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<Work[]> {
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<number>();
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;
}
}