Kaynağa Gözat

refs 125405: make uer-group service independent of ExternalUserGroup

Futa Arai 2 yıl önce
ebeveyn
işleme
0c20ef3dae

+ 12 - 22
apps/app/src/features/external-user-group/server/models/external-user-group-relation.ts

@@ -1,12 +1,20 @@
 import { Schema, Model, Document } from 'mongoose';
 
+import UserGroupRelation from '~/server/models/user-group-relation';
+
 import { getOrCreateModel } from '../../../../server/util/mongoose-utils';
-import { IExternalUserGroupHasId, IExternalUserGroupRelation } from '../../interfaces/external-user-group';
+import { IExternalUserGroupRelation } from '../../interfaces/external-user-group';
+
+import { ExternalUserGroupDocument } from './external-user-group';
 
 export interface ExternalUserGroupRelationDocument extends IExternalUserGroupRelation, Document {}
 
 export interface ExternalUserGroupRelationModel extends Model<ExternalUserGroupRelationDocument> {
   [x:string]: any, // for old methods
+
+  PAGE_ITEMS: 50,
+
+  removeAllByUserGroups: (groupsToDelete: ExternalUserGroupDocument[]) => Promise<any>,
 }
 
 const schema = new Schema<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>({
@@ -16,26 +24,8 @@ const schema = new Schema<ExternalUserGroupRelationDocument, ExternalUserGroupRe
   timestamps: { createdAt: true, updatedAt: false },
 });
 
-schema.statics.createRelations = async function(userGroupIds, user) {
-  const documentsToInsert = userGroupIds.map((groupId) => {
-    return {
-      relatedGroup: groupId,
-      relatedUser: user._id,
-    };
-  });
-
-  return this.insertMany(documentsToInsert);
-};
-
-/**
-   * remove all relation for ExternalUserGroup
-   *
-   * @static
-   * @param {ExternalUserGroup} userGroup related group for remove
-   * @returns {Promise<any>}
-   */
-schema.statics.removeAllByUserGroups = function(groupsToDelete: IExternalUserGroupHasId[]) {
-  return this.deleteMany({ relatedGroup: { $in: groupsToDelete } });
-};
+schema.statics.createRelations = UserGroupRelation.createRelations;
+
+schema.statics.removeAllByUserGroups = UserGroupRelation.removeAllByUserGroups;
 
 export default getOrCreateModel<ExternalUserGroupRelationDocument, ExternalUserGroupRelationModel>('ExternalUserGroupRelation', schema);

+ 4 - 0
apps/app/src/features/external-user-group/server/models/external-user-group.ts

@@ -9,6 +9,10 @@ export interface ExternalUserGroupDocument extends IExternalUserGroup, Document
 
 export interface ExternalUserGroupModel extends Model<ExternalUserGroupDocument> {
   [x:string]: any, // for old methods
+
+  PAGE_ITEMS: 10,
+
+  findGroupsWithDescendantsRecursively: (groups, descendants?) => any,
 }
 
 const schema = new Schema<ExternalUserGroupDocument, ExternalUserGroupModel>({

+ 10 - 3
apps/app/src/features/external-user-group/server/routes/apiv3/external-user-group.ts

@@ -4,8 +4,10 @@ import {
   body, param, query, validationResult,
 } from 'express-validator';
 
-import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
-import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
+import ExternalUserGroup, { ExternalUserGroupDocument } from '~/features/external-user-group/server/models/external-user-group';
+import ExternalUserGroupRelation, {
+  ExternalUserGroupRelationDocument,
+} from '~/features/external-user-group/server/models/external-user-group-relation';
 import { SupportedAction } from '~/interfaces/activity';
 import Crowi from '~/server/crowi';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
@@ -13,6 +15,7 @@ import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { serializeUserGroupRelationSecurely } from '~/server/models/serializers/user-group-relation-serializer';
 import { ApiV3Response } from '~/server/routes/apiv3/interfaces/apiv3-response';
 import { configManager } from '~/server/service/config-manager';
+import UserGroupService from '~/server/service/user-group';
 import loggerFactory from '~/utils/logger';
 
 import LdapUserGroupSyncService from '../../service/ldap-user-group-sync-service';
@@ -137,7 +140,11 @@ module.exports = (crowi: Crowi): Router => {
       const { actionName, transferToUserGroupId } = req.query;
 
       try {
-        const userGroups = await crowi.userGroupService.removeCompletelyByRootGroupId(deleteGroupId, actionName, transferToUserGroupId, req.user, true);
+        const userGroups = await (crowi.userGroupService as UserGroupService)
+          .removeCompletelyByRootGroupId<
+            ExternalUserGroupDocument,
+            ExternalUserGroupRelationDocument
+          >(deleteGroupId, actionName, transferToUserGroupId, req.user, ExternalUserGroup, ExternalUserGroupRelation);
 
         const parameters = { action: SupportedAction.ACTION_ADMIN_USER_GROUP_DELETE };
         activityEvent.emit('update', res.locals.activity._id, parameters);

+ 14 - 13
apps/app/src/server/crowi/index.js

@@ -22,6 +22,7 @@ import Activity from '../models/activity';
 import PageRedirect from '../models/page-redirect';
 import Tag from '../models/tag';
 import UserGroup from '../models/user-group';
+import UserGroupRelation from '../models/user-group-relation';
 import { aclService as aclServiceSingletonInstance } from '../service/acl';
 import AppService from '../service/app';
 import AttachmentService from '../service/attachment';
@@ -35,6 +36,7 @@ import PageOperationService from '../service/page-operation';
 import PassportService from '../service/passport';
 import SearchService from '../service/search';
 import { SlackIntegrationService } from '../service/slack-integration';
+import UserGroupService from '../service/user-group';
 import { UserNotificationService } from '../service/user-notification';
 import { getMongoUri, mongoOptions } from '../util/mongoose-utils';
 
@@ -297,21 +299,21 @@ Crowi.prototype.setupSocketIoService = async function() {
 };
 
 Crowi.prototype.setupModels = async function() {
-  let allModels = {};
-
-  // include models that dependent on crowi
-  allModels = models;
-
-  // include models that independent from crowi
-  allModels.Activity = Activity;
-  allModels.Tag = Tag;
-  allModels.UserGroup = UserGroup;
-  allModels.PageRedirect = PageRedirect;
-
-  Object.keys(allModels).forEach((key) => {
+  Object.keys(models).forEach((key) => {
     return this.model(key, models[key](this));
   });
 
+  // include models that are independent from crowi
+  const crowiIndependent = {};
+  crowiIndependent.Activity = Activity;
+  crowiIndependent.Tag = Tag;
+  crowiIndependent.UserGroup = UserGroup;
+  crowiIndependent.UserGroupRelation = UserGroupRelation;
+  crowiIndependent.PageRedirect = PageRedirect;
+
+  Object.keys(crowiIndependent).forEach((key) => {
+    return this.model(key, crowiIndependent[key]);
+  });
 };
 
 Crowi.prototype.setupCron = function() {
@@ -679,7 +681,6 @@ Crowi.prototype.setUpRestQiitaAPI = async function() {
 };
 
 Crowi.prototype.setupUserGroupService = async function() {
-  const UserGroupService = require('../service/user-group');
   if (this.userGroupService == null) {
     this.userGroupService = new UserGroupService(this);
     return this.userGroupService.init();

+ 0 - 2
apps/app/src/server/models/index.js

@@ -7,9 +7,7 @@ module.exports = {
   PageTagRelation: require('./page-tag-relation'),
   User: require('./user'),
   ExternalAccount: require('./external-account'),
-  UserGroupRelation: require('./user-group-relation'),
   Revision: require('./revision'),
-  Tag: require('./tag'),
   Bookmark: require('./bookmark'),
   Comment: require('./comment'),
   Attachment: require('./attachment'),

+ 0 - 391
apps/app/src/server/models/user-group-relation.js

@@ -1,391 +0,0 @@
-const debug = require('debug')('growi:models:userGroupRelation');
-const mongoose = require('mongoose');
-const mongoosePaginate = require('mongoose-paginate-v2');
-const uniqueValidator = require('mongoose-unique-validator');
-
-const ObjectId = mongoose.Schema.Types.ObjectId;
-
-
-/*
- * define schema
- */
-const schema = new mongoose.Schema({
-  relatedGroup: { type: ObjectId, ref: 'UserGroup', required: true },
-  relatedUser: { type: ObjectId, ref: 'User', required: true },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
-schema.plugin(mongoosePaginate);
-schema.plugin(uniqueValidator);
-
-
-/**
- * UserGroupRelation Class
- *
- * @class UserGroupRelation
- */
-class UserGroupRelation {
-
-  /**
-   * limit items num for pagination
-   *
-   * @readonly
-   * @static
-   * @memberof UserGroupRelation
-   */
-  static get PAGE_ITEMS() {
-    return 50;
-  }
-
-  static set crowi(crowi) {
-    this._crowi = crowi;
-  }
-
-  static get crowi() {
-    return this._crowi;
-  }
-
-  /**
-   * remove all invalid relations that has reference to unlinked document
-   */
-  static removeAllInvalidRelations() {
-    return this.findAllRelation()
-      .then((relations) => {
-        // filter invalid documents
-        return relations.filter((relation) => {
-          return relation.relatedUser == null || relation.relatedGroup == null;
-        });
-      })
-      .then((invalidRelations) => {
-        const ids = invalidRelations.map((relation) => { return relation._id });
-        return this.deleteMany({ _id: { $in: ids } });
-      });
-  }
-
-  /**
-   * find all user and group relation
-   *
-   * @static
-   * @returns {Promise<UserGroupRelation[]>}
-   * @memberof UserGroupRelation
-   */
-  static findAllRelation() {
-    return this
-      .find()
-      .populate('relatedUser')
-      .populate('relatedGroup')
-      .exec();
-  }
-
-  /**
-   * find all user and group relation of UserGroup
-   *
-   * @static
-   * @param {UserGroup} userGroup
-   * @returns {Promise<UserGroupRelation[]>}
-   * @memberof UserGroupRelation
-   */
-  static findAllRelationForUserGroup(userGroup) {
-    debug('findAllRelationForUserGroup is called', userGroup);
-    return this
-      .find({ relatedGroup: userGroup })
-      .populate('relatedUser')
-      .exec();
-  }
-
-  static async findAllUserIdsForUserGroup(userGroup) {
-    const relations = await this
-      .find({ relatedGroup: userGroup })
-      .select('relatedUser')
-      .exec();
-
-    return relations.map(r => r.relatedUser);
-  }
-
-  /**
-   * find all user and group relation of UserGroups
-   *
-   * @static
-   * @param {UserGroup[]} userGroups
-   * @returns {Promise<UserGroupRelation[]>}
-   * @memberof UserGroupRelation
-   */
-  static findAllRelationForUserGroups(userGroups) {
-    return this
-      .find({ relatedGroup: { $in: userGroups } })
-      .populate('relatedUser')
-      .exec();
-  }
-
-  /**
-   * find all user and group relation of User
-   *
-   * @static
-   * @param {User} user
-   * @returns {Promise<UserGroupRelation[]>}
-   * @memberof UserGroupRelation
-   */
-  static findAllRelationForUser(user) {
-    return this
-      .find({ relatedUser: user.id })
-      .populate('relatedGroup')
-      // filter documents only relatedGroup is not null
-      .then((userGroupRelations) => {
-        return userGroupRelations.filter((relation) => {
-          return relation.relatedGroup != null;
-        });
-      });
-  }
-
-  /**
-   * find all UserGroup IDs that related to specified User
-   *
-   * @static
-   * @param {User} user
-   * @returns {Promise<ObjectId[]>}
-   */
-  static async findAllUserGroupIdsRelatedToUser(user) {
-    const relations = await this.find({ relatedUser: user._id })
-      .select('relatedGroup')
-      .exec();
-
-    return relations.map((relation) => { return relation.relatedGroup });
-  }
-
-  /**
-   * count by related group id and related user
-   *
-   * @static
-   * @param {string} userGroupId find query param for relatedGroup
-   * @param {User} userData find query param for relatedUser
-   * @returns {Promise<number>}
-   */
-  static async countByGroupIdAndUser(userGroupId, userData) {
-    const query = {
-      relatedGroup: userGroupId,
-      relatedUser: userData.id,
-    };
-
-    return this.count(query);
-  }
-
-  /**
-   * find all "not" related user for UserGroup
-   *
-   * @static
-   * @param {UserGroup} userGroup for find users not related
-   * @returns {Promise<User>}
-   * @memberof UserGroupRelation
-   */
-  static findUserByNotRelatedGroup(userGroup, queryOptions) {
-    const User = UserGroupRelation.crowi.model('User');
-    let searchWord = new RegExp(`${queryOptions.searchWord}`);
-    switch (queryOptions.searchType) {
-      case 'forward':
-        searchWord = new RegExp(`^${queryOptions.searchWord}`);
-        break;
-      case 'backword':
-        searchWord = new RegExp(`${queryOptions.searchWord}$`);
-        break;
-    }
-    const searthField = [
-      { username: searchWord },
-    ];
-    if (queryOptions.isAlsoMailSearched === 'true') { searthField.push({ email: searchWord }) }
-    if (queryOptions.isAlsoNameSearched === 'true') { searthField.push({ name: searchWord }) }
-
-    return this.findAllRelationForUserGroup(userGroup)
-      .then((relations) => {
-        const relatedUserIds = relations.map((relation) => {
-          return relation.relatedUser.id;
-        });
-        const query = {
-          _id: { $nin: relatedUserIds },
-          status: User.STATUS_ACTIVE,
-          $or: searthField,
-        };
-
-        debug('findUserByNotRelatedGroup ', query);
-        return User.find(query).exec();
-      });
-  }
-
-  /**
-   * get if the user has relation for group
-   *
-   * @static
-   * @param {UserGroup} userGroup
-   * @param {User} user
-   * @returns {Promise<boolean>} is user related for group(or not)
-   * @memberof UserGroupRelation
-   */
-  static isRelatedUserForGroup(userGroup, user) {
-    const query = {
-      relatedGroup: userGroup.id,
-      relatedUser: user.id,
-    };
-
-    return this
-      .count(query)
-      .exec()
-      .then((count) => {
-        // return true or false of the relation is exists(not count)
-        return (count > 0);
-      });
-  }
-
-  /**
-   * create user and group relation
-   *
-   * @static
-   * @param {UserGroup} userGroup
-   * @param {User} user
-   * @returns {Promise<UserGroupRelation>} created relation
-   * @memberof UserGroupRelation
-   */
-  static createRelation(userGroup, user) {
-    return this.create({
-      relatedGroup: userGroup.id,
-      relatedUser: user.id,
-    });
-  }
-
-  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
-   *
-   * @static
-   * @param {UserGroup} userGroup related group for remove
-   * @returns {Promise<any>}
-   * @memberof UserGroupRelation
-   */
-  static removeAllByUserGroups(groupsToDelete) {
-    if (!Array.isArray(groupsToDelete)) {
-      throw Error('groupsToDelete must be an array.');
-    }
-
-    return this.deleteMany({ relatedGroup: { $in: groupsToDelete } });
-  }
-
-  /**
-   * remove relation by id
-   *
-   * @static
-   * @param {ObjectId} id
-   * @returns {Promise<any>}
-   * @memberof UserGroupRelation
-   */
-  static removeById(id) {
-    return this.findById(id)
-      .then((relationData) => {
-        if (relationData == null) {
-          throw new Error('UserGroupRelation data is not exists. id:', id);
-        }
-        else {
-          relationData.remove();
-        }
-      });
-  }
-
-  static async findUserIdsByGroupId(groupId) {
-    const relations = await this.find({ relatedGroup: groupId }, { _id: 0, relatedUser: 1 }).lean().exec(); // .lean() to get not ObjectId but string
-
-    return relations.map(relation => relation.relatedUser);
-  }
-
-  static async createByGroupIdsAndUserIds(groupIds, userIds) {
-    const insertOperations = [];
-
-    groupIds.forEach((groupId) => {
-      userIds.forEach((userId) => {
-        insertOperations.push({
-          insertOne: {
-            document: {
-              relatedGroup: groupId,
-              relatedUser: userId,
-            },
-          },
-        });
-      });
-    });
-
-    await this.bulkWrite(insertOperations);
-  }
-
-  /**
-   * Recursively finds descendant groups by populating relations.
-   * @static
-   * @param {UserGroupDocument[]} groups
-   * @param {UserDocument} user
-   * @returns UserGroupDocument[]
-   */
-  static async findGroupsWithDescendantsByGroupAndUser(group, user) {
-    const descendantGroups = [group];
-
-    const incrementGroupsRecursively = async(groups, user) => {
-      const groupIds = groups.map(g => g._id);
-
-      const populatedRelations = await this.aggregate([
-        {
-          $match: {
-            relatedUser: user._id,
-          },
-        },
-        {
-          $lookup: {
-            from: 'usergroups',
-            localField: 'relatedGroup',
-            foreignField: '_id',
-            as: 'relatedGroup',
-          },
-        },
-        {
-          $unwind: {
-            path: '$relatedGroup',
-          },
-        },
-        {
-          $match: {
-            'relatedGroup.parent': { $in: groupIds },
-          },
-        },
-      ]);
-
-      const nextGroups = populatedRelations.map(d => d.relatedGroup);
-
-      // End
-      const shouldEnd = nextGroups.length === 0;
-      if (shouldEnd) {
-        return;
-      }
-
-      // Increment
-      descendantGroups.push(...nextGroups);
-
-      return incrementGroupsRecursively(nextGroups, user);
-    };
-
-    await incrementGroupsRecursively([group], user);
-
-    return descendantGroups;
-  }
-
-}
-
-module.exports = function(crowi) {
-  UserGroupRelation.crowi = crowi;
-  schema.loadClass(UserGroupRelation);
-  const model = mongoose.model('UserGroupRelation', schema);
-  return model;
-};

+ 368 - 0
apps/app/src/server/models/user-group-relation.ts

@@ -0,0 +1,368 @@
+import { IUserGroupRelation } from '@growi/core';
+import mongoose, { Model, Schema, Document } from 'mongoose';
+
+import { getOrCreateModel } from '../util/mongoose-utils';
+
+import { UserGroupDocument } from './user-group';
+
+const debug = require('debug')('growi:models:userGroupRelation');
+const mongoosePaginate = require('mongoose-paginate-v2');
+const uniqueValidator = require('mongoose-unique-validator');
+
+const ObjectId = Schema.Types.ObjectId;
+
+export interface UserGroupRelationDocument extends IUserGroupRelation, Document {}
+
+export interface UserGroupRelationModel extends Model<UserGroupRelationDocument> {
+  [x:string]: any, // for old methods
+
+  PAGE_ITEMS: 50,
+
+  removeAllByUserGroups: (groupsToDelete: UserGroupDocument[]) => Promise<any>,
+}
+
+/*
+ * define schema
+ */
+const schema = new Schema<UserGroupRelationDocument, UserGroupRelationModel>({
+  relatedGroup: { type: ObjectId, ref: 'UserGroup', required: true },
+  relatedUser: { type: ObjectId, ref: 'User', required: true },
+}, {
+  timestamps: { createdAt: true, updatedAt: false },
+});
+schema.plugin(mongoosePaginate);
+schema.plugin(uniqueValidator);
+
+/**
+ * remove all invalid relations that has reference to unlinked document
+ */
+schema.statics.removeAllInvalidRelations = function() {
+  return this.findAllRelation()
+    .then((relations) => {
+      // filter invalid documents
+      return relations.filter((relation) => {
+        return relation.relatedUser == null || relation.relatedGroup == null;
+      });
+    })
+    .then((invalidRelations) => {
+      const ids = invalidRelations.map((relation) => { return relation._id });
+      return this.deleteMany({ _id: { $in: ids } });
+    });
+};
+
+/**
+   * find all user and group relation
+   *
+   * @static
+   * @returns {Promise<UserGroupRelation[]>}
+   * @memberof UserGroupRelation
+   */
+schema.statics.findAllRelation = function() {
+  return this
+    .find()
+    .populate('relatedUser')
+    .populate('relatedGroup')
+    .exec();
+};
+
+/**
+ * find all user and group relation of UserGroup
+ *
+ * @static
+ * @param {UserGroup} userGroup
+ * @returns {Promise<UserGroupRelation[]>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.findAllRelationForUserGroup = function(userGroup) {
+  debug('findAllRelationForUserGroup is called', userGroup);
+  return this
+    .find({ relatedGroup: userGroup })
+    .populate('relatedUser')
+    .exec();
+};
+
+schema.statics.findAllUserIdsForUserGroup = async function(userGroup) {
+  const relations = await this
+    .find({ relatedGroup: userGroup })
+    .select('relatedUser')
+    .exec();
+
+  return relations.map(r => r.relatedUser);
+};
+
+/**
+ * find all user and group relation of UserGroups
+ *
+ * @static
+ * @param {UserGroup[]} userGroups
+ * @returns {Promise<UserGroupRelation[]>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.findAllRelationForUserGroups = function(userGroups) {
+  return this
+    .find({ relatedGroup: { $in: userGroups } })
+    .populate('relatedUser')
+    .exec();
+};
+
+/**
+ * find all user and group relation of User
+ *
+ * @static
+ * @param {User} user
+ * @returns {Promise<UserGroupRelation[]>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.findAllRelationForUser = function(user) {
+  return this
+    .find({ relatedUser: user.id })
+    .populate('relatedGroup')
+    // filter documents only relatedGroup is not null
+    .then((userGroupRelations) => {
+      return userGroupRelations.filter((relation) => {
+        return relation.relatedGroup != null;
+      });
+    });
+};
+
+/**
+ * find all UserGroup IDs that related to specified User
+ *
+ * @static
+ * @param {User} user
+ * @returns {Promise<ObjectId[]>}
+ */
+schema.statics.findAllUserGroupIdsRelatedToUser = async function(user) {
+  const relations = await this.find({ relatedUser: user._id })
+    .select('relatedGroup')
+    .exec();
+
+  return relations.map((relation) => { return relation.relatedGroup });
+};
+
+/**
+ * count by related group id and related user
+ *
+ * @static
+ * @param {string} userGroupId find query param for relatedGroup
+ * @param {User} userData find query param for relatedUser
+ * @returns {Promise<number>}
+ */
+schema.statics.countByGroupIdAndUser = async function(userGroupId, userData) {
+  const query = {
+    relatedGroup: userGroupId,
+    relatedUser: userData.id,
+  };
+
+  return this.count(query);
+};
+
+/**
+ * find all "not" related user for UserGroup
+ *
+ * @static
+ * @param {UserGroup} userGroup for find users not related
+ * @returns {Promise<User>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.findUserByNotRelatedGroup = function(userGroup, queryOptions) {
+  const User = mongoose.model('User') as any;
+  let searchWord = new RegExp(`${queryOptions.searchWord}`);
+  switch (queryOptions.searchType) {
+    case 'forward':
+      searchWord = new RegExp(`^${queryOptions.searchWord}`);
+      break;
+    case 'backword':
+      searchWord = new RegExp(`${queryOptions.searchWord}$`);
+      break;
+  }
+  const searthField: Record<string, RegExp>[] = [
+    { username: searchWord },
+  ];
+  if (queryOptions.isAlsoMailSearched === 'true') { searthField.push({ email: searchWord }) }
+  if (queryOptions.isAlsoNameSearched === 'true') { searthField.push({ name: searchWord }) }
+
+  return this.findAllRelationForUserGroup(userGroup)
+    .then((relations) => {
+      const relatedUserIds = relations.map((relation) => {
+        return relation.relatedUser.id;
+      });
+      const query = {
+        _id: { $nin: relatedUserIds },
+        status: User.STATUS_ACTIVE,
+        $or: searthField,
+      };
+
+      debug('findUserByNotRelatedGroup ', query);
+      return User.find(query).exec();
+    });
+};
+
+/**
+ * get if the user has relation for group
+ *
+ * @static
+ * @param {UserGroup} userGroup
+ * @param {User} user
+ * @returns {Promise<boolean>} is user related for group(or not)
+ * @memberof UserGroupRelation
+ */
+schema.statics.isRelatedUserForGroup = function(userGroup, user) {
+  const query = {
+    relatedGroup: userGroup.id,
+    relatedUser: user.id,
+  };
+
+  return this
+    .count(query)
+    .exec()
+    .then((count) => {
+      // return true or false of the relation is exists(not count)
+      return (count > 0);
+    });
+};
+
+/**
+ * create user and group relation
+ *
+ * @static
+ * @param {UserGroup} userGroup
+ * @param {User} user
+ * @returns {Promise<UserGroupRelation>} created relation
+ * @memberof UserGroupRelation
+ */
+schema.statics.createRelation = function(userGroup, user) {
+  return this.create({
+    relatedGroup: userGroup.id,
+    relatedUser: user.id,
+  });
+};
+
+schema.statics.createRelations = async function(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
+ *
+ * @static
+ * @param {UserGroup} userGroup related group for remove
+ * @returns {Promise<any>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.removeAllByUserGroups = function(groupsToDelete: UserGroupDocument[]) {
+  return this.deleteMany({ relatedGroup: { $in: groupsToDelete } });
+};
+
+/**
+ * remove relation by id
+ *
+ * @static
+ * @param {ObjectId} id
+ * @returns {Promise<any>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.removeById = function(id) {
+  return this.findById(id)
+    .then((relationData) => {
+      if (relationData == null) {
+        throw new Error('UserGroupRelation data is not exists. id:', id);
+      }
+      else {
+        relationData.remove();
+      }
+    });
+};
+
+schema.statics.findUserIdsByGroupId = async function(groupId) {
+  const relations = await this.find({ relatedGroup: groupId }, { _id: 0, relatedUser: 1 }).lean().exec(); // .lean() to get not ObjectId but string
+
+  return relations.map(relation => relation.relatedUser);
+};
+
+schema.statics.createByGroupIdsAndUserIds = async function(groupIds, userIds) {
+  const insertOperations: any[] = [];
+
+  groupIds.forEach((groupId) => {
+    userIds.forEach((userId) => {
+      insertOperations.push({
+        insertOne: {
+          document: {
+            relatedGroup: groupId,
+            relatedUser: userId,
+          },
+        },
+      });
+    });
+  });
+
+  await this.bulkWrite(insertOperations);
+};
+
+/**
+ * Recursively finds descendant groups by populating relations.
+ * @static
+ * @param {UserGroupDocument[]} groups
+ * @param {UserDocument} user
+ * @returns UserGroupDocument[]
+ */
+schema.statics.findGroupsWithDescendantsByGroupAndUser = async function(group, user) {
+  const descendantGroups = [group];
+
+  const incrementGroupsRecursively = async(groups, user) => {
+    const groupIds = groups.map(g => g._id);
+
+    const populatedRelations = await this.aggregate([
+      {
+        $match: {
+          relatedUser: user._id,
+        },
+      },
+      {
+        $lookup: {
+          from: 'usergroups',
+          localField: 'relatedGroup',
+          foreignField: '_id',
+          as: 'relatedGroup',
+        },
+      },
+      {
+        $unwind: {
+          path: '$relatedGroup',
+        },
+      },
+      {
+        $match: {
+          'relatedGroup.parent': { $in: groupIds },
+        },
+      },
+    ]);
+
+    const nextGroups = populatedRelations.map(d => d.relatedGroup);
+
+    // End
+    const shouldEnd = nextGroups.length === 0;
+    if (shouldEnd) {
+      return;
+    }
+
+    // Increment
+    descendantGroups.push(...nextGroups);
+
+    return incrementGroupsRecursively(nextGroups, user);
+  };
+
+  await incrementGroupsRecursively([group], user);
+
+  return descendantGroups;
+};
+
+export default getOrCreateModel<UserGroupRelationDocument, UserGroupRelationModel>('UserGroupRelation', schema);

+ 4 - 2
apps/app/src/server/models/user-group.ts

@@ -1,4 +1,4 @@
-import mongoose, {
+import {
   Schema, Model, Document,
 } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
@@ -14,12 +14,14 @@ export interface UserGroupModel extends Model<UserGroupDocument> {
   [x:string]: any, // for old methods
 
   PAGE_ITEMS: 10,
+
+  findGroupsWithDescendantsRecursively: (groups, descendants?) => any,
 }
 
 /*
  * define schema
  */
-const ObjectId = mongoose.Schema.Types.ObjectId;
+const ObjectId = Schema.Types.ObjectId;
 
 const schema = new Schema<UserGroupDocument, UserGroupModel>({
   name: { type: String, required: true, unique: true },

+ 13 - 13
apps/app/src/server/service/user-group.ts

@@ -1,19 +1,15 @@
-import mongoose from 'mongoose';
+import { Model } from 'mongoose';
 
-
-import ExternalUserGroup from '~/features/external-user-group/server/models/external-user-group';
-import ExternalUserGroupRelation from '~/features/external-user-group/server/models/external-user-group-relation';
 import { IUser } from '~/interfaces/user';
 import { ObjectIdLike } from '~/server/interfaces/mongoose-utils';
-import UserGroup from '~/server/models/user-group';
+import UserGroup, { UserGroupDocument, UserGroupModel } from '~/server/models/user-group';
 import { excludeTestIdsFromTargetIds, isIncludesObjectId } from '~/server/util/compare-objectId';
 import loggerFactory from '~/utils/logger';
 
-
-const logger = loggerFactory('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars
+import UserGroupRelation, { UserGroupRelationDocument, UserGroupRelationModel } from '../models/user-group-relation';
 
 
-const UserGroupRelation = mongoose.model('UserGroupRelation') as any; // TODO: Typescriptize model
+const logger = loggerFactory('growi:service:UserGroupService'); // eslint-disable-line no-unused-vars
 
 /**
  * the service class of UserGroupService
@@ -117,10 +113,14 @@ class UserGroupService {
     return userGroup.save();
   }
 
-  async removeCompletelyByRootGroupId(deleteRootGroupId, action, transferToUserGroupId, user, isExternalGroup = false) {
-    const userGroupModel = isExternalGroup ? ExternalUserGroup : UserGroup;
-    const userGroupRelationModel = isExternalGroup ? ExternalUserGroupRelation : UserGroupRelation;
-
+  async removeCompletelyByRootGroupId<
+    D extends UserGroupDocument,
+    RD extends UserGroupRelationDocument,
+  >(
+      deleteRootGroupId, action, transferToUserGroupId, user,
+      userGroupModel: Model<D> & UserGroupModel = UserGroup,
+      userGroupRelationModel: Model<RD> & UserGroupRelationModel = UserGroupRelation,
+  ) {
     const rootGroup = await userGroupModel.findById(deleteRootGroupId);
     if (rootGroup == null) {
       throw new Error(`UserGroup data does not exist. id: ${deleteRootGroupId}`);
@@ -157,4 +157,4 @@ class UserGroupService {
 
 }
 
-module.exports = UserGroupService;
+export default UserGroupService;