Procházet zdrojové kódy

Merge pull request #6778 from weseek/feat/gw7833-create-bookmarkfolder-model-and-api

feat: gw7833 create bookmarkfolder model and api
Kaori Tokashiki před 3 roky
rodič
revize
b3af38a812

+ 6 - 0
packages/app/src/interfaces/bookmark-info.ts

@@ -17,3 +17,9 @@ type BookmarkedPage = {
 }
 
 export type MyBookmarkList = BookmarkedPage[]
+
+export interface IBookmarkFolder {
+  name: string
+  owner: Ref<IUser>
+  parent?: Ref<this>
+}

+ 112 - 0
packages/app/src/server/models/bookmark-folder.ts

@@ -0,0 +1,112 @@
+import { isValidObjectId } from '@growi/core/src/utils/objectid-utils';
+import {
+  Types, Document, Model, Schema,
+} from 'mongoose';
+
+import { IBookmarkFolder } from '~/interfaces/bookmark-info';
+
+
+import loggerFactory from '../../utils/logger';
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+import { InvalidParentBookmarkFolderError } from './errors';
+
+const logger = loggerFactory('growi:models:bookmark-folder');
+
+export interface BookmarkFolderItems {
+  _id: string
+  name: string
+  children: this[]
+}
+export interface BookmarkFolderDocument extends Document {
+  _id: Types.ObjectId
+  name: string
+  owner: Types.ObjectId
+  parent?: [this]
+}
+
+export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
+  createByParameters(params: IBookmarkFolder): BookmarkFolderDocument
+  findFolderAndChildren(user: Types.ObjectId | string, parentId?: Types.ObjectId | string): BookmarkFolderItems[]
+  findChildFolderById(parentBookmarkFolder: Types.ObjectId | string): Promise<BookmarkFolderDocument[]>
+  deleteFolderAndChildren(bookmarkFolderId: string): {deletedCount: number}
+  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string): BookmarkFolderDocument | null
+}
+
+const bookmarkFolderSchema = new Schema<BookmarkFolderDocument, BookmarkFolderModel>({
+  name: { type: String },
+  owner: { type: Schema.Types.ObjectId, ref: 'User', index: true },
+  parent: { type: Schema.Types.ObjectId, ref: 'BookmarkFolder', required: false },
+});
+
+bookmarkFolderSchema.virtual('children', {
+  ref: 'BookmarkFolder',
+  localField: '_id',
+  foreignField: 'parent',
+});
+
+bookmarkFolderSchema.statics.createByParameters = async function(params: IBookmarkFolder): Promise<BookmarkFolderDocument> {
+  const { name, owner, parent } = params;
+  let bookmarkFolder;
+
+  if (parent == null) {
+    bookmarkFolder = await this.create({ name, owner }) as unknown as BookmarkFolderDocument;
+  }
+  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 }) as unknown as BookmarkFolderDocument;
+  }
+
+  return bookmarkFolder;
+};
+
+bookmarkFolderSchema.statics.findFolderAndChildren = async function(
+    userId: Types.ObjectId | string,
+    parentId?: Types.ObjectId | string,
+): Promise<BookmarkFolderItems[]> {
+  const parentFolder = await this.findById(parentId) as unknown as BookmarkFolderDocument;
+  const bookmarks = await this.find({ owner: userId, parent: parentFolder }).populate({ path: 'children' }).exec() as unknown as BookmarkFolderItems[];
+  return bookmarks;
+};
+
+bookmarkFolderSchema.statics.findChildFolderById = async function(parentFolderId: Types.ObjectId | string): Promise<BookmarkFolderDocument[]> {
+  const parentFolder = await this.findById(parentFolderId) as unknown as BookmarkFolderDocument;
+  const childFolders = await this.find({ parent: parentFolder });
+  return childFolders;
+};
+
+bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(boookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
+  // Delete parent and all children folder
+  const bookmarkFolder = await this.findByIdAndDelete(boookmarkFolderId);
+  let deletedCount = 0;
+  if (bookmarkFolder != null) {
+    const childFolders = await this.deleteMany({ parent: bookmarkFolder?.id });
+    deletedCount = childFolders.deletedCount + 1;
+  }
+  return { deletedCount };
+};
+
+bookmarkFolderSchema.statics.updateBookmarkFolder = async function(bookmarkFolderId: string, name: string, parent: string):
+ Promise<BookmarkFolderDocument | null> {
+
+  const parentFolder = await this.findById(parent);
+  const updateFields = {
+    name, parent: parentFolder?._id || null,
+  };
+  const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true });
+  return bookmarkFolder;
+
+};
+
+bookmarkFolderSchema.set('toObject', { virtuals: true });
+
+export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);

+ 3 - 0
packages/app/src/server/models/errors.ts

@@ -16,3 +16,6 @@ export class PathAlreadyExistsError extends ExtensibleCustomError {
 * User Authentication
 */
 export class NullUsernameToBeRegisteredError extends ExtensibleCustomError {}
+
+// Invalid Parent bookmark folder error
+export class InvalidParentBookmarkFolderError extends ExtensibleCustomError {}

+ 95 - 0
packages/app/src/server/routes/apiv3/bookmark-folder.ts

@@ -0,0 +1,95 @@
+import { ErrorV3 } from '@growi/core';
+import { body } from 'express-validator';
+
+import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
+import { InvalidParentBookmarkFolderError } from '~/server/models/errors';
+import loggerFactory from '~/utils/logger';
+
+import BookmarkFolder from '../../models/bookmark-folder';
+
+const logger = loggerFactory('growi:routes:apiv3:bookmark-folder');
+
+const express = require('express');
+
+const router = express.Router();
+
+const validator = {
+  bookmarkFolder: [
+    body('name').isString().withMessage('name must be a string'),
+    body('parent').isMongoId().optional({ nullable: true }),
+  ],
+};
+
+module.exports = (crowi) => {
+  const accessTokenParser = require('../../middlewares/access-token-parser')(crowi);
+  const loginRequiredStrictly = require('../../middlewares/login-required')(crowi);
+
+  // Create new bookmark folder
+  router.post('/', accessTokenParser, loginRequiredStrictly, validator.bookmarkFolder, apiV3FormValidator, async(req, res) => {
+    const owner = req.user?._id;
+    const { name, parent } = req.body;
+    const params = {
+      name, owner, parent,
+    };
+
+    try {
+      const bookmarkFolder = await BookmarkFolder.createByParameters(params);
+      logger.debug('bookmark folder created', bookmarkFolder);
+      return res.apiv3({ bookmarkFolder });
+    }
+    catch (err) {
+      if (err instanceof InvalidParentBookmarkFolderError) {
+        return res.apiv3Err(new ErrorV3(err.message, 'failed_to_create_bookmark_folder'));
+      }
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  // List all main bookmark folders
+  router.get('/list', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    try {
+      const bookmarkFolders = await BookmarkFolder.findFolderAndChildren(req.user?._id);
+      return res.apiv3({ bookmarkFolders });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  router.get('/list-child/:parentId', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    const { parentId } = req.params;
+    try {
+      const bookmarkFolders = await BookmarkFolder.findChildFolderById(parentId);
+      return res.apiv3({ bookmarkFolders });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  // Delete bookmark folder and children
+  router.delete('/:id', accessTokenParser, loginRequiredStrictly, async(req, res) => {
+    const { id } = req.params;
+    try {
+      const result = await BookmarkFolder.deleteFolderAndChildren(id);
+      const { deletedCount } = result;
+      return res.apiv3({ deletedCount });
+    }
+    catch (err) {
+      logger.error(err);
+      return res.apiv3Err(err, 500);
+    }
+  });
+
+  router.put('/', accessTokenParser, loginRequiredStrictly, validator.bookmarkFolder, async(req, res) => {
+    const { bookmarkFolderId, name, parent } = req.body;
+    try {
+      const bookmarkFolder = await BookmarkFolder.updateBookmarkFolder(bookmarkFolderId, name, parent);
+      return res.apiv3({ bookmarkFolder });
+    }
+    catch (err) {
+      return res.apiv3Err(err, 500);
+    }
+  });
+  return router;
+};

+ 1 - 0
packages/app/src/server/routes/apiv3/index.js

@@ -101,6 +101,7 @@ module.exports = (crowi, app, isInstalled) => {
 
   router.use('/user-ui-settings', require('./user-ui-settings')(crowi));
 
+  router.use('/bookmark-folder', require('./bookmark-folder')(crowi));
 
   return [router, routerForAdmin, routerForAuth];
 };

+ 3 - 2
packages/app/src/stores/bookmark.ts

@@ -1,7 +1,7 @@
+import { IUserHasId, Nullable } from '@growi/core';
 import { SWRResponse } from 'swr';
 import useSWRImmutable from 'swr/immutable';
 
-import { Nullable } from '~/interfaces/common';
 import { IPageHasId } from '~/interfaces/page';
 
 import { apiv3Get } from '../client/util/apiv3-client';
@@ -25,8 +25,9 @@ export const useSWRBookmarkInfo = (pageId: string | null | undefined): SWRRespon
 export const useSWRxCurrentUserBookmarks = (pageNum?: Nullable<number>): SWRResponse<IPageHasId[], Error> => {
   const { data: currentUser } = useCurrentUser();
   const currentPage = pageNum ?? 1;
+  const user = currentUser as IUserHasId;
   return useSWRImmutable(
-    currentUser != null ? `/bookmarks/${currentUser._id}` : null,
+    currentUser != null ? `/bookmarks/${user._id}` : null,
     endpoint => apiv3Get(endpoint, { page: currentPage }).then((response) => {
       const { paginationResult } = response.data;
       return paginationResult.docs.map((item) => {