Browse Source

imprv: User group relation crud (#5083)

* CC to FC

* Improved userGroupTable

* SWRized

* Updated table & home page component

* Removed hard code

* Implemented listing api

* Fixed lint error

* Implemented key serializer for swr

* Impl update

* Omit serialize middleware

* Upgraded swr ^1.0.1 => ^1.1.2

* Improved sync

* Fixed lint errors

* Removed unnecessary code

* Improved nukey

* Improved types

* Improved interface

* Improved type validation

* Implemented delete

* Normalized view

* Improved

* Implemented delete

* Implemented create

* Renamed type

* Removed comments

* Removed unnecessary code

* Removed unnecessary method

* Added a comment

* Typescriptized

* Omitted crowi

* Fixed

* Fixed ci

* Fixed

* Fixed model import

* Deleted user-group.js

* Added async

* Improved
Haku Mizuki 4 years ago
parent
commit
7b1c44304f

+ 3 - 5
packages/app/src/components/Admin/UserGroupDetail/UserGroupDetailPage.tsx

@@ -104,11 +104,9 @@ const UserGroupDetailPage: FC = () => {
     return users;
   }, [searchType, isAlsoMailSearched, isAlsoNameSearched]);
 
+  // TODO 85062: will be used in UserGroupUserFormByInput
   const addUserByUsername = useCallback(async(username: string) => {
-    const res = await apiv3Post(`/user-groups/${userGroup._id}/users/${username}`);
-
-    // do not add users for ducaplicate
-    if (res.data.userGroupRelation == null) { return }
+    await apiv3Post(`/user-groups/${userGroup._id}/users/${username}`);
 
     await sync();
   }, [userGroup, sync]);
@@ -116,7 +114,7 @@ const UserGroupDetailPage: FC = () => {
   const removeUserByUsername = useCallback(async(username: string) => {
     const res = await apiv3Delete(`/user-groups/${userGroup._id}/users/${username}`);
 
-    setUserGroupRelations(prev => prev.filter(u => u._id !== res.data.userGroupRelation._id));
+    setUserGroupRelations(prev => prev.filter(u => u._id !== res.data.userGroupRelation._id)); // TODO 85062: use swr to sync
   }, [userGroup]);
 
   /*

+ 12 - 0
packages/app/src/server/models/user-group-relation.js

@@ -240,6 +240,18 @@ class UserGroupRelation {
     });
   }
 
+  static async createRelations(userGroupIds, user) {
+    const documentsToInsertMany = userGroupIds.map((groupId) => {
+      return {
+        relatedGroup: groupId,
+        relatedUser: user._id,
+        createdAt: new Date(),
+      };
+    });
+
+    return this.insertMany(documentsToInsertMany);
+  }
+
   /**
    * remove all relation for UserGroup
    *

+ 6 - 6
packages/app/src/server/models/user-group.ts

@@ -84,19 +84,19 @@ schema.statics.createGroup = async function(name, description, parentId) {
   return this.create({ name, description, parent });
 };
 
-schema.statics.findAllAncestorGroups = async function(parent, ancestors = [parent]) {
-  if (parent == null) {
+schema.statics.findGroupsWithAncestorsRecursively = async function(group, ancestors = [group]) {
+  if (group == null) {
     return ancestors;
   }
 
-  const nextParent = await this.findOne({ _id: parent.parent });
-  if (nextParent == null) {
+  const parent = await this.findOne({ _id: group.parent });
+  if (parent == null) {
     return ancestors;
   }
 
-  ancestors.push(nextParent);
+  ancestors.push(parent);
 
-  return this.findAllAncestorGroups(nextParent, ancestors);
+  return this.findGroupsWithAncestorsRecursively(parent, ancestors);
 };
 
 schema.statics.findGroupsWithDescendantsRecursively = async function(groups, descendants = groups) {

+ 15 - 10
packages/app/src/server/routes/apiv3/user-group.js

@@ -1,4 +1,5 @@
 import loggerFactory from '~/utils/logger';
+import { filterIdsByIds } from '~/server/util/compare-objectId';
 
 const logger = loggerFactory('growi:routes:apiv3:user-group'); // eslint-disable-line no-unused-vars
 
@@ -435,18 +436,19 @@ module.exports = (crowi) => {
         User.findUserByUsername(username),
       ]);
 
+      const userGroups = await UserGroup.findGroupsWithAncestorsRecursively(userGroup);
+      const userGroupIds = userGroups.map(g => g._id);
+
       // check for duplicate users in groups
-      const isRelatedUserForGroup = await UserGroupRelation.isRelatedUserForGroup(userGroup, user);
+      const existingRelations = await UserGroupRelation.find({ relatedGroup: { $in: userGroupIds }, relatedUser: user._id });
+      const existingGroupIds = existingRelations.map(r => r.relatedGroup);
 
-      if (isRelatedUserForGroup) {
-        logger.warn('The user is already joined');
-        return res.apiv3();
-      }
+      const groupIdsOfRelationToCreate = filterIdsByIds(userGroupIds, existingGroupIds);
 
-      const userGroupRelation = await UserGroupRelation.createRelation(userGroup, user);
+      const insertedRelations = await UserGroupRelation.createRelations(groupIdsOfRelationToCreate, user);
       const serializedUser = serializeUserSecurely(user);
 
-      return res.apiv3({ user: serializedUser, userGroup, userGroupRelation });
+      return res.apiv3({ user: serializedUser, createdRelationCount: insertedRelations.length });
     }
     catch (err) {
       const msg = `Error occurred in adding the user "${username}" to group "${id}"`;
@@ -504,13 +506,16 @@ module.exports = (crowi) => {
         User.findUserByUsername(username),
       ]);
 
-      const userGroupRelation = await UserGroupRelation.findOneAndDelete({ relatedUser: new ObjectId(user._id), relatedGroup: new ObjectId(userGroup._id) });
+      const groupsOfRelationsToDelete = await UserGroup.findGroupsWithDescendantsRecursively([userGroup]);
+      const relatedGroupIdsToDelete = groupsOfRelationsToDelete.map(g => g._id);
+
+      const deleteManyRes = await UserGroupRelation.deleteMany({ relatedUser: user._id, relatedGroup: { $in: relatedGroupIdsToDelete } });
       const serializedUser = serializeUserSecurely(user);
 
-      return res.apiv3({ user: serializedUser, userGroup, userGroupRelation });
+      return res.apiv3({ user: serializedUser, deletedGroupsCount: deleteManyRes.deletedCount });
     }
     catch (err) {
-      const msg = `Error occurred in removing the user "${username}" from group "${id}"`;
+      const msg = 'Error occurred while removing the user from groups.';
       logger.error(msg, err);
       return res.apiv3Err(new ErrorV3(msg, 'user-group-remove-user-failed'));
     }

+ 1 - 1
packages/app/src/server/service/user-group.ts

@@ -56,7 +56,7 @@ class UserGroupService {
     const usersBelongsToTargetButNotParent = targetGroupUsers.filter(user => !parentGroupUsers.includes(user));
     // add the target group's users to all ancestors
     if (forceUpdateParents) {
-      const ancestorGroups = await UserGroup.findAllAncestorGroups(parent);
+      const ancestorGroups = await UserGroup.findGroupsWithAncestorsRecursively(parent);
       const ancestorGroupIds = ancestorGroups.map(group => group._id);
 
       await UserGroupRelation.createByGroupIdsAndUserIds(ancestorGroupIds, usersBelongsToTargetButNotParent);

+ 16 - 0
packages/app/src/server/util/compare-objectId.ts

@@ -0,0 +1,16 @@
+import mongoose from 'mongoose';
+
+type IObjectId = mongoose.Types.ObjectId;
+const ObjectId = mongoose.Types.ObjectId;
+
+export const filterArr1ByArr2 = (arr1: IObjectId[], arr2: IObjectId[]): IObjectId[] => {
+  // cast to string
+  const arrToBeFiltered = arr1.map(e => e.toString());
+  const arrToFilter = arr2.map(e => e.toString());
+
+  // filter
+  const filtered = arrToBeFiltered.filter(e => !arrToFilter.includes(e));
+
+  // cast to ObjectId
+  return filtered.map(e => new ObjectId(e));
+};