|
|
@@ -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);
|