bookmark-folder.ts 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178
  1. import { objectIdUtils } from '@growi/core';
  2. import monggoose, {
  3. Types, Document, Model, Schema,
  4. } from 'mongoose';
  5. import { IBookmarkFolder } 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. deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
  23. updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null): Promise<BookmarkFolderDocument>
  24. insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
  25. updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
  26. }
  27. const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderModel>({
  28. name: { type: String },
  29. owner: { type: Schema.Types.ObjectId, ref: 'User', index: true },
  30. parent: {
  31. type: Schema.Types.ObjectId,
  32. ref: 'BookmarkFolder',
  33. required: false,
  34. },
  35. bookmarks: {
  36. type: [{
  37. type: Schema.Types.ObjectId, ref: 'Bookmark', required: false,
  38. }],
  39. required: false,
  40. default: [],
  41. },
  42. }, {
  43. toObject: { virtuals: true },
  44. });
  45. bookmarkFolderSchema.virtual('children', {
  46. ref: 'BookmarkFolder',
  47. localField: '_id',
  48. foreignField: 'parent',
  49. });
  50. bookmarkFolderSchema.statics.createByParameters = async function(params: IBookmarkFolder): Promise<BookmarkFolderDocument> {
  51. const { name, owner, parent } = params;
  52. let bookmarkFolder: BookmarkFolderDocument;
  53. if (parent == null) {
  54. bookmarkFolder = await this.create({ name, owner });
  55. }
  56. else {
  57. // Check if parent folder id is valid and parent folder exists
  58. const isParentFolderIdValid = objectIdUtils.isValidObjectId(parent as string);
  59. if (!isParentFolderIdValid) {
  60. throw new InvalidParentBookmarkFolderError('Parent folder id is invalid');
  61. }
  62. const parentFolder = await this.findById(parent);
  63. if (parentFolder == null) {
  64. throw new InvalidParentBookmarkFolderError('Parent folder not found');
  65. }
  66. bookmarkFolder = await this.create({ name, owner, parent: parentFolder._id });
  67. }
  68. return bookmarkFolder;
  69. };
  70. bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
  71. const bookmarkFolder = await this.findById(bookmarkFolderId);
  72. // Delete parent and all children folder
  73. let deletedCount = 0;
  74. if (bookmarkFolder != null) {
  75. // Delete Bookmarks
  76. const bookmarks = bookmarkFolder?.bookmarks;
  77. if (bookmarks && bookmarks.length > 0) {
  78. await Bookmark.deleteMany({ _id: { $in: bookmarks } });
  79. }
  80. // Delete all child recursively and update deleted count
  81. const childFolders = await this.find({ parent: bookmarkFolder._id });
  82. await Promise.all(childFolders.map(async(child) => {
  83. const deletedChildFolder = await this.deleteFolderAndChildren(child._id);
  84. deletedCount += deletedChildFolder.deletedCount;
  85. }));
  86. const deletedChild = await this.deleteMany({ parent: bookmarkFolder._id });
  87. deletedCount += deletedChild.deletedCount + 1;
  88. bookmarkFolder.delete();
  89. }
  90. return { deletedCount };
  91. };
  92. bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parentId: string | null):
  93. Promise<BookmarkFolderDocument> {
  94. const updateFields: {name: string, parent: Types.ObjectId | null} = {
  95. name: '',
  96. parent: null,
  97. };
  98. updateFields.name = name;
  99. const parentFolder = parentId ? await this.findById(parentId) : null;
  100. updateFields.parent = parentFolder?._id ?? null;
  101. // Maximum folder hierarchy of 2 levels
  102. // If the destination folder (parentFolder) has a parent, the source folder cannot be moved because the destination folder hierarchy is already 2.
  103. // If the drop source folder has child folders, the drop source folder cannot be moved because the drop source folder hierarchy is already 2.
  104. if (parentId != null) {
  105. if (parentFolder?.parent != null) {
  106. throw new Error('Update bookmark folder failed');
  107. }
  108. const bookmarkFolder = await this.findById(bookmarkFolderId);
  109. if (bookmarkFolder?.children?.length !== 0) {
  110. throw new Error('Update bookmark folder failed');
  111. }
  112. }
  113. const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true });
  114. if (bookmarkFolder == null) {
  115. throw new Error('Update bookmark folder failed');
  116. }
  117. return bookmarkFolder;
  118. };
  119. bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null):
  120. Promise<BookmarkFolderDocument | null> {
  121. // Find bookmark
  122. const bookmarkedPage = await Bookmark.findOne({ page: pageId, user: userId }, { new: true, upsert: true });
  123. // Remove existing bookmark in bookmark folder
  124. await this.updateMany({ owner: userId }, { $pull: { bookmarks: bookmarkedPage?._id } });
  125. if (folderId == null) {
  126. return null;
  127. }
  128. const bookmarkFolder = await this.findByIdAndUpdate(
  129. { _id: folderId, owner: userId },
  130. { $addToSet: { bookmarks: bookmarkedPage } },
  131. { new: true, upsert: true },
  132. );
  133. return bookmarkFolder;
  134. };
  135. bookmarkFolderSchema.statics.updateBookmark = async function(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId | string):
  136. Promise<BookmarkFolderDocument | null> {
  137. // If isBookmarked
  138. if (status) {
  139. const bookmarkedPage = await Bookmark.findOne({ page: pageId, user: userId });
  140. const bookmarkFolder = await this.findOne({ owner: userId, bookmarks: { $in: [bookmarkedPage?._id] } });
  141. if (bookmarkFolder != null) {
  142. await this.updateOne({ owner: userId, _id: bookmarkFolder._id }, { $pull: { bookmarks: bookmarkedPage?._id } });
  143. }
  144. if (bookmarkedPage) {
  145. await bookmarkedPage.delete();
  146. }
  147. return bookmarkFolder;
  148. }
  149. // else , Add bookmark
  150. await Bookmark.create({ page: pageId, user: userId });
  151. return null;
  152. };
  153. export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);