bookmark-folder.ts 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233
  1. import { objectIdUtils } from '@growi/core';
  2. import monggoose, {
  3. Types, Document, Model, Schema,
  4. } from 'mongoose';
  5. import { IBookmarkFolder, BookmarkFolderItems, MyBookmarkList } from '~/interfaces/bookmark-info';
  6. import { IPageHasId } from '~/interfaces/page';
  7. import loggerFactory from '../../utils/logger';
  8. import { getOrCreateModel } from '../util/mongoose-utils';
  9. import { InvalidParentBookmarkFolderError } from './errors';
  10. const logger = loggerFactory('growi:models:bookmark-folder');
  11. const Bookmark = monggoose.model('Bookmark');
  12. export interface BookmarkFolderDocument extends Document {
  13. _id: Types.ObjectId
  14. name: string
  15. owner: Types.ObjectId
  16. parent?: Types.ObjectId | undefined
  17. bookmarks?: Types.ObjectId[],
  18. children?: BookmarkFolderDocument[]
  19. }
  20. export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
  21. createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
  22. findFolderAndChildren(user: Types.ObjectId | string, parentId?: Types.ObjectId | string): Promise<BookmarkFolderItems[]>
  23. deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
  24. updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null): Promise<BookmarkFolderDocument>
  25. insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
  26. findUserRootBookmarksItem(userId: Types.ObjectId| string): Promise<MyBookmarkList>
  27. updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
  28. }
  29. const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderModel>({
  30. name: { type: String },
  31. owner: { type: Schema.Types.ObjectId, ref: 'User', index: true },
  32. parent: {
  33. type: Schema.Types.ObjectId,
  34. ref: 'BookmarkFolder',
  35. required: false,
  36. },
  37. bookmarks: {
  38. type: [{
  39. type: Schema.Types.ObjectId, ref: 'Bookmark', required: false,
  40. }],
  41. required: false,
  42. default: [],
  43. },
  44. }, {
  45. toObject: { virtuals: true },
  46. });
  47. bookmarkFolderSchema.virtual('children', {
  48. ref: 'BookmarkFolder',
  49. localField: '_id',
  50. foreignField: 'parent',
  51. });
  52. bookmarkFolderSchema.statics.createByParameters = async function(params: IBookmarkFolder): Promise<BookmarkFolderDocument> {
  53. const { name, owner, parent } = params;
  54. let bookmarkFolder: BookmarkFolderDocument;
  55. if (parent == null) {
  56. bookmarkFolder = await this.create({ name, owner });
  57. }
  58. else {
  59. // Check if parent folder id is valid and parent folder exists
  60. const isParentFolderIdValid = objectIdUtils.isValidObjectId(parent as string);
  61. if (!isParentFolderIdValid) {
  62. throw new InvalidParentBookmarkFolderError('Parent folder id is invalid');
  63. }
  64. const parentFolder = await this.findById(parent);
  65. if (parentFolder == null) {
  66. throw new InvalidParentBookmarkFolderError('Parent folder not found');
  67. }
  68. bookmarkFolder = await this.create({ name, owner, parent: parentFolder._id });
  69. }
  70. return bookmarkFolder;
  71. };
  72. bookmarkFolderSchema.statics.findFolderAndChildren = async function(
  73. userId: Types.ObjectId | string,
  74. parentId?: Types.ObjectId | string,
  75. ): Promise<BookmarkFolderItems[]> {
  76. const folderItems: BookmarkFolderItems[] = [];
  77. const folders = await this.find({ owner: userId, parent: parentId })
  78. .populate('children')
  79. .populate({
  80. path: 'bookmarks',
  81. model: 'Bookmark',
  82. populate: {
  83. path: 'page',
  84. model: 'Page',
  85. },
  86. });
  87. const promises = folders.map(async(folder) => {
  88. const children = await this.findFolderAndChildren(userId, folder._id);
  89. const {
  90. _id, name, owner, bookmarks, parent,
  91. } = folder;
  92. const res = {
  93. _id: _id.toString(),
  94. name,
  95. owner,
  96. bookmarks,
  97. children,
  98. parent,
  99. };
  100. return res;
  101. });
  102. const results = await Promise.all(promises) as unknown as BookmarkFolderItems[];
  103. folderItems.push(...results);
  104. return folderItems;
  105. };
  106. bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
  107. const bookmarkFolder = await this.findById(bookmarkFolderId);
  108. // Delete parent and all children folder
  109. let deletedCount = 0;
  110. if (bookmarkFolder != null) {
  111. // Delete Bookmarks
  112. const bookmarks = bookmarkFolder?.bookmarks;
  113. if (bookmarks && bookmarks.length > 0) {
  114. await Bookmark.deleteMany({ _id: { $in: bookmarks } });
  115. }
  116. // Delete all child recursively and update deleted count
  117. const childFolders = await this.find({ parent: bookmarkFolder._id });
  118. await Promise.all(childFolders.map(async(child) => {
  119. const deletedChildFolder = await this.deleteFolderAndChildren(child._id);
  120. deletedCount += deletedChildFolder.deletedCount;
  121. }));
  122. const deletedChild = await this.deleteMany({ parent: bookmarkFolder._id });
  123. deletedCount += deletedChild.deletedCount + 1;
  124. bookmarkFolder.delete();
  125. }
  126. return { deletedCount };
  127. };
  128. bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parentId: string | null):
  129. Promise<BookmarkFolderDocument> {
  130. const updateFields: {name: string, parent: Types.ObjectId | null} = {
  131. name: '',
  132. parent: null,
  133. };
  134. updateFields.name = name;
  135. const parentFolder = parentId ? await this.findById(parentId) : null;
  136. updateFields.parent = parentFolder?._id ?? null;
  137. // Maximum folder hierarchy of 2 levels
  138. // If the destination folder (parentFolder) has a parent, the source folder cannot be moved because the destination folder hierarchy is already 2.
  139. // If the drop source folder has child folders, the drop source folder cannot be moved because the drop source folder hierarchy is already 2.
  140. if (parentId != null) {
  141. if (parentFolder?.parent != null) {
  142. throw new Error('Update bookmark folder failed');
  143. }
  144. const bookmarkFolder = await this.findById(bookmarkFolderId).populate('children');
  145. if (bookmarkFolder?.children?.length !== 0) {
  146. throw new Error('Update bookmark folder failed');
  147. }
  148. }
  149. const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true });
  150. if (bookmarkFolder == null) {
  151. throw new Error('Update bookmark folder failed');
  152. }
  153. return bookmarkFolder;
  154. };
  155. bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null):
  156. Promise<BookmarkFolderDocument | null> {
  157. // Create bookmark or update existing
  158. const bookmarkedPage = await Bookmark.findOneAndUpdate({ page: pageId, user: userId }, { page: pageId, user: userId }, { new: true, upsert: true });
  159. // Remove existing bookmark in bookmark folder
  160. await this.updateMany({}, { $pull: { bookmarks: bookmarkedPage._id } });
  161. // Insert bookmark into bookmark folder
  162. if (folderId != null) {
  163. const bookmarkFolder = await this.findByIdAndUpdate(folderId, { $addToSet: { bookmarks: bookmarkedPage } }, { new: true, upsert: true });
  164. return bookmarkFolder;
  165. }
  166. return null;
  167. };
  168. bookmarkFolderSchema.statics.findUserRootBookmarksItem = async function(userId: Types.ObjectId | string): Promise<MyBookmarkList> {
  169. const bookmarkIdsInFolders = await this.distinct('bookmarks', { owner: userId });
  170. const userRootBookmarks: MyBookmarkList = await Bookmark.find({
  171. _id: { $nin: bookmarkIdsInFolders },
  172. user: userId,
  173. }).populate({
  174. path: 'page',
  175. model: 'Page',
  176. populate: {
  177. path: 'lastUpdateUser',
  178. model: 'User',
  179. },
  180. });
  181. return userRootBookmarks;
  182. };
  183. bookmarkFolderSchema.statics.updateBookmark = async function(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId | string):
  184. Promise<BookmarkFolderDocument | null> {
  185. // If isBookmarked
  186. if (status) {
  187. const bookmarkedPage = await Bookmark.findOne({ page: pageId });
  188. const bookmarkFolder = await this.findOne({ owner: userId, bookmarks: { $in: [bookmarkedPage?._id] } });
  189. if (bookmarkFolder != null) {
  190. await this.updateOne({ owner: userId, _id: bookmarkFolder._id }, { $pull: { bookmarks: bookmarkedPage?._id } });
  191. }
  192. if (bookmarkedPage) {
  193. await bookmarkedPage.delete();
  194. }
  195. return bookmarkFolder;
  196. }
  197. // else , Add bookmark
  198. await Bookmark.create({ page: pageId, user: userId });
  199. return null;
  200. };
  201. export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);