import { isValidObjectId } from '@growi/core/src/utils/objectid-utils'; import monggoose, { Types, Document, Model, Schema, } from 'mongoose'; import { IBookmarkFolder, BookmarkFolderItems, MyBookmarkList } from '~/interfaces/bookmark-info'; import { IPageHasId } from '~/interfaces/page'; import loggerFactory from '../../utils/logger'; import { getOrCreateModel } from '../util/mongoose-utils'; import { InvalidParentBookmarkFolderError } from './errors'; const logger = loggerFactory('growi:models:bookmark-folder'); const Bookmark = monggoose.model('Bookmark'); export interface BookmarkFolderDocument extends Document { _id: Types.ObjectId name: string owner: Types.ObjectId parent?: this[] bookmarks?: Types.ObjectId[] } export interface BookmarkFolderModel extends Model{ createByParameters(params: IBookmarkFolder): Promise findFolderAndChildren(user: Types.ObjectId | string, parentId?: Types.ObjectId | string): Promise deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null): Promise insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise findUserRootBookmarksItem(userId: Types.ObjectId| string): Promise } const bookmarkFolderSchema = new Schema({ name: { type: String }, owner: { type: Schema.Types.ObjectId, ref: 'User', index: true }, parent: { type: Schema.Types.ObjectId, ref: 'BookmarkFolder', required: false }, bookmarks: { type: [{ type: Schema.Types.ObjectId, ref: 'Bookmark', required: false, }], required: false, default: [], }, }, { toObject: { virtuals: true }, }); bookmarkFolderSchema.virtual('children', { ref: 'BookmarkFolder', localField: '_id', foreignField: 'parent', }); bookmarkFolderSchema.statics.createByParameters = async function(params: IBookmarkFolder): Promise { const { name, owner, parent } = params; let bookmarkFolder: BookmarkFolderDocument; if (parent == null) { bookmarkFolder = await this.create({ name, owner }); } else { // Check if parent folder id is valid and parent folder exists const isParentFolderIdValid = isValidObjectId(parent as string); if (!isParentFolderIdValid) { throw new InvalidParentBookmarkFolderError('Parent folder id is invalid'); } const parentFolder = await this.findById(parent); if (parentFolder == null) { throw new InvalidParentBookmarkFolderError('Parent folder not found'); } bookmarkFolder = await this.create({ name, owner, parent: parentFolder._id }); } return bookmarkFolder; }; bookmarkFolderSchema.statics.findFolderAndChildren = async function( userId: Types.ObjectId | string, parentId?: Types.ObjectId | string, ): Promise { let parentFolder: BookmarkFolderDocument | null; let query = {}; // Load child bookmark folders if (parentId != null) { parentFolder = await this.findById(parentId); if (parentFolder != null) { query = { owner: userId, parent: parentFolder }; } else { throw new InvalidParentBookmarkFolderError('Parent folder not found'); } } // Load initial / root bookmark folders else { query = { owner: userId, parent: null }; } const bookmarkFolders: BookmarkFolderItems[] = await this.find(query) .populate({ path: 'children' }) .populate({ path: 'bookmarks', model: 'Bookmark', populate: { path: 'page', model: 'Page', }, }); return bookmarkFolders; }; bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> { const bookmarkFolder = await this.findById(bookmarkFolderId); // Delete parent and all children folder let deletedCount = 0; if (bookmarkFolder != null) { // Delete Bookmarks const bookmarks = bookmarkFolder?.bookmarks; if (bookmarks && bookmarks.length > 0) { await Bookmark.deleteMany({ _id: { $in: bookmarks } }); } // Delete all child recursively and update deleted count const childFolders = await this.find({ parent: bookmarkFolder }); await Promise.all(childFolders.map(async(child) => { const deletedChildFolder = await this.deleteFolderAndChildren(child._id); deletedCount += deletedChildFolder.deletedCount; })); const deletedChild = await this.deleteMany({ parent: bookmarkFolder }); deletedCount += deletedChild.deletedCount + 1; bookmarkFolder.delete(); } return { deletedCount }; }; bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parentId: string | null): Promise { const updateFields: {name: string, parent: Types.ObjectId | null} = { name: '', parent: null, }; updateFields.name = name; const parentFolder = parentId ? await this.findById(parentId) : null; updateFields.parent = parentFolder?._id ?? null; const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true }); if (bookmarkFolder == null) { throw new Error('Update bookmark folder failed'); } return bookmarkFolder; }; bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise { // Create bookmark or update existing const bookmarkedPage = await Bookmark.findOneAndUpdate({ page: pageId, user: userId }, { page: pageId, user: userId }, { new: true, upsert: true }); // Remove existing bookmark in bookmark folder await this.updateMany({}, { $pull: { bookmarks: bookmarkedPage._id } }); // Insert bookmark into bookmark folder if (folderId != null) { const bookmarkFolder = await this.findByIdAndUpdate(folderId, { $addToSet: { bookmarks: bookmarkedPage } }, { new: true, upsert: true }); return bookmarkFolder; } return null; }; bookmarkFolderSchema.statics.findUserRootBookmarksItem = async function(userId: Types.ObjectId | string): Promise { const bookmarkIdsInFolders = await this.distinct('bookmarks', { owner: userId }); const userRootBookmarks: MyBookmarkList = await Bookmark.find({ _id: { $nin: bookmarkIdsInFolders }, }).populate({ path: 'page', model: 'Page', populate: { path: 'lastUpdateUser', model: 'User', }, }); return userRootBookmarks; }; export default getOrCreateModel('BookmarkFolder', bookmarkFolderSchema);