bookmark-folder.ts 6.8 KB

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