Просмотр исходного кода

configure biome for app/src/server/models

Futa Arai 5 месяцев назад
Родитель
Сommit
9d8ce8176d
48 измененных файлов с 2494 добавлено и 1679 удалено
  1. 1 0
      apps/app/.eslintrc.js
  2. 5 2
      apps/app/src/server/models/GlobalNotificationSetting.ts
  3. 15 8
      apps/app/src/server/models/GlobalNotificationSetting/GlobalNotificationMailSetting.js
  4. 15 8
      apps/app/src/server/models/GlobalNotificationSetting/GlobalNotificationSlackSetting.js
  5. 11 12
      apps/app/src/server/models/GlobalNotificationSetting/index.js
  6. 90 45
      apps/app/src/server/models/access-token.ts
  7. 115 89
      apps/app/src/server/models/activity.ts
  8. 60 41
      apps/app/src/server/models/attachment.ts
  9. 127 62
      apps/app/src/server/models/bookmark-folder.ts
  10. 24 33
      apps/app/src/server/models/bookmark.js
  11. 14 10
      apps/app/src/server/models/config.ts
  12. 12 10
      apps/app/src/server/models/editor-settings.ts
  13. 2 5
      apps/app/src/server/models/errors.ts
  14. 2 1
      apps/app/src/server/models/eslint-rules-dir/no-populate.js
  15. 7 4
      apps/app/src/server/models/eslint-rules-dir/test/no-populate.spec.ts
  16. 97 66
      apps/app/src/server/models/external-account.ts
  17. 15 8
      apps/app/src/server/models/in-app-notification-settings.ts
  18. 80 67
      apps/app/src/server/models/in-app-notification.ts
  19. 12 9
      apps/app/src/server/models/named-query.ts
  20. 296 146
      apps/app/src/server/models/obsolete-page.js
  21. 123 91
      apps/app/src/server/models/page-operation.ts
  22. 45 30
      apps/app/src/server/models/page-redirect.ts
  23. 91 53
      apps/app/src/server/models/page-tag-relation.ts
  24. 415 294
      apps/app/src/server/models/page.ts
  25. 39 34
      apps/app/src/server/models/password-reset-order.ts
  26. 59 35
      apps/app/src/server/models/revision.ts
  27. 5 1
      apps/app/src/server/models/serializers/page-serializer.js
  28. 7 2
      apps/app/src/server/models/serializers/user-group-relation-serializer.js
  29. 21 19
      apps/app/src/server/models/share-link.ts
  30. 23 13
      apps/app/src/server/models/slack-app-integration.js
  31. 103 56
      apps/app/src/server/models/subscription.ts
  32. 18 14
      apps/app/src/server/models/tag.ts
  33. 19 12
      apps/app/src/server/models/transfer-key.ts
  34. 48 35
      apps/app/src/server/models/update-post.ts
  35. 135 100
      apps/app/src/server/models/user-group-relation.ts
  36. 50 30
      apps/app/src/server/models/user-group.ts
  37. 38 30
      apps/app/src/server/models/user-registration-order.ts
  38. 9 11
      apps/app/src/server/models/user-ui-settings.ts
  39. 230 154
      apps/app/src/server/models/user.js
  40. 0 2
      apps/app/src/server/models/vo/collection-progress.ts
  41. 1 6
      apps/app/src/server/models/vo/collection-progressing-status.ts
  42. 2 3
      apps/app/src/server/models/vo/g2g-transfer-error.ts
  43. 1 6
      apps/app/src/server/models/vo/s2c-message.js
  44. 1 3
      apps/app/src/server/models/vo/s2s-message.js
  45. 0 2
      apps/app/src/server/models/vo/search-error.ts
  46. 11 14
      apps/app/src/server/models/vo/slack-command-handler-error.ts
  47. 0 2
      apps/app/src/server/models/vo/v5-conversion-error.ts
  48. 0 1
      biome.json

+ 1 - 0
apps/app/.eslintrc.js

@@ -51,6 +51,7 @@ module.exports = {
     'src/server/crowi/**',
     'src/server/events/**',
     'src/server/interfaces/**',
+    'src/server/models/**',
     'src/server/util/**',
     'src/server/app.ts',
     'src/server/repl.ts',

+ 5 - 2
apps/app/src/server/models/GlobalNotificationSetting.ts

@@ -29,10 +29,13 @@ export const GlobalNotificationSettingType = {
 };
 
 /** @param {import('~/server/crowi').default} crowi Crowi instance */
-const factory = function(crowi) {
+const factory = (crowi) => {
   GlobalNotificationSettingClass.crowi = crowi;
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
-  return mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
+  return mongoose.model(
+    'GlobalNotificationSetting',
+    GlobalNotificationSettingSchema,
+  );
 };
 
 export default factory;

+ 15 - 8
apps/app/src/server/models/GlobalNotificationSetting/GlobalNotificationMailSetting.js

@@ -12,15 +12,22 @@ const factory = (crowi) => {
   GlobalNotificationSettingClass.crowi = crowi;
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
 
-  const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
-  const GlobalNotificationMailSettingModel = GlobalNotificationSettingModel.discriminator(
-    GlobalNotificationSettingType.MAIL,
-    new mongoose.Schema({
-      toEmail: String,
-    }, {
-      discriminatorKey: 'type',
-    }),
+  const GlobalNotificationSettingModel = mongoose.model(
+    'GlobalNotificationSetting',
+    GlobalNotificationSettingSchema,
   );
+  const GlobalNotificationMailSettingModel =
+    GlobalNotificationSettingModel.discriminator(
+      GlobalNotificationSettingType.MAIL,
+      new mongoose.Schema(
+        {
+          toEmail: String,
+        },
+        {
+          discriminatorKey: 'type',
+        },
+      ),
+    );
 
   return GlobalNotificationMailSettingModel;
 };

+ 15 - 8
apps/app/src/server/models/GlobalNotificationSetting/GlobalNotificationSlackSetting.js

@@ -12,15 +12,22 @@ const factory = (crowi) => {
   GlobalNotificationSettingClass.crowi = crowi;
   GlobalNotificationSettingSchema.loadClass(GlobalNotificationSettingClass);
 
-  const GlobalNotificationSettingModel = mongoose.model('GlobalNotificationSetting', GlobalNotificationSettingSchema);
-  const GlobalNotificationSlackSettingModel = GlobalNotificationSettingModel.discriminator(
-    GlobalNotificationSettingType.SLACK,
-    new mongoose.Schema({
-      slackChannels: String,
-    }, {
-      discriminatorKey: 'type',
-    }),
+  const GlobalNotificationSettingModel = mongoose.model(
+    'GlobalNotificationSetting',
+    GlobalNotificationSettingSchema,
   );
+  const GlobalNotificationSlackSettingModel =
+    GlobalNotificationSettingModel.discriminator(
+      GlobalNotificationSettingType.SLACK,
+      new mongoose.Schema(
+        {
+          slackChannels: String,
+        },
+        {
+          discriminatorKey: 'type',
+        },
+      ),
+    );
 
   return GlobalNotificationSlackSettingModel;
 };

+ 11 - 12
apps/app/src/server/models/GlobalNotificationSetting/index.js

@@ -13,8 +13,8 @@ const globalNotificationSettingSchema = new mongoose.Schema({
 });
 
 /*
-* e.g. "/a/b/c" => ["/a/b/c", "/a/b", "/a", "/"]
-*/
+ * e.g. "/a/b/c" => ["/a/b/c", "/a/b", "/a", "/"]
+ */
 const generatePathsOnTree = (path, pathList) => {
   pathList.push(path);
 
@@ -28,8 +28,8 @@ const generatePathsOnTree = (path, pathList) => {
 };
 
 /*
-* e.g. "/a/b/c" => ["/a/b/c", "/a/b", "/a", "/"]
-*/
+ * e.g. "/a/b/c" => ["/a/b/c", "/a/b", "/a", "/"]
+ */
 const generatePathsToMatch = (originalPath) => {
   const pathList = generatePathsOnTree(originalPath, []);
   return pathList.map((path) => {
@@ -48,7 +48,6 @@ const generatePathsToMatch = (originalPath) => {
  * @class GlobalNotificationSetting
  */
 class GlobalNotificationSetting {
-
   /** @type {import('~/server/crowi').default} Crowi instance */
   crowi;
 
@@ -62,7 +61,7 @@ class GlobalNotificationSetting {
    * @param {string} id
    */
   static async enable(id) {
-    const setting = await this.findOne({ _id: id });
+    const setting = await GlobalNotificationSetting.findOne({ _id: id });
 
     setting.isEnabled = true;
     setting.save();
@@ -75,7 +74,7 @@ class GlobalNotificationSetting {
    * @param {string} id
    */
   static async disable(id) {
-    const setting = await this.findOne({ _id: id });
+    const setting = await GlobalNotificationSetting.findOne({ _id: id });
 
     setting.isEnabled = false;
     setting.save();
@@ -87,7 +86,9 @@ class GlobalNotificationSetting {
    * find all notification settings
    */
   static async findAll() {
-    const settings = await this.find().sort({ triggerPath: 1 });
+    const settings = await GlobalNotificationSetting.find().sort({
+      triggerPath: 1,
+    });
 
     return settings;
   }
@@ -100,17 +101,15 @@ class GlobalNotificationSetting {
   static async findSettingByPathAndEvent(event, path, type) {
     const pathsToMatch = generatePathsToMatch(path);
 
-    const settings = await this.find({
+    const settings = await GlobalNotificationSetting.find({
       triggerPath: { $in: pathsToMatch },
       triggerEvents: event,
       __t: type,
       isEnabled: true,
-    })
-      .sort({ triggerPath: 1 });
+    }).sort({ triggerPath: 1 });
 
     return settings;
   }
-
 }
 
 module.exports = {

+ 90 - 45
apps/app/src/server/models/access-token.ts

@@ -1,9 +1,6 @@
+import type { IUserHasId, Ref, Scope } from '@growi/core/dist/interfaces';
 import crypto from 'crypto';
-
-import type { Ref, IUserHasId, Scope } from '@growi/core/dist/interfaces';
-import type {
-  Document, Model, Types, HydratedDocument,
-} from 'mongoose';
+import type { Document, HydratedDocument, Model, Types } from 'mongoose';
 import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
@@ -15,42 +12,58 @@ import { extractScopes } from '../util/scope-utils';
 
 const logger = loggerFactory('growi:models:access-token');
 
-const generateTokenHash = (token: string) => crypto.createHash('sha256').update(token).digest('hex');
+const generateTokenHash = (token: string) =>
+  crypto.createHash('sha256').update(token).digest('hex');
 
 type GenerateTokenResult = {
-  token: string,
-  _id: Types.ObjectId,
-  expiredAt: Date,
-  scopes?: Scope[],
-  description?: string,
-}
+  token: string;
+  _id: Types.ObjectId;
+  expiredAt: Date;
+  scopes?: Scope[];
+  description?: string;
+};
 
 export type IAccessToken = {
-  user: Ref<IUserHasId>,
-  tokenHash: string,
-  expiredAt: Date,
-  scopes?: Scope[],
-  description?: string,
-}
+  user: Ref<IUserHasId>;
+  tokenHash: string;
+  expiredAt: Date;
+  scopes?: Scope[];
+  description?: string;
+};
 
 export interface IAccessTokenDocument extends IAccessToken, Document {
-  isExpired: () => boolean
+  isExpired: () => boolean;
 }
 
 export interface IAccessTokenModel extends Model<IAccessTokenDocument> {
-  generateToken: (userId: Types.ObjectId | string, expiredAt: Date, scopes?: Scope[], description?: string,) => Promise<GenerateTokenResult>
-  deleteToken: (token: string) => Promise<void>
-  deleteTokenById: (tokenId: Types.ObjectId | string) => Promise<void>
-  deleteAllTokensByUserId: (userId: Types.ObjectId | string) => Promise<void>
-  deleteExpiredToken: () => Promise<void>
-  findUserIdByToken: (token: string, requiredScopes: Scope[]) => Promise<HydratedDocument<IAccessTokenDocument> | null>
-  findTokenByUserId: (userId: Types.ObjectId | string) => Promise<HydratedDocument<IAccessTokenDocument>[] | null>
-  validateTokenScopes: (token: string, requiredScopes: Scope[]) => Promise<boolean>
+  generateToken: (
+    userId: Types.ObjectId | string,
+    expiredAt: Date,
+    scopes?: Scope[],
+    description?: string,
+  ) => Promise<GenerateTokenResult>;
+  deleteToken: (token: string) => Promise<void>;
+  deleteTokenById: (tokenId: Types.ObjectId | string) => Promise<void>;
+  deleteAllTokensByUserId: (userId: Types.ObjectId | string) => Promise<void>;
+  deleteExpiredToken: () => Promise<void>;
+  findUserIdByToken: (
+    token: string,
+    requiredScopes: Scope[],
+  ) => Promise<HydratedDocument<IAccessTokenDocument> | null>;
+  findTokenByUserId: (
+    userId: Types.ObjectId | string,
+  ) => Promise<HydratedDocument<IAccessTokenDocument>[] | null>;
+  validateTokenScopes: (
+    token: string,
+    requiredScopes: Scope[],
+  ) => Promise<boolean>;
 }
 
 const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
   user: {
-    type: Schema.Types.ObjectId, ref: 'User', required: true,
+    type: Schema.Types.ObjectId,
+    ref: 'User',
+    required: true,
   },
   tokenHash: { type: String, required: true, unique: true },
   expiredAt: { type: Date, required: true, index: true },
@@ -61,67 +74,99 @@ const accessTokenSchema = new Schema<IAccessTokenDocument, IAccessTokenModel>({
 accessTokenSchema.plugin(mongoosePaginate);
 accessTokenSchema.plugin(uniqueValidator);
 
-accessTokenSchema.statics.generateToken = async function(userId: Types.ObjectId | string, expiredAt: Date, scopes?: Scope[], description?: string) {
-
+accessTokenSchema.statics.generateToken = async function (
+  userId: Types.ObjectId | string,
+  expiredAt: Date,
+  scopes?: Scope[],
+  description?: string,
+) {
   const extractedScopes = extractScopes(scopes ?? []);
   const token = crypto.randomBytes(32).toString('hex');
   const tokenHash = generateTokenHash(token);
 
   try {
     const { _id } = await this.create({
-      user: userId, tokenHash, expiredAt, scopes: extractedScopes, description,
+      user: userId,
+      tokenHash,
+      expiredAt,
+      scopes: extractedScopes,
+      description,
     });
 
     logger.debug('Token generated');
     return {
-      token, _id, expiredAt, scopes: extractedScopes, description,
+      token,
+      _id,
+      expiredAt,
+      scopes: extractedScopes,
+      description,
     };
-  }
-  catch (err) {
+  } catch (err) {
     logger.debug('Failed to generate token');
     throw err;
   }
 };
 
-accessTokenSchema.statics.deleteToken = async function(token: string) {
+accessTokenSchema.statics.deleteToken = async function (token: string) {
   const tokenHash = generateTokenHash(token);
   await this.deleteOne({ tokenHash });
 };
 
-accessTokenSchema.statics.deleteTokenById = async function(tokenId: Types.ObjectId | string) {
+accessTokenSchema.statics.deleteTokenById = async function (
+  tokenId: Types.ObjectId | string,
+) {
   await this.deleteOne({ _id: tokenId });
 };
 
-accessTokenSchema.statics.deleteAllTokensByUserId = async function(userId: Types.ObjectId | string) {
+accessTokenSchema.statics.deleteAllTokensByUserId = async function (
+  userId: Types.ObjectId | string,
+) {
   await this.deleteMany({ user: userId });
 };
 
-accessTokenSchema.statics.deleteExpiredToken = async function() {
+accessTokenSchema.statics.deleteExpiredToken = async function () {
   const now = new Date();
   await this.deleteMany({ expiredAt: { $lt: now } });
 };
 
-accessTokenSchema.statics.findUserIdByToken = async function(token: string, requiredScopes: Scope[]) {
+accessTokenSchema.statics.findUserIdByToken = async function (
+  token: string,
+  requiredScopes: Scope[],
+) {
   const tokenHash = generateTokenHash(token);
   const now = new Date();
   if (requiredScopes.length === 0) {
     return;
   }
   const extractedScopes = extractScopes(requiredScopes);
-  return this.findOne({ tokenHash, expiredAt: { $gte: now }, scopes: { $all: extractedScopes } }).select('user');
+  return this.findOne({
+    tokenHash,
+    expiredAt: { $gte: now },
+    scopes: { $all: extractedScopes },
+  }).select('user');
 };
 
-accessTokenSchema.statics.findTokenByUserId = async function(userId: Types.ObjectId | string) {
+accessTokenSchema.statics.findTokenByUserId = async function (
+  userId: Types.ObjectId | string,
+) {
   const now = new Date();
-  return this.find({ user: userId, expiredAt: { $gte: now } }).select('_id expiredAt scopes description');
+  return this.find({ user: userId, expiredAt: { $gte: now } }).select(
+    '_id expiredAt scopes description',
+  );
 };
 
-accessTokenSchema.statics.validateTokenScopes = async function(token: string, requiredScopes: Scope[]) {
+accessTokenSchema.statics.validateTokenScopes = async function (
+  token: string,
+  requiredScopes: Scope[],
+) {
   return this.findUserIdByToken(token, requiredScopes) != null;
 };
 
-accessTokenSchema.methods.isExpired = function() {
+accessTokenSchema.methods.isExpired = function () {
   return this.expiredAt < new Date();
 };
 
-export const AccessToken = getOrCreateModel<IAccessTokenDocument, IAccessTokenModel>('AccessToken', accessTokenSchema);
+export const AccessToken = getOrCreateModel<
+  IAccessTokenDocument,
+  IAccessTokenModel
+>('AccessToken', accessTokenSchema);

+ 115 - 89
apps/app/src/server/models/activity.ts

@@ -1,41 +1,42 @@
-import type { Ref, IUser } from '@growi/core';
-import type {
-  Types, Document, Model, SortOrder,
-} from 'mongoose';
+import type { IUser, Ref } from '@growi/core';
+import type { Document, Model, SortOrder, Types } from 'mongoose';
 import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 import type {
-  IActivity, ISnapshot, SupportedActionType, SupportedTargetModelType, SupportedEventModelType,
+  IActivity,
+  ISnapshot,
+  SupportedActionType,
+  SupportedEventModelType,
+  SupportedTargetModelType,
 } from '~/interfaces/activity';
 import {
   AllSupportedActions,
-  AllSupportedTargetModels,
   AllSupportedEventModels,
+  AllSupportedTargetModels,
 } from '~/interfaces/activity';
 
 import loggerFactory from '../../utils/logger';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 const logger = loggerFactory('growi:models:activity');
 
 export interface ActivityDocument extends Document {
-  _id: Types.ObjectId
-  user: Ref<IUser>
-  ip: string
-  endpoint: string
-  targetModel: SupportedTargetModelType
-  target: Types.ObjectId
-  eventModel: SupportedEventModelType
-  event: Types.ObjectId
-  action: SupportedActionType
-  snapshot: ISnapshot
+  _id: Types.ObjectId;
+  user: Ref<IUser>;
+  ip: string;
+  endpoint: string;
+  targetModel: SupportedTargetModelType;
+  target: Types.ObjectId;
+  eventModel: SupportedEventModelType;
+  event: Types.ObjectId;
+  action: SupportedActionType;
+  snapshot: ISnapshot;
 }
 
 export interface ActivityModel extends Model<ActivityDocument> {
-  [x:string]: any
-  getActionUsersFromActivities(activities: ActivityDocument[]): any[]
+  [x: string]: any;
+  getActionUsersFromActivities(activities: ActivityDocument[]): any[];
 }
 
 const snapshotSchema = new Schema<ISnapshot>({
@@ -43,91 +44,116 @@ const snapshotSchema = new Schema<ISnapshot>({
 });
 
 // TODO: add revision id
-const activitySchema = new Schema<ActivityDocument, ActivityModel>({
-  user: {
-    type: Schema.Types.ObjectId,
-    ref: 'User',
-    index: true,
-  },
-  ip: {
-    type: String,
-  },
-  endpoint: {
-    type: String,
-  },
-  targetModel: {
-    type: String,
-    enum: AllSupportedTargetModels,
-  },
-  target: {
-    type: Schema.Types.ObjectId,
-    refPath: 'targetModel',
-  },
-  eventModel: {
-    type: String,
-    enum: AllSupportedEventModels,
-  },
-  event: {
-    type: Schema.Types.ObjectId,
+const activitySchema = new Schema<ActivityDocument, ActivityModel>(
+  {
+    user: {
+      type: Schema.Types.ObjectId,
+      ref: 'User',
+      index: true,
+    },
+    ip: {
+      type: String,
+    },
+    endpoint: {
+      type: String,
+    },
+    targetModel: {
+      type: String,
+      enum: AllSupportedTargetModels,
+    },
+    target: {
+      type: Schema.Types.ObjectId,
+      refPath: 'targetModel',
+    },
+    eventModel: {
+      type: String,
+      enum: AllSupportedEventModels,
+    },
+    event: {
+      type: Schema.Types.ObjectId,
+    },
+    action: {
+      type: String,
+      enum: AllSupportedActions,
+      required: true,
+    },
+    snapshot: snapshotSchema,
   },
-  action: {
-    type: String,
-    enum: AllSupportedActions,
-    required: true,
+  {
+    timestamps: {
+      createdAt: true,
+      updatedAt: false,
+    },
   },
-  snapshot: snapshotSchema,
-}, {
-  timestamps: {
-    createdAt: true,
-    updatedAt: false,
-  },
-});
+);
 // activitySchema.index({ createdAt: 1 }); // Do not create index here because it is created by ActivityService as TTL index
 activitySchema.index({ target: 1, action: 1 });
-activitySchema.index({
-  user: 1, target: 1, action: 1, createdAt: 1,
-}, { unique: true });
+activitySchema.index(
+  {
+    user: 1,
+    target: 1,
+    action: 1,
+    createdAt: 1,
+  },
+  { unique: true },
+);
 activitySchema.plugin(mongoosePaginate);
 
-activitySchema.post('save', function() {
+activitySchema.post('save', function () {
   logger.debug('activity has been created', this);
 });
 
-activitySchema.statics.createByParameters = async function(parameters): Promise<IActivity> {
-  const activity = await this.create(parameters) as unknown as IActivity;
+activitySchema.statics.createByParameters = async function (
+  parameters,
+): Promise<IActivity> {
+  const activity = (await this.create(parameters)) as unknown as IActivity;
 
   return activity;
 };
 
 // When using this method, ensure that activity updates are allowed using ActivityService.shoudUpdateActivity
-activitySchema.statics.updateByParameters = async function(activityId: string, parameters): Promise<ActivityDocument | null> {
-  const activity = await this.findOneAndUpdate({ _id: activityId }, parameters, { new: true }).exec();
+activitySchema.statics.updateByParameters = async function (
+  activityId: string,
+  parameters,
+): Promise<ActivityDocument | null> {
+  const activity = await this.findOneAndUpdate(
+    { _id: activityId },
+    parameters,
+    { new: true },
+  ).exec();
 
   return activity;
 };
 
-activitySchema.statics.findSnapshotUsernamesByUsernameRegexWithTotalCount = async function(
-    q: string, option: { sortOpt: SortOrder, offset: number, limit: number},
-): Promise<{usernames: string[], totalCount: number}> {
-  const opt = option || {};
-  const sortOpt = opt.sortOpt || 1;
-  const offset = opt.offset || 0;
-  const limit = opt.limit || 10;
-
-  const conditions = { 'snapshot.username': { $regex: q, $options: 'i' } };
-
-  const usernames = await this.aggregate()
-    .skip(0)
-    .limit(10000) // Narrow down the search target
-    .match(conditions)
-    .group({ _id: '$snapshot.username' })
-    .sort({ _id: sortOpt }) // Sort "snapshot.username" in ascending order
-    .skip(offset)
-    .limit(limit);
-
-  const totalCount = (await this.find(conditions).distinct('snapshot.username')).length;
-
-  return { usernames: usernames.map(r => r._id), totalCount };
-};
-
-export default getOrCreateModel<ActivityDocument, ActivityModel>('Activity', activitySchema);
+activitySchema.statics.findSnapshotUsernamesByUsernameRegexWithTotalCount =
+  async function (
+    q: string,
+    option: { sortOpt: SortOrder; offset: number; limit: number },
+  ): Promise<{ usernames: string[]; totalCount: number }> {
+    const opt = option || {};
+    const sortOpt = opt.sortOpt || 1;
+    const offset = opt.offset || 0;
+    const limit = opt.limit || 10;
+
+    const conditions = { 'snapshot.username': { $regex: q, $options: 'i' } };
+
+    const usernames = await this.aggregate()
+      .skip(0)
+      .limit(10000) // Narrow down the search target
+      .match(conditions)
+      .group({ _id: '$snapshot.username' })
+      .sort({ _id: sortOpt }) // Sort "snapshot.username" in ascending order
+      .skip(offset)
+      .limit(limit);
+
+    const totalCount = (
+      await this.find(conditions).distinct('snapshot.username')
+    ).length;
+
+    return { usernames: usernames.map((r) => r._id), totalCount };
+  };
+
+export default getOrCreateModel<ActivityDocument, ActivityModel>(
+  'Activity',
+  activitySchema,
+);

+ 60 - 41
apps/app/src/server/models/attachment.ts

@@ -1,12 +1,9 @@
-import path from 'path';
-
 import type { IAttachment } from '@growi/core';
 import { addSeconds } from 'date-fns/addSeconds';
-import {
-  Schema, type Model, type Document,
-} from 'mongoose';
+import { type Document, type Model, Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
+import path from 'path';
 
 import loggerFactory from '~/utils/logger';
 
@@ -16,7 +13,6 @@ import { getOrCreateModel } from '../util/mongoose-utils';
 // eslint-disable-next-line no-unused-vars
 const logger = loggerFactory('growi:models:attachment');
 
-
 function generateFileHash(fileName) {
   const hash = require('crypto').createHash('md5');
   hash.update(`${fileName}_${Date.now()}`);
@@ -25,65 +21,78 @@ function generateFileHash(fileName) {
 }
 
 type GetValidTemporaryUrl = () => string | null | undefined;
-type CashTemporaryUrlByProvideSec = (temporaryUrl: string, lifetimeSec: number) => Promise<IAttachmentDocument>;
+type CashTemporaryUrlByProvideSec = (
+  temporaryUrl: string,
+  lifetimeSec: number,
+) => Promise<IAttachmentDocument>;
 
 export interface IAttachmentDocument extends IAttachment, Document {
-  getValidTemporaryUrl: GetValidTemporaryUrl
-  cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec,
+  getValidTemporaryUrl: GetValidTemporaryUrl;
+  cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec;
 }
 export interface IAttachmentModel extends Model<IAttachmentDocument> {
   createWithoutSave: (
-    pageId, user, originalName: string, fileFormat: string, fileSize: number, attachmentType: AttachmentType,
+    pageId,
+    user,
+    originalName: string,
+    fileFormat: string,
+    fileSize: number,
+    attachmentType: AttachmentType,
   ) => IAttachmentDocument;
 }
 
-const attachmentSchema = new Schema({
-  page: { type: Schema.Types.ObjectId, ref: 'Page', index: true },
-  creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
-  filePath: { type: String }, // DEPRECATED: remains for backward compatibility for v3.3.x or below
-  fileName: { type: String, required: true, unique: true },
-  fileFormat: { type: String, required: true },
-  fileSize: { type: Number, default: 0 },
-  originalName: { type: String },
-  temporaryUrlCached: { type: String },
-  temporaryUrlExpiredAt: { type: Date },
-  attachmentType: {
-    type: String,
-    enum: AttachmentType,
-    required: true,
+const attachmentSchema = new Schema(
+  {
+    page: { type: Schema.Types.ObjectId, ref: 'Page', index: true },
+    creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
+    filePath: { type: String }, // DEPRECATED: remains for backward compatibility for v3.3.x or below
+    fileName: { type: String, required: true, unique: true },
+    fileFormat: { type: String, required: true },
+    fileSize: { type: Number, default: 0 },
+    originalName: { type: String },
+    temporaryUrlCached: { type: String },
+    temporaryUrlExpiredAt: { type: Date },
+    attachmentType: {
+      type: String,
+      enum: AttachmentType,
+      required: true,
+    },
   },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
+  {
+    timestamps: { createdAt: true, updatedAt: false },
+  },
+);
 attachmentSchema.plugin(uniqueValidator);
 attachmentSchema.plugin(mongoosePaginate);
 
 // virtual
-attachmentSchema.virtual('filePathProxied').get(function() {
+attachmentSchema.virtual('filePathProxied').get(function () {
   return `/attachment/${this._id}`;
 });
 
-attachmentSchema.virtual('downloadPathProxied').get(function() {
+attachmentSchema.virtual('downloadPathProxied').get(function () {
   return `/download/${this._id}`;
 });
 
 attachmentSchema.set('toObject', { virtuals: true });
 attachmentSchema.set('toJSON', { virtuals: true });
 
-
-attachmentSchema.statics.createWithoutSave = function(
-    pageId, user, originalName: string, fileFormat: string, fileSize: number, attachmentType: AttachmentType,
+attachmentSchema.statics.createWithoutSave = function (
+  pageId,
+  user,
+  originalName: string,
+  fileFormat: string,
+  fileSize: number,
+  attachmentType: AttachmentType,
 ) {
-  // eslint-disable-next-line @typescript-eslint/no-this-alias
-  const Attachment = this;
-
   const extname = path.extname(originalName);
   let fileName = generateFileHash(originalName);
-  if (extname.length > 1) { // ignore if empty or '.' only
+  if (extname.length > 1) {
+    // ignore if empty or '.' only
     fileName = `${fileName}${extname}`;
   }
 
-  const attachment = new Attachment();
+  const attachment = new this();
   attachment.page = pageId;
   attachment.creator = user._id;
   attachment.originalName = originalName;
@@ -94,7 +103,9 @@ attachmentSchema.statics.createWithoutSave = function(
   return attachment;
 };
 
-const getValidTemporaryUrl: GetValidTemporaryUrl = function(this: IAttachmentDocument) {
+const getValidTemporaryUrl: GetValidTemporaryUrl = function (
+  this: IAttachmentDocument,
+) {
   if (this.temporaryUrlExpiredAt == null) {
     return null;
   }
@@ -106,7 +117,11 @@ const getValidTemporaryUrl: GetValidTemporaryUrl = function(this: IAttachmentDoc
 };
 attachmentSchema.methods.getValidTemporaryUrl = getValidTemporaryUrl;
 
-const cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec = function(this: IAttachmentDocument, temporaryUrl, lifetimeSec) {
+const cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec = function (
+  this: IAttachmentDocument,
+  temporaryUrl,
+  lifetimeSec,
+) {
   if (temporaryUrl == null) {
     throw new Error('url is required.');
   }
@@ -115,6 +130,10 @@ const cashTemporaryUrlByProvideSec: CashTemporaryUrlByProvideSec = function(this
 
   return this.save();
 };
-attachmentSchema.methods.cashTemporaryUrlByProvideSec = cashTemporaryUrlByProvideSec;
+attachmentSchema.methods.cashTemporaryUrlByProvideSec =
+  cashTemporaryUrlByProvideSec;
 
-export const Attachment = getOrCreateModel<IAttachmentDocument, IAttachmentModel>('Attachment', attachmentSchema);
+export const Attachment = getOrCreateModel<
+  IAttachmentDocument,
+  IAttachmentModel
+>('Attachment', attachmentSchema);

+ 127 - 62
apps/app/src/server/models/bookmark-folder.ts

@@ -1,54 +1,80 @@
 import type { IPageHasId } from '@growi/core';
 import { objectIdUtils } from '@growi/core/dist/utils';
-import type { Types, Document, Model } from 'mongoose';
+import type { Document, Model, Types } from 'mongoose';
 import monggoose, { Schema } from 'mongoose';
 
-import type { BookmarkFolderItems, IBookmarkFolder } from '~/interfaces/bookmark-info';
+import type {
+  BookmarkFolderItems,
+  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');
 const Bookmark = monggoose.model('Bookmark');
 
 export interface BookmarkFolderDocument extends Document {
-  _id: Types.ObjectId
-  name: string
-  owner: Types.ObjectId
-  parent?: Types.ObjectId | undefined
-  bookmarks?: Types.ObjectId[],
-  childFolder?: BookmarkFolderDocument[]
+  _id: Types.ObjectId;
+  name: string;
+  owner: Types.ObjectId;
+  parent?: Types.ObjectId | undefined;
+  bookmarks?: Types.ObjectId[];
+  childFolder?: BookmarkFolderDocument[];
 }
 
-export interface BookmarkFolderModel extends Model<BookmarkFolderDocument>{
-  createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>
-  deleteFolderAndChildren(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}>
-  updateBookmarkFolder(bookmarkFolderId: string, name: string, parent: string | null, childFolder: BookmarkFolderItems[]): Promise<BookmarkFolderDocument>
-  insertOrUpdateBookmarkedPage(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null): Promise<BookmarkFolderDocument | null>
-  updateBookmark(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId| string): Promise<BookmarkFolderDocument | null>
+export interface BookmarkFolderModel extends Model<BookmarkFolderDocument> {
+  createByParameters(params: IBookmarkFolder): Promise<BookmarkFolderDocument>;
+  deleteFolderAndChildren(
+    bookmarkFolderId: Types.ObjectId | string,
+  ): Promise<{ deletedCount: number }>;
+  updateBookmarkFolder(
+    bookmarkFolderId: string,
+    name: string,
+    parent: string | null,
+    childFolder: BookmarkFolderItems[],
+  ): Promise<BookmarkFolderDocument>;
+  insertOrUpdateBookmarkedPage(
+    pageId: IPageHasId,
+    userId: Types.ObjectId | string,
+    folderId: string | null,
+  ): Promise<BookmarkFolderDocument | null>;
+  updateBookmark(
+    pageId: Types.ObjectId | string,
+    status: boolean,
+    userId: Types.ObjectId | string,
+  ): Promise<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,
+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,
+    },
+    bookmarks: {
+      type: [
+        {
+          type: Schema.Types.ObjectId,
+          ref: 'Bookmark',
+          required: false,
+        },
+      ],
+      required: false,
+      default: [],
+    },
   },
-  bookmarks: {
-    type: [{
-      type: Schema.Types.ObjectId, ref: 'Bookmark', required: false,
-    }],
-    required: false,
-    default: [],
+  {
+    toObject: { virtuals: true },
   },
-}, {
-  toObject: { virtuals: true },
-});
+);
 
 bookmarkFolderSchema.virtual('childFolder', {
   ref: 'BookmarkFolder',
@@ -56,16 +82,19 @@ bookmarkFolderSchema.virtual('childFolder', {
   foreignField: 'parent',
 });
 
-bookmarkFolderSchema.statics.createByParameters = async function(params: IBookmarkFolder): Promise<BookmarkFolderDocument> {
+bookmarkFolderSchema.statics.createByParameters = async function (
+  params: IBookmarkFolder,
+): Promise<BookmarkFolderDocument> {
   const { name, owner, parent } = params;
   let bookmarkFolder: BookmarkFolderDocument;
 
   if (parent == null) {
     bookmarkFolder = await this.create({ name, owner });
-  }
-  else {
+  } else {
     // Check if parent folder id is valid and parent folder exists
-    const isParentFolderIdValid = objectIdUtils.isValidObjectId(parent as string);
+    const isParentFolderIdValid = objectIdUtils.isValidObjectId(
+      parent as string,
+    );
 
     if (!isParentFolderIdValid) {
       throw new InvalidParentBookmarkFolderError('Parent folder id is invalid');
@@ -74,13 +103,19 @@ bookmarkFolderSchema.statics.createByParameters = async function(params: IBookma
     if (parentFolder == null) {
       throw new InvalidParentBookmarkFolderError('Parent folder not found');
     }
-    bookmarkFolder = await this.create({ name, owner, parent:  parentFolder._id });
+    bookmarkFolder = await this.create({
+      name,
+      owner,
+      parent: parentFolder._id,
+    });
   }
 
   return bookmarkFolder;
 };
 
-bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFolderId: Types.ObjectId | string): Promise<{deletedCount: number}> {
+bookmarkFolderSchema.statics.deleteFolderAndChildren = async function (
+  bookmarkFolderId: Types.ObjectId | string,
+): Promise<{ deletedCount: number }> {
   const bookmarkFolder = await this.findById(bookmarkFolderId);
   // Delete parent and all children folder
   let deletedCount = 0;
@@ -92,10 +127,14 @@ bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFo
     }
     // Delete all child recursively and update deleted count
     const childFolders = await this.find({ parent: bookmarkFolder._id });
-    await Promise.all(childFolders.map(async(child) => {
-      const deletedChildFolder = await this.deleteFolderAndChildren(child._id);
-      deletedCount += deletedChildFolder.deletedCount;
-    }));
+    await Promise.all(
+      childFolders.map(async (child) => {
+        const deletedChildFolder = await this.deleteFolderAndChildren(
+          child._id,
+        );
+        deletedCount += deletedChildFolder.deletedCount;
+      }),
+    );
     const deletedChild = await this.deleteMany({ parent: bookmarkFolder._id });
     deletedCount += deletedChild.deletedCount + 1;
     bookmarkFolder.delete();
@@ -103,14 +142,13 @@ bookmarkFolderSchema.statics.deleteFolderAndChildren = async function(bookmarkFo
   return { deletedCount };
 };
 
-bookmarkFolderSchema.statics.updateBookmarkFolder = async function(
-    bookmarkFolderId: string,
-    name: string,
-    parentId: string | null,
-    childFolder: BookmarkFolderItems[],
-):
- Promise<BookmarkFolderDocument> {
-  const updateFields: {name: string, parent: Types.ObjectId | null} = {
+bookmarkFolderSchema.statics.updateBookmarkFolder = async function (
+  bookmarkFolderId: string,
+  name: string,
+  parentId: string | null,
+  childFolder: BookmarkFolderItems[],
+): Promise<BookmarkFolderDocument> {
+  const updateFields: { name: string; parent: Types.ObjectId | null } = {
     name: '',
     parent: null,
   };
@@ -131,21 +169,33 @@ bookmarkFolderSchema.statics.updateBookmarkFolder = async function(
     }
   }
 
-  const bookmarkFolder = await this.findByIdAndUpdate(bookmarkFolderId, { $set: updateFields }, { new: true });
+  const bookmarkFolder = await this.findByIdAndUpdate(
+    bookmarkFolderId,
+    { $set: updateFields },
+    { new: true },
+  );
   if (bookmarkFolder == null) {
     throw new Error('Update bookmark folder failed');
   }
   return bookmarkFolder;
-
 };
 
-bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function(pageId: IPageHasId, userId: Types.ObjectId | string, folderId: string | null):
-Promise<BookmarkFolderDocument | null> {
+bookmarkFolderSchema.statics.insertOrUpdateBookmarkedPage = async function (
+  pageId: IPageHasId,
+  userId: Types.ObjectId | string,
+  folderId: string | null,
+): Promise<BookmarkFolderDocument | null> {
   // Find bookmark
-  const bookmarkedPage = await Bookmark.findOne({ page: pageId, user: userId }, { new: true, upsert: true });
+  const bookmarkedPage = await Bookmark.findOne(
+    { page: pageId, user: userId },
+    { new: true, upsert: true },
+  );
 
   // Remove existing bookmark in bookmark folder
-  await this.updateMany({ owner: userId }, { $pull: { bookmarks:  bookmarkedPage?._id } });
+  await this.updateMany(
+    { owner: userId },
+    { $pull: { bookmarks: bookmarkedPage?._id } },
+  );
   if (folderId == null) {
     return null;
   }
@@ -160,14 +210,26 @@ Promise<BookmarkFolderDocument | null> {
   return bookmarkFolder;
 };
 
-bookmarkFolderSchema.statics.updateBookmark = async function(pageId: Types.ObjectId | string, status: boolean, userId: Types.ObjectId | string):
-Promise<BookmarkFolderDocument | null> {
+bookmarkFolderSchema.statics.updateBookmark = async function (
+  pageId: Types.ObjectId | string,
+  status: boolean,
+  userId: Types.ObjectId | string,
+): Promise<BookmarkFolderDocument | null> {
   // If isBookmarked
   if (status) {
-    const bookmarkedPage = await Bookmark.findOne({ page: pageId, user: userId });
-    const bookmarkFolder = await this.findOne({ owner: userId, bookmarks: { $in: [bookmarkedPage?._id] } });
+    const bookmarkedPage = await Bookmark.findOne({
+      page: pageId,
+      user: userId,
+    });
+    const bookmarkFolder = await this.findOne({
+      owner: userId,
+      bookmarks: { $in: [bookmarkedPage?._id] },
+    });
     if (bookmarkFolder != null) {
-      await this.updateOne({ owner: userId, _id: bookmarkFolder._id }, { $pull: { bookmarks:  bookmarkedPage?._id } });
+      await this.updateOne(
+        { owner: userId, _id: bookmarkFolder._id },
+        { $pull: { bookmarks: bookmarkedPage?._id } },
+      );
     }
 
     if (bookmarkedPage) {
@@ -179,4 +241,7 @@ Promise<BookmarkFolderDocument | null> {
   await Bookmark.create({ page: pageId, user: userId });
   return null;
 };
-export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>('BookmarkFolder', bookmarkFolderSchema);
+export default getOrCreateModel<BookmarkFolderDocument, BookmarkFolderModel>(
+  'BookmarkFolder',
+  bookmarkFolderSchema,
+);

+ 24 - 33
apps/app/src/server/models/bookmark.js

@@ -16,25 +16,27 @@ const factory = (crowi) => {
 
   let bookmarkSchema = null;
 
-
-  bookmarkSchema = new mongoose.Schema({
-    page: { type: ObjectId, ref: 'Page', index: true },
-    user: { type: ObjectId, ref: 'User', index: true },
-  }, {
-    timestamps: { createdAt: true, updatedAt: false },
-  });
+  bookmarkSchema = new mongoose.Schema(
+    {
+      page: { type: ObjectId, ref: 'Page', index: true },
+      user: { type: ObjectId, ref: 'User', index: true },
+    },
+    {
+      timestamps: { createdAt: true, updatedAt: false },
+    },
+  );
   bookmarkSchema.index({ page: 1, user: 1 }, { unique: true });
   bookmarkSchema.plugin(mongoosePaginate);
   bookmarkSchema.plugin(uniqueValidator);
 
-  bookmarkSchema.statics.countByPageId = async function(pageId) {
+  bookmarkSchema.statics.countByPageId = async function (pageId) {
     return await this.count({ page: pageId });
   };
 
   /**
    * @return {object} key: page._id, value: bookmark count
    */
-  bookmarkSchema.statics.getPageIdToCountMap = async function(pageIds) {
+  bookmarkSchema.statics.getPageIdToCountMap = async function (pageIds) {
     const results = await this.aggregate()
       .match({ page: { $in: pageIds } })
       .group({ _id: '$page', count: { $sum: 1 } });
@@ -49,31 +51,26 @@ const factory = (crowi) => {
   };
 
   // bookmark チェック用
-  bookmarkSchema.statics.findByPageIdAndUserId = function(pageId, userId) {
-    const Bookmark = this;
-
-    return new Promise(((resolve, reject) => {
-      return Bookmark.findOne({ page: pageId, user: userId }, (err, doc) => {
+  bookmarkSchema.statics.findByPageIdAndUserId = function (pageId, userId) {
+    return new Promise((resolve, reject) => {
+      return this.findOne({ page: pageId, user: userId }, (err, doc) => {
         if (err) {
           return reject(err);
         }
 
         return resolve(doc);
       });
-    }));
+    });
   };
 
-  bookmarkSchema.statics.add = async function(page, user) {
-    const Bookmark = this;
-
-    const newBookmark = new Bookmark({ page, user });
+  bookmarkSchema.statics.add = async function (page, user) {
+    const newBookmark = new this({ page, user });
 
     try {
       const bookmark = await newBookmark.save();
       bookmarkEvent.emit('create', page._id);
       return bookmark;
-    }
-    catch (err) {
+    } catch (err) {
       if (err.code === 11000) {
         // duplicate key (dummy response of new object)
         return newBookmark;
@@ -88,29 +85,23 @@ const factory = (crowi) => {
    * used only when removing the page
    * @param {string} pageId
    */
-  bookmarkSchema.statics.removeBookmarksByPageId = async function(pageId) {
-    const Bookmark = this;
-
+  bookmarkSchema.statics.removeBookmarksByPageId = async function (pageId) {
     try {
-      const data = await Bookmark.remove({ page: pageId });
+      const data = await this.remove({ page: pageId });
       bookmarkEvent.emit('delete', pageId);
       return data;
-    }
-    catch (err) {
+    } catch (err) {
       logger.debug('Bookmark.remove failed (removeBookmarkByPage)', err);
       throw err;
     }
   };
 
-  bookmarkSchema.statics.removeBookmark = async function(pageId, user) {
-    const Bookmark = this;
-
+  bookmarkSchema.statics.removeBookmark = async function (pageId, user) {
     try {
-      const data = await Bookmark.findOneAndRemove({ page: pageId, user });
+      const data = await this.findOneAndRemove({ page: pageId, user });
       bookmarkEvent.emit('delete', pageId);
       return data;
-    }
-    catch (err) {
+    } catch (err) {
       logger.debug('Bookmark.findOneAndRemove failed', err);
       throw err;
     }

+ 14 - 10
apps/app/src/server/models/config.ts

@@ -4,7 +4,6 @@ import uniqueValidator from 'mongoose-unique-validator';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface IConfig {
   _id: Types.ObjectId;
   ns: string;
@@ -13,15 +12,20 @@ export interface IConfig {
   createdAt: Date;
 }
 
-
-const schema = new Schema<IConfig>({
-  ns: { type: String },
-  key: { type: String, required: true, unique: true },
-  value: { type: String, required: true },
-}, {
-  timestamps: true,
-});
+const schema = new Schema<IConfig>(
+  {
+    ns: { type: String },
+    key: { type: String, required: true, unique: true },
+    value: { type: String, required: true },
+  },
+  {
+    timestamps: true,
+  },
+);
 
 schema.plugin(uniqueValidator);
 
-export const Config = getOrCreateModel<IConfig, Record<string, never>>('Config', schema);
+export const Config = getOrCreateModel<IConfig, Record<string, never>>(
+  'Config',
+  schema,
+);

+ 12 - 10
apps/app/src/server/models/editor-settings.ts

@@ -1,18 +1,18 @@
 import type { EditorSettings } from '@growi/editor';
-import type { Model, Document } from 'mongoose';
-import {
-  Schema,
-} from 'mongoose';
+import type { Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface EditorSettingsDocument extends EditorSettings, Document {
-  userId: Schema.Types.ObjectId,
+  userId: Schema.Types.ObjectId;
 }
-export type EditorSettingsModel = Model<EditorSettingsDocument>
+export type EditorSettingsModel = Model<EditorSettingsDocument>;
 
-const editorSettingsSchema = new Schema<EditorSettingsDocument, EditorSettingsModel>({
+const editorSettingsSchema = new Schema<
+  EditorSettingsDocument,
+  EditorSettingsModel
+>({
   userId: { type: Schema.Types.ObjectId },
   theme: { type: String },
   keymapMode: { type: String },
@@ -20,5 +20,7 @@ const editorSettingsSchema = new Schema<EditorSettingsDocument, EditorSettingsMo
   autoFormatMarkdownTable: { type: Boolean, default: true },
 });
 
-
-export default getOrCreateModel<EditorSettingsDocument, EditorSettingsModel>('EditorSettings', editorSettingsSchema);
+export default getOrCreateModel<EditorSettingsDocument, EditorSettingsModel>(
+  'EditorSettings',
+  editorSettingsSchema,
+);

+ 2 - 5
apps/app/src/server/models/errors.ts

@@ -1,20 +1,17 @@
 import ExtensibleCustomError from 'extensible-custom-error';
 
 export class PathAlreadyExistsError extends ExtensibleCustomError {
-
   targetPath: string;
 
   constructor(message: string, targetPath: string) {
     super(message);
     this.targetPath = targetPath;
   }
-
 }
 
-
 /*
-* User Authentication
-*/
+ * User Authentication
+ */
 export class NullUsernameToBeRegisteredError extends ExtensibleCustomError {}
 
 // Invalid Parent bookmark folder error

+ 2 - 1
apps/app/src/server/models/eslint-rules-dir/no-populate.js

@@ -18,7 +18,8 @@ module.exports = {
         if (node.callee.property && node.callee.property.name === 'populate') {
           context.report({
             node,
-            message: "The 'populate' method should not be called in model modules.",
+            message:
+              "The 'populate' method should not be called in model modules.",
           });
         }
       },

+ 7 - 4
apps/app/src/server/models/eslint-rules-dir/test/no-populate.spec.ts

@@ -10,13 +10,16 @@ const ruleTester = new RuleTester({
 
 test('test no-populate', () => {
   ruleTester.run('no-populate', noPopulate, {
-    valid: [
-      { code: 'Model.find();' },
-    ],
+    valid: [{ code: 'Model.find();' }],
     invalid: [
       {
         code: "Model.find().populate('children');",
-        errors: [{ message: "The 'populate' method should not be called in model modules." }],
+        errors: [
+          {
+            message:
+              "The 'populate' method should not be called in model modules.",
+          },
+        ],
       },
     ],
   });

+ 97 - 66
apps/app/src/server/models/external-account.ts

@@ -1,7 +1,11 @@
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
-import type { IUser, IUserHasId, IExternalAccount } from '@growi/core/dist/interfaces';
-import type { Model, Document, HydratedDocument } from 'mongoose';
+import type {
+  IExternalAccount,
+  IUser,
+  IUserHasId,
+} from '@growi/core/dist/interfaces';
+import type { Document, HydratedDocument, Model } from 'mongoose';
 import mongoose, { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
@@ -14,19 +18,24 @@ import { getOrCreateModel } from '../util/mongoose-utils';
 
 const logger = loggerFactory('growi:models:external-account');
 
-export interface ExternalAccountDocument extends IExternalAccount<IExternalAuthProviderType>, Document {}
+export interface ExternalAccountDocument
+  extends IExternalAccount<IExternalAuthProviderType>,
+    Document {}
 
 export interface ExternalAccountModel extends Model<ExternalAccountDocument> {
-  [x:string]: any, // for old methods
+  [x: string]: any; // for old methods
 }
 
-const schema = new Schema<ExternalAccountDocument, ExternalAccountModel>({
-  providerType: { type: String, required: true },
-  accountId: { type: String, required: true },
-  user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
+const schema = new Schema<ExternalAccountDocument, ExternalAccountModel>(
+  {
+    providerType: { type: String, required: true },
+    accountId: { type: String, required: true },
+    user: { type: Schema.Types.ObjectId, ref: 'User', required: true },
+  },
+  {
+    timestamps: { createdAt: true, updatedAt: false },
+  },
+);
 // compound index
 schema.index({ providerType: 1, accountId: 1 }, { unique: true });
 // apply plugins
@@ -44,7 +53,6 @@ const DEFAULT_LIMIT = 50;
  * @class DuplicatedUsernameException
  */
 class DuplicatedUsernameException {
-
   name: string;
 
   message: string;
@@ -56,72 +64,92 @@ class DuplicatedUsernameException {
     this.message = message;
     this.user = user;
   }
-
 }
 
 /**
  * find an account or register if not found
  */
-schema.statics.findOrRegister = function(
-    isSameUsernameTreatedAsIdenticalUser: boolean,
-    isSameEmailTreatedAsIdenticalUser: boolean,
-    providerType: string,
-    accountId: string,
-    usernameToBeRegistered?: string,
-    nameToBeRegistered?: string,
-    mailToBeRegistered?: string,
+schema.statics.findOrRegister = function (
+  isSameUsernameTreatedAsIdenticalUser: boolean,
+  isSameEmailTreatedAsIdenticalUser: boolean,
+  providerType: string,
+  accountId: string,
+  usernameToBeRegistered?: string,
+  nameToBeRegistered?: string,
+  mailToBeRegistered?: string,
 ): Promise<HydratedDocument<IExternalAccount<IExternalAuthProviderType>>> {
-  return this.findOne({ providerType, accountId })
-    .then((account) => {
+  return this.findOne({ providerType, accountId }).then((account) => {
     // ExternalAccount is found
-      if (account != null) {
-        logger.debug(`ExternalAccount '${accountId}' is found `, account);
-        return account;
-      }
-
-      if (usernameToBeRegistered == null) {
-        throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
-      }
-
-      const User = mongoose.model<HydratedDocument<IUser>, Model<IUser> & { createUser, STATUS_ACTIVE }>('User');
-
-      let promise = User.findOne({ username: usernameToBeRegistered }).exec();
-      if (isSameUsernameTreatedAsIdenticalUser && isSameEmailTreatedAsIdenticalUser) {
-        promise = promise
-          .then((user) => {
-            if (user == null) { return User.findOne({ email: mailToBeRegistered }) }
-            return user;
-          });
-      }
-      else if (isSameEmailTreatedAsIdenticalUser) {
-        promise = User.findOne({ email: mailToBeRegistered }).exec();
-      }
-
-      return promise
-        .then((user) => {
+    if (account != null) {
+      logger.debug(`ExternalAccount '${accountId}' is found `, account);
+      return account;
+    }
+
+    if (usernameToBeRegistered == null) {
+      throw new NullUsernameToBeRegisteredError('username_should_not_be_null');
+    }
+
+    const User = mongoose.model<
+      HydratedDocument<IUser>,
+      Model<IUser> & { createUser; STATUS_ACTIVE }
+    >('User');
+
+    let promise = User.findOne({ username: usernameToBeRegistered }).exec();
+    if (
+      isSameUsernameTreatedAsIdenticalUser &&
+      isSameEmailTreatedAsIdenticalUser
+    ) {
+      promise = promise.then((user) => {
+        if (user == null) {
+          return User.findOne({ email: mailToBeRegistered });
+        }
+        return user;
+      });
+    } else if (isSameEmailTreatedAsIdenticalUser) {
+      promise = User.findOne({ email: mailToBeRegistered }).exec();
+    }
+
+    return promise
+      .then((user) => {
         // when the User that have the same `username` exists
-          if (user != null) {
-            throw new DuplicatedUsernameException(`User '${usernameToBeRegistered}' already exists`, user);
-          }
-          if (nameToBeRegistered == null) {
+        if (user != null) {
+          throw new DuplicatedUsernameException(
+            `User '${usernameToBeRegistered}' already exists`,
+            user,
+          );
+        }
+        if (nameToBeRegistered == null) {
           // eslint-disable-next-line no-param-reassign
-            nameToBeRegistered = '';
-          }
-
-          // create a new User with STATUS_ACTIVE
-          logger.debug(`ExternalAccount '${accountId}' is not found, it is going to be registered.`);
-          return User.createUser(nameToBeRegistered, usernameToBeRegistered, mailToBeRegistered, undefined, undefined, User.STATUS_ACTIVE);
-        })
-        .then((newUser) => {
-          return this.associate(providerType, accountId, newUser);
-        });
-    });
+          nameToBeRegistered = '';
+        }
+
+        // create a new User with STATUS_ACTIVE
+        logger.debug(
+          `ExternalAccount '${accountId}' is not found, it is going to be registered.`,
+        );
+        return User.createUser(
+          nameToBeRegistered,
+          usernameToBeRegistered,
+          mailToBeRegistered,
+          undefined,
+          undefined,
+          User.STATUS_ACTIVE,
+        );
+      })
+      .then((newUser) => {
+        return this.associate(providerType, accountId, newUser);
+      });
+  });
 };
 
 /**
  * Create ExternalAccount document and associate to existing User
  */
-schema.statics.associate = function(providerType: string, accountId: string, user: IUserHasId) {
+schema.statics.associate = function (
+  providerType: string,
+  accountId: string,
+  user: IUserHasId,
+) {
   return this.create({ providerType, accountId, user: user._id });
 };
 
@@ -135,7 +163,7 @@ schema.statics.associate = function(providerType: string, accountId: string, use
  * @returns {Promise<any>} mongoose-paginate result object
  * @memberof ExternalAccount
  */
-schema.statics.findAllWithPagination = function(opts) {
+schema.statics.findAllWithPagination = function (opts) {
   const query = {};
   const options = Object.assign({ populate: 'user' }, opts);
   if (options.sort == null) {
@@ -148,4 +176,7 @@ schema.statics.findAllWithPagination = function(opts) {
   return this.paginate(query, options);
 };
 
-export default getOrCreateModel<ExternalAccountDocument, ExternalAccountModel>('ExternalAccount', schema);
+export default getOrCreateModel<ExternalAccountDocument, ExternalAccountModel>(
+  'ExternalAccount',
+  schema,
+);

+ 15 - 8
apps/app/src/server/models/in-app-notification-settings.ts

@@ -1,17 +1,21 @@
-import type { Model, Document, Types } from 'mongoose';
-import {
-  Schema,
-} from 'mongoose';
+import type { Document, Model, Types } from 'mongoose';
+import { Schema } from 'mongoose';
 
 import type { IInAppNotificationSettings } from '~/interfaces/in-app-notification';
 import { subscribeRuleNames } from '~/interfaces/in-app-notification';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-export interface InAppNotificationSettingsDocument extends IInAppNotificationSettings<Types.ObjectId>, Document {}
-export type InAppNotificationSettingsModel = Model<InAppNotificationSettingsDocument>
+export interface InAppNotificationSettingsDocument
+  extends IInAppNotificationSettings<Types.ObjectId>,
+    Document {}
+export type InAppNotificationSettingsModel =
+  Model<InAppNotificationSettingsDocument>;
 
-const inAppNotificationSettingsSchema = new Schema<InAppNotificationSettingsDocument, InAppNotificationSettingsModel>({
+const inAppNotificationSettingsSchema = new Schema<
+  InAppNotificationSettingsDocument,
+  InAppNotificationSettingsModel
+>({
   userId: { type: Schema.Types.ObjectId },
   subscribeRules: [
     {
@@ -22,4 +26,7 @@ const inAppNotificationSettingsSchema = new Schema<InAppNotificationSettingsDocu
 });
 
 // eslint-disable-next-line max-len
-export default getOrCreateModel<InAppNotificationSettingsDocument, InAppNotificationSettingsModel>('InAppNotificationSettings', inAppNotificationSettingsSchema);
+export default getOrCreateModel<
+  InAppNotificationSettingsDocument,
+  InAppNotificationSettingsModel
+>('InAppNotificationSettings', inAppNotificationSettingsSchema);

+ 80 - 67
apps/app/src/server/models/in-app-notification.ts

@@ -1,82 +1,93 @@
-import type { Types, Document, Model } from 'mongoose';
+import type { Document, Model, Types } from 'mongoose';
 import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
-import { AllSupportedTargetModels, AllSupportedActions } from '~/interfaces/activity';
+import {
+  AllSupportedActions,
+  AllSupportedTargetModels,
+} from '~/interfaces/activity';
 import { InAppNotificationStatuses } from '~/interfaces/in-app-notification';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
-
 import type { ActivityDocument } from './activity';
 
-
 const { STATUS_UNOPENED, STATUS_OPENED } = InAppNotificationStatuses;
 
 export interface InAppNotificationDocument extends Document {
-  _id: Types.ObjectId
-  user: Types.ObjectId
-  targetModel: string
-  target: Types.ObjectId
-  action: string
-  activities: ActivityDocument[]
-  status: string
-  createdAt: Date
-  snapshot: string
+  _id: Types.ObjectId;
+  user: Types.ObjectId;
+  targetModel: string;
+  target: Types.ObjectId;
+  action: string;
+  activities: ActivityDocument[];
+  status: string;
+  createdAt: Date;
+  snapshot: string;
 }
 
+export interface InAppNotificationModel
+  extends Model<InAppNotificationDocument> {
+  findLatestInAppNotificationsByUser(
+    user: Types.ObjectId,
+    skip: number,
+    offset: number,
+  );
+  getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>;
+  open(user, id: Types.ObjectId): Promise<InAppNotificationDocument | null>;
+  read(user) /* : Promise<Query<any>> */;
 
-export interface InAppNotificationModel extends Model<InAppNotificationDocument> {
-  findLatestInAppNotificationsByUser(user: Types.ObjectId, skip: number, offset: number)
-  getUnreadCountByUser(user: Types.ObjectId): Promise<number | undefined>
-  open(user, id: Types.ObjectId): Promise<InAppNotificationDocument | null>
-  read(user) /* : Promise<Query<any>> */
-
-  STATUS_UNOPENED: string
-  STATUS_OPENED: string
+  STATUS_UNOPENED: string;
+  STATUS_OPENED: string;
 }
 
-const inAppNotificationSchema = new Schema<InAppNotificationDocument, InAppNotificationModel>({
-  user: {
-    type: Schema.Types.ObjectId,
-    ref: 'User',
-    index: true,
-    required: true,
-  },
-  targetModel: {
-    type: String,
-    required: true,
-    enum: AllSupportedTargetModels,
-  },
-  target: {
-    type: Schema.Types.ObjectId,
-    refPath: 'targetModel',
-    required: true,
-  },
-  action: {
-    type: String,
-    required: true,
-    enum: AllSupportedActions,
-  },
-  activities: [
-    {
+const inAppNotificationSchema = new Schema<
+  InAppNotificationDocument,
+  InAppNotificationModel
+>(
+  {
+    user: {
+      type: Schema.Types.ObjectId,
+      ref: 'User',
+      index: true,
+      required: true,
+    },
+    targetModel: {
+      type: String,
+      required: true,
+      enum: AllSupportedTargetModels,
+    },
+    target: {
       type: Schema.Types.ObjectId,
-      ref: 'Activity',
+      refPath: 'targetModel',
+      required: true,
+    },
+    action: {
+      type: String,
+      required: true,
+      enum: AllSupportedActions,
+    },
+    activities: [
+      {
+        type: Schema.Types.ObjectId,
+        ref: 'Activity',
+      },
+    ],
+    status: {
+      type: String,
+      default: STATUS_UNOPENED,
+      enum: InAppNotificationStatuses,
+      index: true,
+      required: true,
+    },
+    snapshot: {
+      type: String,
+      required: true,
     },
-  ],
-  status: {
-    type: String,
-    default: STATUS_UNOPENED,
-    enum: InAppNotificationStatuses,
-    index: true,
-    required: true,
   },
-  snapshot: {
-    type: String,
-    required: true,
+  {
+    timestamps: { createdAt: true, updatedAt: false },
   },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
+);
 // indexes
 inAppNotificationSchema.index({ createdAt: 1 });
 // apply plugins
@@ -88,16 +99,18 @@ const transform = (doc, ret) => {
 inAppNotificationSchema.set('toObject', { virtuals: true, transform });
 inAppNotificationSchema.set('toJSON', { virtuals: true, transform });
 inAppNotificationSchema.index({
-  user: 1, target: 1, action: 1, createdAt: 1,
+  user: 1,
+  target: 1,
+  action: 1,
+  createdAt: 1,
 });
 
-inAppNotificationSchema.statics.STATUS_UNOPENED = function() {
-  return STATUS_UNOPENED;
-};
-inAppNotificationSchema.statics.STATUS_OPENED = function() {
-  return STATUS_OPENED;
-};
+inAppNotificationSchema.statics.STATUS_UNOPENED = () => STATUS_UNOPENED;
+inAppNotificationSchema.statics.STATUS_OPENED = () => STATUS_OPENED;
 
-const InAppNotification = getOrCreateModel<InAppNotificationDocument, InAppNotificationModel>('InAppNotification', inAppNotificationSchema);
+const InAppNotification = getOrCreateModel<
+  InAppNotificationDocument,
+  InAppNotificationModel
+>('InAppNotification', inAppNotificationSchema);
 
 export { InAppNotification };

+ 12 - 9
apps/app/src/server/models/named-query.ts

@@ -1,9 +1,7 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import type { Model, Document } from 'mongoose';
-import {
-  Schema,
-} from 'mongoose';
+import type { Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 
 import type { INamedQuery } from '~/interfaces/named-query';
 import { SearchDelegatorName } from '~/interfaces/named-query';
@@ -11,24 +9,26 @@ import { SearchDelegatorName } from '~/interfaces/named-query';
 import loggerFactory from '../../utils/logger';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
 const logger = loggerFactory('growi:models:named-query');
 
 export interface NamedQueryDocument extends INamedQuery, Document {}
 
-export type NamedQueryModel = Model<NamedQueryDocument>
+export type NamedQueryModel = Model<NamedQueryDocument>;
 
 const schema = new Schema<NamedQueryDocument, NamedQueryModel>({
   name: { type: String, required: true, unique: true },
   aliasOf: { type: String },
   delegatorName: { type: String, enum: SearchDelegatorName },
   creator: {
-    type: Schema.Types.ObjectId, ref: 'User', index: true, default: null,
+    type: Schema.Types.ObjectId,
+    ref: 'User',
+    index: true,
+    default: null,
   },
 });
 
-schema.pre('validate', async function(this, next) {
+schema.pre('validate', async function (this, next) {
   if (this.aliasOf == null && this.delegatorName == null) {
     throw Error('Either of aliasOf and delegatorNameName must not be null.');
   }
@@ -36,4 +36,7 @@ schema.pre('validate', async function(this, next) {
   next();
 });
 
-export default getOrCreateModel<NamedQueryDocument, NamedQueryModel>('NamedQuery', schema);
+export default getOrCreateModel<NamedQueryDocument, NamedQueryModel>(
+  'NamedQuery',
+  schema,
+);

+ 296 - 146
apps/app/src/server/models/obsolete-page.js

@@ -1,5 +1,9 @@
 import { GroupType, Origin } from '@growi/core';
-import { templateChecker, pagePathUtils, pathUtils } from '@growi/core/dist/utils';
+import {
+  pagePathUtils,
+  pathUtils,
+  templateChecker,
+} from '@growi/core/dist/utils';
 import { differenceInYears } from 'date-fns/differenceInYears';
 import escapeStringRegexp from 'escape-string-regexp';
 
@@ -9,13 +13,11 @@ import ExternalUserGroupRelation from '~/features/external-user-group/server/mod
 import loggerFactory from '~/utils/logger';
 
 import { configManager } from '../service/config-manager';
-
 import UserGroup from './user-group';
 import UserGroupRelation from './user-group-relation';
 
 const logger = loggerFactory('growi:models:page');
 
-
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
 
@@ -72,17 +74,25 @@ export const extractToAncestorsPaths = (pagePath) => {
  * @param {boolean} shouldExcludeBody boolean indicating whether to include 'revision.body' or not
  */
 /* eslint-disable object-curly-newline, object-property-newline */
-export const populateDataToShowRevision = (page, userPublicFields, shouldExcludeBody = false) => {
-  return page
-    .populate([
-      { path: 'lastUpdateUser', select: userPublicFields },
-      { path: 'creator', select: userPublicFields },
-      { path: 'deleteUser', select: userPublicFields },
-      { path: 'grantedGroups.item' },
-      { path: 'revision', select: shouldExcludeBody ? '-body' : undefined, populate: {
-        path: 'author', select: userPublicFields,
-      } },
-    ]);
+export const populateDataToShowRevision = (
+  page,
+  userPublicFields,
+  shouldExcludeBody = false,
+) => {
+  return page.populate([
+    { path: 'lastUpdateUser', select: userPublicFields },
+    { path: 'creator', select: userPublicFields },
+    { path: 'deleteUser', select: userPublicFields },
+    { path: 'grantedGroups.item' },
+    {
+      path: 'revision',
+      select: shouldExcludeBody ? '-body' : undefined,
+      populate: {
+        path: 'author',
+        select: userPublicFields,
+      },
+    },
+  ]);
 };
 /* eslint-enable object-curly-newline, object-property-newline */
 
@@ -101,15 +111,17 @@ export const getPageSchema = (crowi) => {
 
   function validateCrowi() {
     if (crowi == null) {
-      throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
+      throw new Error(
+        '"crowi" is null. Init User model with "crowi" argument first.',
+      );
     }
   }
 
-  pageSchema.methods.isDeleted = function() {
+  pageSchema.methods.isDeleted = function () {
     return isTrashPage(this.path);
   };
 
-  pageSchema.methods.isPublic = function() {
+  pageSchema.methods.isPublic = function () {
     if (!this.grant || this.grant === GRANT_PUBLIC) {
       return true;
     }
@@ -117,15 +129,15 @@ export const getPageSchema = (crowi) => {
     return false;
   };
 
-  pageSchema.methods.isTopPage = function() {
+  pageSchema.methods.isTopPage = function () {
     return isTopPage(this.path);
   };
 
-  pageSchema.methods.isTemplate = function() {
+  pageSchema.methods.isTemplate = function () {
     return checkTemplatePath(this.path);
   };
 
-  pageSchema.methods.isLatestRevision = function() {
+  pageSchema.methods.isLatestRevision = function () {
     // populate されていなくて判断できない
     if (!this.latestRevision || !this.revision) {
       return true;
@@ -133,19 +145,30 @@ export const getPageSchema = (crowi) => {
 
     // comparing ObjectId with string
     // eslint-disable-next-line eqeqeq
-    return (this.latestRevision == this.revision._id.toString());
+    return this.latestRevision == this.revision._id.toString();
   };
 
-  pageSchema.methods.findRelatedTagsById = async function() {
+  pageSchema.methods.findRelatedTagsById = async function () {
     const PageTagRelation = mongoose.model('PageTagRelation');
-    const relations = await PageTagRelation.find({ relatedPage: this._id }).populate('relatedTag');
-    return relations.map((relation) => { return relation.relatedTag.name });
+    const relations = await PageTagRelation.find({
+      relatedPage: this._id,
+    }).populate('relatedTag');
+    return relations.map((relation) => {
+      return relation.relatedTag.name;
+    });
   };
 
-  pageSchema.methods.isUpdatable = async function(previousRevision, origin) {
-    const populatedPageDataWithRevisionOrigin = await this.populate('revision', 'origin');
-    const latestRevisionOrigin = populatedPageDataWithRevisionOrigin.revision.origin;
-    const ignoreLatestRevision = origin === Origin.Editor && (latestRevisionOrigin === Origin.Editor || latestRevisionOrigin === Origin.View);
+  pageSchema.methods.isUpdatable = async function (previousRevision, origin) {
+    const populatedPageDataWithRevisionOrigin = await this.populate(
+      'revision',
+      'origin',
+    );
+    const latestRevisionOrigin =
+      populatedPageDataWithRevisionOrigin.revision.origin;
+    const ignoreLatestRevision =
+      origin === Origin.Editor &&
+      (latestRevisionOrigin === Origin.Editor ||
+        latestRevisionOrigin === Origin.View);
     if (ignoreLatestRevision) {
       return true;
     }
@@ -159,7 +182,7 @@ export const getPageSchema = (crowi) => {
     return true;
   };
 
-  pageSchema.methods.isLiked = function(user) {
+  pageSchema.methods.isLiked = function (user) {
     if (user == null || user._id == null) {
       return false;
     }
@@ -169,53 +192,47 @@ export const getPageSchema = (crowi) => {
     });
   };
 
-  pageSchema.methods.like = function(userData) {
-    const self = this;
-
-    return new Promise(((resolve, reject) => {
-      const added = self.liker.addToSet(userData._id);
+  pageSchema.methods.like = function (userData) {
+    return new Promise((resolve, reject) => {
+      const added = this.liker.addToSet(userData._id);
       if (added.length > 0) {
-        self.save((err, data) => {
+        this.save((err, data) => {
           if (err) {
             return reject(err);
           }
           logger.debug('liker updated!', added);
           return resolve(data);
         });
-      }
-      else {
+      } else {
         logger.debug('liker not updated');
         return reject(new Error('Already liked'));
       }
-    }));
+    });
   };
 
-  pageSchema.methods.unlike = function(userData, callback) {
-    const self = this;
-
-    return new Promise(((resolve, reject) => {
-      const beforeCount = self.liker.length;
-      self.liker.pull(userData._id);
-      if (self.liker.length !== beforeCount) {
-        self.save((err, data) => {
+  pageSchema.methods.unlike = function (userData, callback) {
+    return new Promise((resolve, reject) => {
+      const beforeCount = this.liker.length;
+      this.liker.pull(userData._id);
+      if (this.liker.length !== beforeCount) {
+        this.save((err, data) => {
           if (err) {
             return reject(err);
           }
           return resolve(data);
         });
-      }
-      else {
+      } else {
         logger.debug('liker not updated');
         return reject(new Error('Already unliked'));
       }
-    }));
+    });
   };
 
-  pageSchema.methods.isSeenUser = function(userData) {
+  pageSchema.methods.isSeenUser = function (userData) {
     return this.seenUsers.includes(userData._id);
   };
 
-  pageSchema.methods.seen = async function(userData) {
+  pageSchema.methods.seen = async function (userData) {
     if (this.isSeenUser(userData)) {
       logger.debug('seenUsers not updated');
       return this;
@@ -234,27 +251,35 @@ export const getPageSchema = (crowi) => {
     return saved;
   };
 
-  pageSchema.methods.updateSlackChannels = function(slackChannels) {
+  pageSchema.methods.updateSlackChannels = function (slackChannels) {
     this.slackChannels = slackChannels;
 
     return this.save();
   };
 
-  pageSchema.methods.initLatestRevisionField = async function(revisionId) {
+  pageSchema.methods.initLatestRevisionField = async function (revisionId) {
     this.latestRevision = this.revision;
     if (revisionId != null) {
       this.revision = revisionId;
     }
   };
 
-  pageSchema.methods.populateDataToShowRevision = async function(shouldExcludeBody) {
+  pageSchema.methods.populateDataToShowRevision = async function (
+    shouldExcludeBody,
+  ) {
     validateCrowi();
 
     const User = crowi.model('User');
-    return populateDataToShowRevision(this, User.USER_FIELDS_EXCEPT_CONFIDENTIAL, shouldExcludeBody);
+    return populateDataToShowRevision(
+      this,
+      User.USER_FIELDS_EXCEPT_CONFIDENTIAL,
+      shouldExcludeBody,
+    );
   };
 
-  pageSchema.methods.populateDataToMakePresentation = async function(revisionId) {
+  pageSchema.methods.populateDataToMakePresentation = async function (
+    revisionId,
+  ) {
     this.latestRevision = this.revision;
     if (revisionId != null) {
       this.revision = revisionId;
@@ -262,7 +287,7 @@ export const getPageSchema = (crowi) => {
     return this.populate('revision');
   };
 
-  pageSchema.methods.applyScope = function(user, grant, grantUserGroupIds) {
+  pageSchema.methods.applyScope = function (user, grant, grantUserGroupIds) {
     // Reset
     this.grantedUsers = [];
     this.grantedGroups = [];
@@ -278,29 +303,25 @@ export const getPageSchema = (crowi) => {
     }
   };
 
-  pageSchema.methods.getContentAge = function() {
+  pageSchema.methods.getContentAge = function () {
     return differenceInYears(new Date(), this.updatedAt);
   };
 
-
-  pageSchema.statics.updateCommentCount = function(pageId) {
+  pageSchema.statics.updateCommentCount = function (pageId) {
     validateCrowi();
-
-    const self = this;
-    return Comment.countCommentByPageId(pageId)
-      .then((count) => {
-        self.update({ _id: pageId }, { commentCount: count }, {}, (err, data) => {
-          if (err) {
-            logger.debug('Update commentCount Error', err);
-            throw err;
-          }
-
-          return data;
-        });
+    return Comment.countCommentByPageId(pageId).then((count) => {
+      this.update({ _id: pageId }, { commentCount: count }, {}, (err, data) => {
+        if (err) {
+          logger.debug('Update commentCount Error', err);
+          throw err;
+        }
+
+        return data;
       });
+    });
   };
 
-  pageSchema.statics.getDeletedPageName = function(path) {
+  pageSchema.statics.getDeletedPageName = (path) => {
     if (path.match('/')) {
       // eslint-disable-next-line no-param-reassign
       path = path.substr(1);
@@ -308,16 +329,12 @@ export const getPageSchema = (crowi) => {
     return `/trash/${path}`;
   };
 
-  pageSchema.statics.getRevertDeletedPageName = function(path) {
-    return path.replace('/trash', '');
-  };
+  pageSchema.statics.getRevertDeletedPageName = (path) =>
+    path.replace('/trash', '');
 
-  pageSchema.statics.fixToCreatableName = function(path) {
-    return path
-      .replace(/\/\//g, '/');
-  };
+  pageSchema.statics.fixToCreatableName = (path) => path.replace(/\/\//g, '/');
 
-  pageSchema.statics.updateRevision = function(pageId, revisionId, cb) {
+  pageSchema.statics.updateRevision = function (pageId, revisionId, cb) {
     this.update({ _id: pageId }, { revision: revisionId }, {}, (err, data) => {
       cb(err, data);
     });
@@ -328,13 +345,18 @@ export const getPageSchema = (crowi) => {
    * @param {string} id ObjectId
    * @param {User} user
    */
-  pageSchema.statics.isAccessiblePageByViewer = async function(id, user) {
+  pageSchema.statics.isAccessiblePageByViewer = async function (id, user) {
     const baseQuery = this.count({ _id: id });
 
-    const userGroups = user != null ? [
-      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-    ] : [];
+    const userGroups =
+      user != null
+        ? [
+            ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+            ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
+              user,
+            )),
+          ]
+        : [];
 
     const queryBuilder = new this.PageQueryBuilder(baseQuery);
     queryBuilder.addConditionToFilteringByViewer(user, userGroups, true);
@@ -348,13 +370,23 @@ export const getPageSchema = (crowi) => {
    * @param {User} user User instance
    * @param {UserGroup[]} userGroups List of UserGroup instances
    */
-  pageSchema.statics.findByIdAndViewer = async function(id, user, userGroups, includeEmpty = false) {
+  pageSchema.statics.findByIdAndViewer = async function (
+    id,
+    user,
+    userGroups,
+    includeEmpty = false,
+  ) {
     const baseQuery = this.findOne({ _id: id });
 
-    const relatedUserGroups = (user != null && userGroups == null) ? [
-      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-    ] : userGroups;
+    const relatedUserGroups =
+      user != null && userGroups == null
+        ? [
+            ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+            ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
+              user,
+            )),
+          ]
+        : userGroups;
 
     const queryBuilder = new this.PageQueryBuilder(baseQuery, includeEmpty);
     queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups, true);
@@ -363,12 +395,15 @@ export const getPageSchema = (crowi) => {
   };
 
   // find page by path
-  pageSchema.statics.findByPath = function(path, includeEmpty = false) {
+  pageSchema.statics.findByPath = function (path, includeEmpty = false) {
     if (path == null) {
       return null;
     }
 
-    const builder = new this.PageQueryBuilder(this.findOne({ path }), includeEmpty);
+    const builder = new this.PageQueryBuilder(
+      this.findOne({ path }),
+      includeEmpty,
+    );
 
     return builder.query.exec();
   };
@@ -378,7 +413,12 @@ export const getPageSchema = (crowi) => {
    * @param {User} user User instance
    * @param {UserGroup[]} userGroups List of UserGroup instances
    */
-  pageSchema.statics.findAncestorByPathAndViewer = async function(path, user, userGroups, includeEmpty = false) {
+  pageSchema.statics.findAncestorByPathAndViewer = async function (
+    path,
+    user,
+    userGroups,
+    includeEmpty = false,
+  ) {
     if (path == null) {
       throw new Error('path is required.');
     }
@@ -390,12 +430,19 @@ export const getPageSchema = (crowi) => {
     const ancestorsPaths = extractToAncestorsPaths(path);
 
     // pick the longest one
-    const baseQuery = this.findOne({ path: { $in: ancestorsPaths } }).sort({ path: -1 });
+    const baseQuery = this.findOne({ path: { $in: ancestorsPaths } }).sort({
+      path: -1,
+    });
 
-    const relatedUserGroups = (user != null && userGroups == null) ? [
-      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-    ] : userGroups;
+    const relatedUserGroups =
+      user != null && userGroups == null
+        ? [
+            ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+            ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
+              user,
+            )),
+          ]
+        : userGroups;
 
     const queryBuilder = new this.PageQueryBuilder(baseQuery, includeEmpty);
     queryBuilder.addConditionToFilteringByViewer(user, relatedUserGroups);
@@ -406,7 +453,12 @@ export const getPageSchema = (crowi) => {
   /**
    * find pages that is match with `path` and its descendants
    */
-  pageSchema.statics.findListWithDescendants = async function(path, user, option = {}, includeEmpty = false) {
+  pageSchema.statics.findListWithDescendants = async function (
+    path,
+    user,
+    option = {},
+    includeEmpty = false,
+  ) {
     const builder = new this.PageQueryBuilder(this.find(), includeEmpty);
     builder.addConditionToListWithDescendants(path, option);
 
@@ -416,7 +468,12 @@ export const getPageSchema = (crowi) => {
   /**
    * find pages that is match with `path` and its descendants which user is able to manage
    */
-  pageSchema.statics.findManageableListWithDescendants = async function(page, user, option = {}, includeEmpty = false) {
+  pageSchema.statics.findManageableListWithDescendants = async function (
+    page,
+    user,
+    option = {},
+    includeEmpty = false,
+  ) {
     if (user == null) {
       return null;
     }
@@ -427,7 +484,12 @@ export const getPageSchema = (crowi) => {
     // add grant conditions
     await addConditionToFilteringByViewerToEdit(builder, user);
 
-    const { pages } = await findListFromBuilderAndViewer(builder, user, false, option);
+    const { pages } = await findListFromBuilderAndViewer(
+      builder,
+      user,
+      false,
+      option,
+    );
 
     // add page if 'grant' is GRANT_RESTRICTED
     // because addConditionToListWithDescendants excludes GRANT_RESTRICTED pages
@@ -441,7 +503,12 @@ export const getPageSchema = (crowi) => {
   /**
    * find pages that start with `path`
    */
-  pageSchema.statics.findListByStartWith = async function(path, user, option, includeEmpty = false) {
+  pageSchema.statics.findListByStartWith = async function (
+    path,
+    user,
+    option,
+    includeEmpty = false,
+  ) {
     const builder = new this.PageQueryBuilder(this.find(), includeEmpty);
     builder.addConditionToListByStartWith(path, option);
 
@@ -455,16 +522,27 @@ export const getPageSchema = (crowi) => {
    * @param {User} currentUser
    * @param {any} option
    */
-  pageSchema.statics.findListByCreator = async function(targetUser, currentUser, option) {
+  pageSchema.statics.findListByCreator = async function (
+    targetUser,
+    currentUser,
+    option,
+  ) {
     const opt = Object.assign({ sort: 'createdAt', desc: -1 }, option);
-    const builder = new this.PageQueryBuilder(this.find({ creator: targetUser._id }));
+    const builder = new this.PageQueryBuilder(
+      this.find({ creator: targetUser._id }),
+    );
 
     let showAnyoneKnowsLink = null;
     if (targetUser != null && currentUser != null) {
       showAnyoneKnowsLink = targetUser._id.equals(currentUser._id);
     }
 
-    return await findListFromBuilderAndViewer(builder, currentUser, showAnyoneKnowsLink, opt);
+    return await findListFromBuilderAndViewer(
+      builder,
+      currentUser,
+      showAnyoneKnowsLink,
+      opt,
+    );
   };
 
   /**
@@ -474,7 +552,12 @@ export const getPageSchema = (crowi) => {
    * @param {boolean} showAnyoneKnowsLink
    * @param {any} option
    */
-  async function findListFromBuilderAndViewer(builder, user, showAnyoneKnowsLink, option) {
+  async function findListFromBuilderAndViewer(
+    builder,
+    user,
+    showAnyoneKnowsLink,
+    option,
+  ) {
     validateCrowi();
 
     const User = crowi.model('User');
@@ -488,7 +571,11 @@ export const getPageSchema = (crowi) => {
     }
 
     // add grant conditions
-    await addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink);
+    await addConditionToFilteringByViewerForList(
+      builder,
+      user,
+      showAnyoneKnowsLink,
+    );
 
     // count
     const totalCount = await builder.query.exec('count');
@@ -498,7 +585,10 @@ export const getPageSchema = (crowi) => {
     builder.populateDataToList(User.USER_FIELDS_EXCEPT_CONFIDENTIAL);
     const pages = await builder.query.lean().clone().exec('find');
     const result = {
-      pages, totalCount, offset: opt.offset, limit: opt.limit,
+      pages,
+      totalCount,
+      offset: opt.offset,
+      limit: opt.limit,
     };
     return result;
   }
@@ -511,20 +601,39 @@ export const getPageSchema = (crowi) => {
    * @param {User} user
    * @param {boolean} showAnyoneKnowsLink
    */
-  async function addConditionToFilteringByViewerForList(builder, user, showAnyoneKnowsLink) {
+  async function addConditionToFilteringByViewerForList(
+    builder,
+    user,
+    showAnyoneKnowsLink,
+  ) {
     validateCrowi();
 
     // determine User condition
-    const hidePagesRestrictedByOwner = configManager.getConfig('security:list-policy:hideRestrictedByOwner');
-    const hidePagesRestrictedByGroup = configManager.getConfig('security:list-policy:hideRestrictedByGroup');
+    const hidePagesRestrictedByOwner = configManager.getConfig(
+      'security:list-policy:hideRestrictedByOwner',
+    );
+    const hidePagesRestrictedByGroup = configManager.getConfig(
+      'security:list-policy:hideRestrictedByGroup',
+    );
 
     // determine UserGroup condition
-    const userGroups = user != null ? [
-      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-    ] : null;
-
-    return builder.addConditionToFilteringByViewer(user, userGroups, showAnyoneKnowsLink, !hidePagesRestrictedByOwner, !hidePagesRestrictedByGroup);
+    const userGroups =
+      user != null
+        ? [
+            ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+            ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
+              user,
+            )),
+          ]
+        : null;
+
+    return builder.addConditionToFilteringByViewer(
+      user,
+      userGroups,
+      showAnyoneKnowsLink,
+      !hidePagesRestrictedByOwner,
+      !hidePagesRestrictedByGroup,
+    );
   }
 
   /**
@@ -537,46 +646,67 @@ export const getPageSchema = (crowi) => {
    */
   async function addConditionToFilteringByViewerToEdit(builder, user) {
     // determine UserGroup condition
-    const userGroups = user != null ? [
-      ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-      ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
-    ] : null;
-
-    return builder.addConditionToFilteringByViewer(user, userGroups, false, false, false);
+    const userGroups =
+      user != null
+        ? [
+            ...(await UserGroupRelation.findAllUserGroupIdsRelatedToUser(user)),
+            ...(await ExternalUserGroupRelation.findAllUserGroupIdsRelatedToUser(
+              user,
+            )),
+          ]
+        : null;
+
+    return builder.addConditionToFilteringByViewer(
+      user,
+      userGroups,
+      false,
+      false,
+      false,
+    );
   }
 
   /**
    * export addConditionToFilteringByViewerForList as static method
    */
-  pageSchema.statics.addConditionToFilteringByViewerForList = addConditionToFilteringByViewerForList;
+  pageSchema.statics.addConditionToFilteringByViewerForList =
+    addConditionToFilteringByViewerForList;
 
   /**
    * export addConditionToFilteringByViewerToEdit as static method
    */
-  pageSchema.statics.addConditionToFilteringByViewerToEdit = addConditionToFilteringByViewerToEdit;
+  pageSchema.statics.addConditionToFilteringByViewerToEdit =
+    addConditionToFilteringByViewerToEdit;
 
   /**
    * Throw error for growi-lsx-plugin (v1.x)
    */
-  pageSchema.statics.generateQueryToListByStartWith = function(path, user, option) {
+  pageSchema.statics.generateQueryToListByStartWith = function (
+    path,
+    user,
+    option,
+  ) {
     const dummyQuery = this.find();
-    dummyQuery.exec = async() => {
-      throw new Error('Plugin version mismatch. Upgrade growi-lsx-plugin to v2.0.0 or above.');
+    dummyQuery.exec = async () => {
+      throw new Error(
+        'Plugin version mismatch. Upgrade growi-lsx-plugin to v2.0.0 or above.',
+      );
     };
     return dummyQuery;
   };
-  pageSchema.statics.generateQueryToListWithDescendants = pageSchema.statics.generateQueryToListByStartWith;
-
+  pageSchema.statics.generateQueryToListWithDescendants =
+    pageSchema.statics.generateQueryToListByStartWith;
 
   /**
    * find all templates applicable to the new page
    */
-  pageSchema.statics.findTemplate = async function(path) {
+  pageSchema.statics.findTemplate = async function (path) {
     const templatePath = nodePath.posix.dirname(path);
     const pathList = generatePathsOnTree(path, []);
     const regexpList = pathList.map((path) => {
       const pathWithTrailingSlash = pathUtils.addTrailingSlash(path);
-      return new RegExp(`^${escapeStringRegexp(pathWithTrailingSlash)}_{1,2}template$`);
+      return new RegExp(
+        `^${escapeStringRegexp(pathWithTrailingSlash)}_{1,2}template$`,
+      );
     });
 
     const templatePages = await this.find({ path: { $in: regexpList } })
@@ -602,12 +732,16 @@ export const getPageSchema = (crowi) => {
     const targetTemplatePath = urljoin(path, `${type}template`);
 
     return templates.find((template) => {
-      return (template.path === targetTemplatePath);
+      return template.path === targetTemplatePath;
     });
   };
 
   const assignDecendantsTemplate = (decendantsTemplates, path) => {
-    const decendantsTemplate = assignTemplateByType(decendantsTemplates, path, '__');
+    const decendantsTemplate = assignTemplateByType(
+      decendantsTemplates,
+      path,
+      '__',
+    );
     if (decendantsTemplate) {
       return decendantsTemplate;
     }
@@ -620,7 +754,7 @@ export const getPageSchema = (crowi) => {
     return assignDecendantsTemplate(decendantsTemplates, newPath);
   };
 
-  const fetchTemplate = async(templates, templatePath) => {
+  const fetchTemplate = async (templates, templatePath) => {
     let templateBody;
     let templateTags;
     /**
@@ -633,13 +767,15 @@ export const getPageSchema = (crowi) => {
      * get decendants templates
      * _tempate: applicable to all pages under
      */
-    const decendantsTemplate = assignDecendantsTemplate(templates, templatePath);
+    const decendantsTemplate = assignDecendantsTemplate(
+      templates,
+      templatePath,
+    );
 
     if (childrenTemplate) {
       templateBody = childrenTemplate.revision.body;
       templateTags = await childrenTemplate.findRelatedTagsById();
-    }
-    else if (decendantsTemplate) {
+    } else if (decendantsTemplate) {
       templateBody = decendantsTemplate.revision.body;
       templateTags = await decendantsTemplate.findRelatedTagsById();
     }
@@ -647,7 +783,10 @@ export const getPageSchema = (crowi) => {
     return { templateBody, templateTags };
   };
 
-  pageSchema.statics.findListByPathsArray = async function(paths, includeEmpty = false) {
+  pageSchema.statics.findListByPathsArray = async function (
+    paths,
+    includeEmpty = false,
+  ) {
     const queryBuilder = new this.PageQueryBuilder(this.find(), includeEmpty);
     queryBuilder.addConditionToListByPathsArray(paths);
 
@@ -659,19 +798,30 @@ export const getPageSchema = (crowi) => {
    * @param {Page[]} pages
    * @param {IGrantedGroup} transferToUserGroup
    */
-  pageSchema.statics.transferPagesToGroup = async function(pages, transferToUserGroup) {
-    const userGroupModel = transferToUserGroup.type === GroupType.userGroup ? UserGroup : ExternalUserGroup;
+  pageSchema.statics.transferPagesToGroup = async function (
+    pages,
+    transferToUserGroup,
+  ) {
+    const userGroupModel =
+      transferToUserGroup.type === GroupType.userGroup
+        ? UserGroup
+        : ExternalUserGroup;
 
     if ((await userGroupModel.count({ _id: transferToUserGroup.item })) === 0) {
-      throw Error('Cannot find the group to which private pages belong to. _id: ', transferToUserGroup.item);
+      throw Error(
+        'Cannot find the group to which private pages belong to. _id: ',
+        transferToUserGroup.item,
+      );
     }
 
-    await this.updateMany({ _id: { $in: pages.map(p => p._id) } }, { grantedGroups: [transferToUserGroup] });
+    await this.updateMany(
+      { _id: { $in: pages.map((p) => p._id) } },
+      { grantedGroups: [transferToUserGroup] },
+    );
   };
 
-  pageSchema.statics.getHistories = function() {
+  pageSchema.statics.getHistories = () => {
     // TODO
-
   };
 
   pageSchema.statics.STATUS_PUBLISHED = STATUS_PUBLISHED;

+ 123 - 91
apps/app/src/server/models/page-operation.ts

@@ -1,15 +1,11 @@
 import type { IGrantedGroup } from '@growi/core';
 import { GroupType } from '@growi/core';
 import { addSeconds } from 'date-fns/addSeconds';
-import type {
-  Model, Document, QueryOptions, FilterQuery,
-} from 'mongoose';
-import mongoose, {
-  Schema,
-} from 'mongoose';
+import type { Document, FilterQuery, Model, QueryOptions } from 'mongoose';
+import mongoose, { Schema } from 'mongoose';
 
 import type { IOptionsForCreate, IOptionsForUpdate } from '~/interfaces/page';
-import { PageActionType, PageActionStage } from '~/interfaces/page-operation';
+import { PageActionStage, PageActionType } from '~/interfaces/page-operation';
 
 import loggerFactory from '../../utils/logger';
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
@@ -21,61 +17,69 @@ const logger = loggerFactory('growi:models:page-operation');
 
 const ObjectId = mongoose.Schema.Types.ObjectId;
 
-
 type IPageForResuming = {
-  _id: ObjectIdLike,
-  path: string,
-  isEmpty: boolean,
-  parent?: ObjectIdLike,
-  grant?: number,
-  grantedUsers?: ObjectIdLike[],
-  grantedGroups: IGrantedGroup[],
-  descendantCount: number,
-  status?: number,
-  revision?: ObjectIdLike,
-  lastUpdateUser?: ObjectIdLike,
-  creator?: ObjectIdLike,
+  _id: ObjectIdLike;
+  path: string;
+  isEmpty: boolean;
+  parent?: ObjectIdLike;
+  grant?: number;
+  grantedUsers?: ObjectIdLike[];
+  grantedGroups: IGrantedGroup[];
+  descendantCount: number;
+  status?: number;
+  revision?: ObjectIdLike;
+  lastUpdateUser?: ObjectIdLike;
+  creator?: ObjectIdLike;
 };
 
 type IUserForResuming = {
-  _id: ObjectIdLike,
+  _id: ObjectIdLike;
 };
 
 type IOptionsForResuming = {
-  format: 'md' | 'pdf',
-  updateMetadata?: boolean,
-  createRedirectPage?: boolean,
-  prevDescendantCount?: number,
-} & IOptionsForUpdate & IOptionsForCreate;
-
+  format: 'md' | 'pdf';
+  updateMetadata?: boolean;
+  createRedirectPage?: boolean;
+  prevDescendantCount?: number;
+} & IOptionsForUpdate &
+  IOptionsForCreate;
 
 /*
  * Main Schema
  */
 export interface IPageOperation {
-  actionType: PageActionType,
-  actionStage: PageActionStage,
-  fromPath: string,
-  toPath?: string,
-  page: IPageForResuming,
-  user: IUserForResuming,
-  options?: IOptionsForResuming,
-  incForUpdatingDescendantCount?: number,
-  unprocessableExpiryDate: Date,
-  exPage?: IPageForResuming,
-
-  isProcessable(): boolean
+  actionType: PageActionType;
+  actionStage: PageActionStage;
+  fromPath: string;
+  toPath?: string;
+  page: IPageForResuming;
+  user: IUserForResuming;
+  options?: IOptionsForResuming;
+  incForUpdatingDescendantCount?: number;
+  unprocessableExpiryDate: Date;
+  exPage?: IPageForResuming;
+
+  isProcessable(): boolean;
 }
 
 export interface PageOperationDocument extends IPageOperation, Document {}
 
-export type PageOperationDocumentHasId = PageOperationDocument & { _id: ObjectIdLike };
+export type PageOperationDocumentHasId = PageOperationDocument & {
+  _id: ObjectIdLike;
+};
 
 export interface PageOperationModel extends Model<PageOperationDocument> {
-  findByIdAndUpdatePageActionStage(pageOpId: ObjectIdLike, stage: PageActionStage): Promise<PageOperationDocumentHasId | null>
-  findMainOps(filter?: FilterQuery<PageOperationDocument>, projection?: any, options?: QueryOptions): Promise<PageOperationDocumentHasId[]>
-  deleteByActionTypes(deleteTypeList: PageActionType[]): Promise<void>
-  extendExpiryDate(operationId: ObjectIdLike): Promise<void>
+  findByIdAndUpdatePageActionStage(
+    pageOpId: ObjectIdLike,
+    stage: PageActionStage,
+  ): Promise<PageOperationDocumentHasId | null>;
+  findMainOps(
+    filter?: FilterQuery<PageOperationDocument>,
+    projection?: any,
+    options?: QueryOptions,
+  ): Promise<PageOperationDocumentHasId[]>;
+  deleteByActionTypes(deleteTypeList: PageActionType[]): Promise<void>;
+  extendExpiryDate(operationId: ObjectIdLike): Promise<void>;
 }
 
 const pageSchemaForResuming = new Schema<IPageForResuming>({
@@ -88,17 +92,21 @@ const pageSchemaForResuming = new Schema<IPageForResuming>({
   status: { type: String },
   grant: { type: Number },
   grantedUsers: [{ type: ObjectId, ref: 'User' }],
-  grantedGroups: [{
-    type: {
-      type: String,
-      enum: Object.values(GroupType),
-      required: true,
-      default: 'UserGroup',
+  grantedGroups: [
+    {
+      type: {
+        type: String,
+        enum: Object.values(GroupType),
+        required: true,
+        default: 'UserGroup',
+      },
+      item: {
+        type: ObjectId,
+        refPath: 'grantedGroups.type',
+        required: true,
+      },
     },
-    item: {
-      type: ObjectId, refPath: 'grantedGroups.type', required: true,
-    },
-  }],
+  ],
   creator: { type: ObjectId, ref: 'User' },
   lastUpdateUser: { type: ObjectId, ref: 'User' },
 });
@@ -107,25 +115,32 @@ const userSchemaForResuming = new Schema<IUserForResuming>({
   _id: { type: ObjectId, ref: 'User', required: true },
 });
 
-const optionsSchemaForResuming = new Schema<IOptionsForResuming>({
-  createRedirectPage: { type: Boolean },
-  updateMetadata: { type: Boolean },
-  prevDescendantCount: { type: Number },
-  grant: { type: Number },
-  grantUserGroupIds: [{
-    type: {
-      type: String,
-      enum: Object.values(GroupType),
-      required: true,
-      default: 'UserGroup',
-    },
-    item: {
-      type: ObjectId, refPath: 'grantedGroups.type', required: true,
-    },
-  }],
-  format: { type: String },
-  overwriteScopesOfDescendants: { type: Boolean },
-}, { _id: false });
+const optionsSchemaForResuming = new Schema<IOptionsForResuming>(
+  {
+    createRedirectPage: { type: Boolean },
+    updateMetadata: { type: Boolean },
+    prevDescendantCount: { type: Number },
+    grant: { type: Number },
+    grantUserGroupIds: [
+      {
+        type: {
+          type: String,
+          enum: Object.values(GroupType),
+          required: true,
+          default: 'UserGroup',
+        },
+        item: {
+          type: ObjectId,
+          refPath: 'grantedGroups.type',
+          required: true,
+        },
+      },
+    ],
+    format: { type: String },
+    overwriteScopesOfDescendants: { type: Boolean },
+  },
+  { _id: false },
+);
 
 const schema = new Schema<PageOperationDocument, PageOperationModel>({
   actionType: {
@@ -147,22 +162,30 @@ const schema = new Schema<PageOperationDocument, PageOperationModel>({
   user: { type: userSchemaForResuming, required: true },
   options: { type: optionsSchemaForResuming },
   incForUpdatingDescendantCount: { type: Number },
-  unprocessableExpiryDate: { type: Date, default: () => addSeconds(new Date(), 10) },
+  unprocessableExpiryDate: {
+    type: Date,
+    default: () => addSeconds(new Date(), 10),
+  },
 });
 
-schema.statics.findByIdAndUpdatePageActionStage = async function(
-    pageOpId: ObjectIdLike, stage: PageActionStage,
+schema.statics.findByIdAndUpdatePageActionStage = async function (
+  pageOpId: ObjectIdLike,
+  stage: PageActionStage,
 ): Promise<PageOperationDocumentHasId | null> {
-
-  return this.findByIdAndUpdate(pageOpId, {
-    $set: { actionStage: stage },
-  }, { new: true });
+  return this.findByIdAndUpdate(
+    pageOpId,
+    {
+      $set: { actionStage: stage },
+    },
+    { new: true },
+  );
 };
 
-schema.statics.findMainOps = async function(
-    filter?: FilterQuery<PageOperationDocument>, projection?: any, options?: QueryOptions,
+schema.statics.findMainOps = async function (
+  filter?: FilterQuery<PageOperationDocument>,
+  projection?: any,
+  options?: QueryOptions,
 ): Promise<PageOperationDocumentHasId[]> {
-
   return this.find(
     { ...filter, actionStage: PageActionStage.Main },
     projection,
@@ -170,25 +193,34 @@ schema.statics.findMainOps = async function(
   );
 };
 
-schema.statics.deleteByActionTypes = async function(
-    actionTypes: PageActionType[],
+schema.statics.deleteByActionTypes = async function (
+  actionTypes: PageActionType[],
 ): Promise<void> {
-
   await this.deleteMany({ actionType: { $in: actionTypes } });
-  logger.info(`Deleted all PageOperation documents with actionType: [${actionTypes}]`);
+  logger.info(
+    `Deleted all PageOperation documents with actionType: [${actionTypes}]`,
+  );
 };
 
 /**
  * add TIME_TO_ADD_SEC to current time and update unprocessableExpiryDate with it
  */
-schema.statics.extendExpiryDate = async function(operationId: ObjectIdLike): Promise<void> {
+schema.statics.extendExpiryDate = async function (
+  operationId: ObjectIdLike,
+): Promise<void> {
   const date = addSeconds(new Date(), TIME_TO_ADD_SEC);
   await this.findByIdAndUpdate(operationId, { unprocessableExpiryDate: date });
 };
 
-schema.methods.isProcessable = function(): boolean {
+schema.methods.isProcessable = function (): boolean {
   const { unprocessableExpiryDate } = this;
-  return unprocessableExpiryDate == null || (unprocessableExpiryDate != null && new Date() > unprocessableExpiryDate);
+  return (
+    unprocessableExpiryDate == null ||
+    (unprocessableExpiryDate != null && new Date() > unprocessableExpiryDate)
+  );
 };
 
-export default getOrCreateModel<PageOperationDocument, PageOperationModel>('PageOperation', schema);
+export default getOrCreateModel<PageOperationDocument, PageOperationModel>(
+  'PageOperation',
+  schema,
+);

+ 45 - 30
apps/app/src/server/models/page-redirect.ts

@@ -1,47 +1,54 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import type { Model, Document } from 'mongoose';
+import type { Document, Model } from 'mongoose';
 import { Schema } from 'mongoose';
 
 import loggerFactory from '~/utils/logger';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 const logger = loggerFactory('growi:models:page-redirects');
 
-
 export type IPageRedirect = {
-  fromPath: string,
-  toPath: string,
-}
+  fromPath: string;
+  toPath: string;
+};
 
 export type IPageRedirectEndpoints = {
-  start: IPageRedirect,
-  end: IPageRedirect,
-}
+  start: IPageRedirect;
+  end: IPageRedirect;
+};
 
 export interface PageRedirectDocument extends IPageRedirect, Document {}
 
 export interface PageRedirectModel extends Model<PageRedirectDocument> {
-  retrievePageRedirectEndpoints(fromPath: string): Promise<IPageRedirectEndpoints>
-  removePageRedirectsByToPath(toPath: string): Promise<void>
+  retrievePageRedirectEndpoints(
+    fromPath: string,
+  ): Promise<IPageRedirectEndpoints>;
+  removePageRedirectsByToPath(toPath: string): Promise<void>;
 }
 
 const CHAINS_FIELD_NAME = 'chains';
 const DEPTH_FIELD_NAME = 'depth';
 type IPageRedirectWithChains = PageRedirectDocument & {
-  [CHAINS_FIELD_NAME]: (PageRedirectDocument & { [DEPTH_FIELD_NAME]: number })[]
+  [CHAINS_FIELD_NAME]: (PageRedirectDocument & {
+    [DEPTH_FIELD_NAME]: number;
+  })[];
 };
 
 const schema = new Schema<PageRedirectDocument, PageRedirectModel>({
   fromPath: {
-    type: String, required: true, unique: true, index: true,
+    type: String,
+    required: true,
+    unique: true,
+    index: true,
   },
   toPath: { type: String, required: true },
 });
 
-schema.statics.retrievePageRedirectEndpoints = async function(fromPath: string): Promise<IPageRedirectEndpoints|null> {
+schema.statics.retrievePageRedirectEndpoints = async function (
+  fromPath: string,
+): Promise<IPageRedirectEndpoints | null> {
   const aggResult: IPageRedirectWithChains[] = await this.aggregate([
     { $match: { fromPath } },
     {
@@ -82,23 +89,30 @@ schema.statics.retrievePageRedirectEndpoints = async function(fromPath: string):
   }
 
   if (aggResult.length > 1) {
-    logger.warn(`Although two or more PageRedirect documents starts from '${fromPath}' exists, The first one is used.`);
+    logger.warn(
+      `Although two or more PageRedirect documents starts from '${fromPath}' exists, The first one is used.`,
+    );
   }
 
   const redirectWithChains = aggResult[0];
 
   // sort chains in desc
-  const sortedChains = redirectWithChains[CHAINS_FIELD_NAME].sort((a, b) => b[DEPTH_FIELD_NAME] - a[DEPTH_FIELD_NAME]);
+  const sortedChains = redirectWithChains[CHAINS_FIELD_NAME].sort(
+    (a, b) => b[DEPTH_FIELD_NAME] - a[DEPTH_FIELD_NAME],
+  );
 
-  const start = { fromPath: redirectWithChains.fromPath, toPath: redirectWithChains.toPath };
-  const end = sortedChains.length === 0
-    ? start
-    : sortedChains[0];
+  const start = {
+    fromPath: redirectWithChains.fromPath,
+    toPath: redirectWithChains.toPath,
+  };
+  const end = sortedChains.length === 0 ? start : sortedChains[0];
 
   return { start, end };
 };
 
-schema.statics.removePageRedirectsByToPath = async function(toPath: string): Promise<void> {
+schema.statics.removePageRedirectsByToPath = async function (
+  toPath: string,
+): Promise<void> {
   const aggResult: IPageRedirectWithChains[] = await this.aggregate([
     { $match: { toPath } },
     {
@@ -145,17 +159,18 @@ schema.statics.removePageRedirectsByToPath = async function(toPath: string): Pro
     return;
   }
 
-  const idsToRemove = aggResult
-    .map((redirectWithChains) => {
-      return [
-        redirectWithChains._id,
-        redirectWithChains[CHAINS_FIELD_NAME].map(doc => doc._id),
-      ].flat();
-    })
-    .flat();
+  const idsToRemove = aggResult.flatMap((redirectWithChains) => {
+    return [
+      redirectWithChains._id,
+      redirectWithChains[CHAINS_FIELD_NAME].map((doc) => doc._id),
+    ].flat();
+  });
 
   await this.deleteMany({ _id: { $in: idsToRemove } });
   return;
 };
 
-export default getOrCreateModel<PageRedirectDocument, PageRedirectModel>('PageRedirect', schema);
+export default getOrCreateModel<PageRedirectDocument, PageRedirectModel>(
+  'PageRedirect',
+  schema,
+);

+ 91 - 53
apps/app/src/server/models/page-tag-relation.ts

@@ -1,7 +1,5 @@
 import type { ITag } from '@growi/core';
-import type {
-  Document, Model, ObjectId, Types,
-} from 'mongoose';
+import type { Document, Model, ObjectId, Types } from 'mongoose';
 import mongoose from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
@@ -10,52 +8,65 @@ import type { IPageTagRelation } from '~/interfaces/page-tag-relation';
 
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
-
 import type { IdToNamesMap } from './tag';
 import Tag from './tag';
 
-
 // disable no-return-await for model functions
 /* eslint-disable no-return-await */
 
 const flatMap = require('array.prototype.flatmap');
 
-
-export interface PageTagRelationDocument extends IPageTagRelation, Document {
-}
+export interface PageTagRelationDocument extends IPageTagRelation, Document {}
 
 type CreateTagListWithCountOpts = {
-  sortOpt?: any,
-  offset?: number,
-  limit?: number,
-}
+  sortOpt?: any;
+  offset?: number;
+  limit?: number;
+};
 type CreateTagListWithCountResult = {
-  data: ITag[],
-  totalCount: number
-}
-type CreateTagListWithCount = (this: PageTagRelationModel, opts?: CreateTagListWithCountOpts) => Promise<CreateTagListWithCountResult>;
-
-type ListTagNamesByPage = (pageId: Types.ObjectId | string) => Promise<PageTagRelationDocument[]>;
-
-type FindByPageId = (pageId: Types.ObjectId | string, options?: { nullable?: boolean }) => Promise<PageTagRelationDocument[]>;
-
-type GetIdToTagNamesMap = (this: PageTagRelationModel, pageIds: string[]) => Promise<IdToNamesMap>;
-
-type UpdatePageTags = (this: PageTagRelationModel, pageId: Types.ObjectId | string, tags: string[]) => Promise<void>
+  data: ITag[];
+  totalCount: number;
+};
+type CreateTagListWithCount = (
+  this: PageTagRelationModel,
+  opts?: CreateTagListWithCountOpts,
+) => Promise<CreateTagListWithCountResult>;
+
+type ListTagNamesByPage = (
+  pageId: Types.ObjectId | string,
+) => Promise<PageTagRelationDocument[]>;
+
+type FindByPageId = (
+  pageId: Types.ObjectId | string,
+  options?: { nullable?: boolean },
+) => Promise<PageTagRelationDocument[]>;
+
+type GetIdToTagNamesMap = (
+  this: PageTagRelationModel,
+  pageIds: string[],
+) => Promise<IdToNamesMap>;
+
+type UpdatePageTags = (
+  this: PageTagRelationModel,
+  pageId: Types.ObjectId | string,
+  tags: string[],
+) => Promise<void>;
 
 export interface PageTagRelationModel extends Model<PageTagRelationDocument> {
-  createTagListWithCount: CreateTagListWithCount
-  findByPageId: FindByPageId
-  listTagNamesByPage: ListTagNamesByPage
-  getIdToTagNamesMap: GetIdToTagNamesMap
-  updatePageTags: UpdatePageTags
+  createTagListWithCount: CreateTagListWithCount;
+  findByPageId: FindByPageId;
+  listTagNamesByPage: ListTagNamesByPage;
+  getIdToTagNamesMap: GetIdToTagNamesMap;
+  updatePageTags: UpdatePageTags;
 }
 
-
 /*
  * define schema
  */
-const schema = new mongoose.Schema<PageTagRelationDocument, PageTagRelationModel>({
+const schema = new mongoose.Schema<
+  PageTagRelationDocument,
+  PageTagRelationModel
+>({
   relatedPage: {
     type: mongoose.Schema.Types.ObjectId,
     ref: 'Page',
@@ -80,7 +91,10 @@ schema.index({ relatedPage: 1, relatedTag: 1 }, { unique: true });
 schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);
 
-const createTagListWithCount: CreateTagListWithCount = async function(this, opts) {
+const createTagListWithCount: CreateTagListWithCount = async function (
+  this,
+  opts,
+) {
   const sortOpt = opts?.sortOpt || {};
   const offset = opts?.offset ?? 0;
   const limit = opts?.limit;
@@ -94,7 +108,11 @@ const createTagListWithCount: CreateTagListWithCount = async function(this, opts
       as: 'tag',
     })
     .unwind('$tag')
-    .group({ _id: '$relatedTag', count: { $sum: 1 }, name: { $first: '$tag.name' } })
+    .group({
+      _id: '$relatedTag',
+      count: { $sum: 1 },
+      name: { $first: '$tag.name' },
+    })
     .sort(sortOpt)
     .skip(offset);
 
@@ -102,26 +120,36 @@ const createTagListWithCount: CreateTagListWithCount = async function(this, opts
     query = query.limit(limit);
   }
 
-  const totalCount = (await this.find({ isPageTrashed: false }).distinct('relatedTag')).length;
+  const totalCount = (
+    await this.find({ isPageTrashed: false }).distinct('relatedTag')
+  ).length;
 
   return { data: await query.exec(), totalCount };
 };
 schema.statics.createTagListWithCount = createTagListWithCount;
 
-const findByPageId: FindByPageId = async function(pageId, options = {}) {
+const findByPageId: FindByPageId = async function (pageId, options = {}) {
   const isAcceptRelatedTagNull = options.nullable || null;
-  const relations = await this.find({ relatedPage: pageId }).populate('relatedTag').select('relatedTag');
-  return isAcceptRelatedTagNull ? relations : relations.filter((relation) => { return relation.relatedTag !== null });
+  const relations = await this.find({ relatedPage: pageId })
+    .populate('relatedTag')
+    .select('relatedTag');
+  return isAcceptRelatedTagNull
+    ? relations
+    : relations.filter((relation) => {
+        return relation.relatedTag !== null;
+      });
 };
 schema.statics.findByPageId = findByPageId;
 
-const listTagNamesByPage: ListTagNamesByPage = async function(pageId) {
+const listTagNamesByPage: ListTagNamesByPage = async function (pageId) {
   const relations = await this.findByPageId(pageId);
-  return relations.map((relation) => { return relation.relatedTag.name });
+  return relations.map((relation) => {
+    return relation.relatedTag.name;
+  });
 };
 schema.statics.listTagNamesByPage = listTagNamesByPage;
 
-const getIdToTagNamesMap: GetIdToTagNamesMap = async function(this, pageIds) {
+const getIdToTagNamesMap: GetIdToTagNamesMap = async function (this, pageIds) {
   /**
    * @see https://docs.mongodb.com/manual/reference/operator/aggregation/group/#pivot-data
    *
@@ -132,7 +160,10 @@ const getIdToTagNamesMap: GetIdToTagNamesMap = async function(this, pageIds) {
    *   ...
    * ]
    */
-  const results = await this.aggregate<{ _id: ObjectId, tagIds: ObjectIdLike[] }>()
+  const results = await this.aggregate<{
+    _id: ObjectId;
+    tagIds: ObjectIdLike[];
+  }>()
     .match({ relatedPage: { $in: pageIds } })
     .group({ _id: '$relatedPage', tagIds: { $push: '$relatedTag' } });
 
@@ -143,8 +174,7 @@ const getIdToTagNamesMap: GetIdToTagNamesMap = async function(this, pageIds) {
   results.flatMap = flatMap.shim(); // TODO: remove after upgrading to node v12
 
   // extract distinct tag ids
-  const allTagIds = results
-    .flatMap(result => result.tagIds); // map + flatten
+  const allTagIds = results.flatMap((result) => result.tagIds); // map + flatten
   const distinctTagIds = Array.from(new Set(allTagIds));
 
   // TODO: set IdToNameMap type by 93933
@@ -154,8 +184,8 @@ const getIdToTagNamesMap: GetIdToTagNamesMap = async function(this, pageIds) {
   const idToTagNamesMap = {};
   results.forEach((result) => {
     const tagNames = result.tagIds
-      .map(tagId => tagIdToNameMap[tagId.toString()])
-      .filter(tagName => tagName != null); // filter null object
+      .map((tagId) => tagIdToNameMap[tagId.toString()])
+      .filter((tagName) => tagName != null); // filter null object
 
     idToTagNamesMap[result._id.toString()] = tagNames;
   });
@@ -164,14 +194,16 @@ const getIdToTagNamesMap: GetIdToTagNamesMap = async function(this, pageIds) {
 };
 schema.statics.getIdToTagNamesMap = getIdToTagNamesMap;
 
-const updatePageTags: UpdatePageTags = async function(pageId, tags) {
+const updatePageTags: UpdatePageTags = async function (pageId, tags) {
   if (pageId == null || tags == null) {
-    throw new Error('args \'pageId\' and \'tags\' are required.');
+    throw new Error("args 'pageId' and 'tags' are required.");
   }
 
   // filter empty string
   // eslint-disable-next-line no-param-reassign
-  tags = tags.filter((tag) => { return tag !== '' });
+  tags = tags.filter((tag) => {
+    return tag !== '';
+  });
 
   // get relations for this page
   const relations = await this.findByPageId(pageId, { nullable: true });
@@ -182,17 +214,20 @@ const updatePageTags: UpdatePageTags = async function(pageId, tags) {
   relations.forEach((relation) => {
     if (relation.relatedTag == null) {
       unlinkTagRelationIds.push(relation._id);
-    }
-    else {
+    } else {
       relatedTagNames.push(relation.relatedTag.name);
       if (!tags.includes(relation.relatedTag.name)) {
         unlinkTagRelationIds.push(relation._id);
       }
     }
   });
-  const bulkDeletePromise = this.deleteMany({ _id: { $in: unlinkTagRelationIds } });
+  const bulkDeletePromise = this.deleteMany({
+    _id: { $in: unlinkTagRelationIds },
+  });
   // find or create tags
-  const tagsToCreate = tags.filter((tag) => { return !relatedTagNames.includes(tag) });
+  const tagsToCreate = tags.filter((tag) => {
+    return !relatedTagNames.includes(tag);
+  });
   const tagEntities = await Tag.findOrCreateMany(tagsToCreate);
 
   // create relations
@@ -209,4 +244,7 @@ const updatePageTags: UpdatePageTags = async function(pageId, tags) {
 };
 schema.statics.updatePageTags = updatePageTags;
 
-export default getOrCreateModel<PageTagRelationDocument, PageTagRelationModel>('PageTagRelation', schema);
+export default getOrCreateModel<PageTagRelationDocument, PageTagRelationModel>(
+  'PageTagRelation',
+  schema,
+);

Разница между файлами не показана из-за своего большого размера
+ 415 - 294
apps/app/src/server/models/page.ts


+ 39 - 34
apps/app/src/server/models/password-reset-order.ts

@@ -1,61 +1,63 @@
 import crypto from 'crypto';
-
 import { addMinutes } from 'date-fns/addMinutes';
-import type { Model, Document } from 'mongoose';
-import {
-  Schema,
-} from 'mongoose';
+import type { Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface IPasswordResetOrder {
-  token: string,
-  email: string,
+  token: string;
+  email: string;
   // eslint-disable-next-line @typescript-eslint/no-explicit-any
-  relatedUser: any,
-  isRevoked: boolean,
-  createdAt: Date,
-  expiredAt: Date,
+  relatedUser: any;
+  isRevoked: boolean;
+  createdAt: Date;
+  expiredAt: Date;
 }
 
-export interface PasswordResetOrderDocument extends IPasswordResetOrder, Document {
-  isExpired(): boolean
-  revokeOneTimeToken(): Promise<void>
+export interface PasswordResetOrderDocument
+  extends IPasswordResetOrder,
+    Document {
+  isExpired(): boolean;
+  revokeOneTimeToken(): Promise<void>;
 }
 
-export interface PasswordResetOrderModel extends Model<PasswordResetOrderDocument> {
-  generateOneTimeToken(): string
-  createPasswordResetOrder(email: string): PasswordResetOrderDocument
+export interface PasswordResetOrderModel
+  extends Model<PasswordResetOrderDocument> {
+  generateOneTimeToken(): string;
+  createPasswordResetOrder(email: string): PasswordResetOrderDocument;
 }
 
 const expiredAt = (): Date => {
   return addMinutes(new Date(), 10);
 };
 
-const schema = new Schema<PasswordResetOrderDocument, PasswordResetOrderModel>({
-  token: { type: String, required: true, unique: true },
-  email: { type: String, required: true },
-  relatedUser: { type: Schema.Types.ObjectId, ref: 'User' },
-  isRevoked: { type: Boolean, default: false, required: true },
-  expiredAt: { type: Date, default: expiredAt, required: true },
-}, {
-  timestamps: {
-    createdAt: true,
-    updatedAt: false,
+const schema = new Schema<PasswordResetOrderDocument, PasswordResetOrderModel>(
+  {
+    token: { type: String, required: true, unique: true },
+    email: { type: String, required: true },
+    relatedUser: { type: Schema.Types.ObjectId, ref: 'User' },
+    isRevoked: { type: Boolean, default: false, required: true },
+    expiredAt: { type: Date, default: expiredAt, required: true },
+  },
+  {
+    timestamps: {
+      createdAt: true,
+      updatedAt: false,
+    },
   },
-});
+);
 schema.plugin(uniqueValidator);
 
-schema.statics.generateOneTimeToken = function() {
+schema.statics.generateOneTimeToken = () => {
   const buf = crypto.randomBytes(256);
   const token = buf.toString('hex');
 
   return token;
 };
 
-schema.statics.createPasswordResetOrder = async function(email) {
+schema.statics.createPasswordResetOrder = async function (email) {
   let token;
   let duplicateToken;
 
@@ -70,13 +72,16 @@ schema.statics.createPasswordResetOrder = async function(email) {
   return passwordResetOrderData;
 };
 
-schema.methods.isExpired = function() {
+schema.methods.isExpired = function () {
   return this.expiredAt.getTime() < Date.now();
 };
 
-schema.methods.revokeOneTimeToken = async function() {
+schema.methods.revokeOneTimeToken = async function () {
   this.isRevoked = true;
   return this.save();
 };
 
-export default getOrCreateModel<PasswordResetOrderDocument, PasswordResetOrderModel>('PasswordResetOrder', schema);
+export default getOrCreateModel<
+  PasswordResetOrderDocument,
+  PasswordResetOrderModel
+>('PasswordResetOrder', schema);

+ 59 - 35
apps/app/src/server/models/revision.ts

@@ -5,61 +5,74 @@ import type {
   Origin,
 } from '@growi/core/dist/interfaces';
 import type { Types } from 'mongoose';
-import {
-  Schema, type Document, type Model,
-} from 'mongoose';
+import { type Document, type Model, Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 import loggerFactory from '~/utils/logger';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
-
 import type { PageDocument } from './page';
 
 const logger = loggerFactory('growi:models:revision');
 
+export interface IRevisionDocument extends IRevision, Document {}
 
-export interface IRevisionDocument extends IRevision, Document {
-}
-
-type UpdateRevisionListByPageId = (pageId: Types.ObjectId, updateData: Partial<IRevision>) => Promise<void>;
+type UpdateRevisionListByPageId = (
+  pageId: Types.ObjectId,
+  updateData: Partial<IRevision>,
+) => Promise<void>;
 type PrepareRevision = (
-  pageData: PageDocument, body: string, previousBody: string | null, user: HasObjectId, origin?: Origin, options?: { format: string }
+  pageData: PageDocument,
+  body: string,
+  previousBody: string | null,
+  user: HasObjectId,
+  origin?: Origin,
+  options?: { format: string },
 ) => IRevisionDocument;
 
 export interface IRevisionModel extends Model<IRevisionDocument> {
-  updateRevisionListByPageId: UpdateRevisionListByPageId,
-  prepareRevision: PrepareRevision,
+  updateRevisionListByPageId: UpdateRevisionListByPageId;
+  prepareRevision: PrepareRevision;
 }
 
 // Use this to allow empty strings to pass the `required` validator
-Schema.Types.String.checkRequired(v => typeof v === 'string');
+Schema.Types.String.checkRequired((v) => typeof v === 'string');
 
-const revisionSchema = new Schema<IRevisionDocument, IRevisionModel>({
-  // The type of pageId is always converted to String at server startup
-  // Refer to this method (/src/server/service/normalize-data/convert-revision-page-id-to-string.ts) to change the pageId type
-  pageId: {
-    type: Schema.Types.ObjectId, ref: 'Page', required: true, index: true,
-  },
-  body: {
-    type: String,
-    required: true,
-    get: (data) => {
-    // replace CR/CRLF to LF above v3.1.5
-    // see https://github.com/growilabs/growi/issues/463
-      return data ? data.replace(/\r\n?/g, '\n') : '';
+const revisionSchema = new Schema<IRevisionDocument, IRevisionModel>(
+  {
+    // The type of pageId is always converted to String at server startup
+    // Refer to this method (/src/server/service/normalize-data/convert-revision-page-id-to-string.ts) to change the pageId type
+    pageId: {
+      type: Schema.Types.ObjectId,
+      ref: 'Page',
+      required: true,
+      index: true,
+    },
+    body: {
+      type: String,
+      required: true,
+      get: (data) => {
+        // replace CR/CRLF to LF above v3.1.5
+        // see https://github.com/growilabs/growi/issues/463
+        return data ? data.replace(/\r\n?/g, '\n') : '';
+      },
     },
+    format: { type: String, default: 'markdown' },
+    author: { type: Schema.Types.ObjectId, ref: 'User' },
+    hasDiffToPrev: { type: Boolean },
+    origin: { type: String, enum: allOrigin },
+  },
+  {
+    timestamps: { createdAt: true, updatedAt: false },
   },
-  format: { type: String, default: 'markdown' },
-  author: { type: Schema.Types.ObjectId, ref: 'User' },
-  hasDiffToPrev: { type: Boolean },
-  origin: { type: String, enum: allOrigin },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
+);
 revisionSchema.plugin(mongoosePaginate);
 
-const updateRevisionListByPageId: UpdateRevisionListByPageId = async function(this: IRevisionModel, pageId, updateData) {
+const updateRevisionListByPageId: UpdateRevisionListByPageId = async function (
+  this: IRevisionModel,
+  pageId,
+  updateData,
+) {
   // Check pageId for safety
   if (pageId == null) {
     throw new Error('Error: pageId is required');
@@ -68,7 +81,15 @@ const updateRevisionListByPageId: UpdateRevisionListByPageId = async function(th
 };
 revisionSchema.statics.updateRevisionListByPageId = updateRevisionListByPageId;
 
-const prepareRevision: PrepareRevision = function(this: IRevisionModel, pageData, body, previousBody, user, origin, options = { format: 'markdown' }) {
+const prepareRevision: PrepareRevision = function (
+  this: IRevisionModel,
+  pageData,
+  body,
+  previousBody,
+  user,
+  origin,
+  options = { format: 'markdown' },
+) {
   if (user._id == null) {
     throw new Error('user should have _id');
   }
@@ -90,4 +111,7 @@ const prepareRevision: PrepareRevision = function(this: IRevisionModel, pageData
 };
 revisionSchema.statics.prepareRevision = prepareRevision;
 
-export const Revision = getOrCreateModel<IRevisionDocument, IRevisionModel>('Revision', revisionSchema);
+export const Revision = getOrCreateModel<IRevisionDocument, IRevisionModel>(
+  'Revision',
+  revisionSchema,
+);

+ 5 - 1
apps/app/src/server/models/serializers/page-serializer.js

@@ -14,7 +14,11 @@ function serializeInsecureUserAttributes(page) {
   if (page.creator != null && page.creator._id != null) {
     page.creator = serializeUserSecurely(page.creator);
   }
-  if (page.revision != null && page.revision.author != null && page.revision.author._id != null) {
+  if (
+    page.revision != null &&
+    page.revision.author != null &&
+    page.revision.author._id != null
+  ) {
     page.revision.author = serializeUserSecurely(page.revision.author);
   }
   return page;

+ 7 - 2
apps/app/src/server/models/serializers/user-group-relation-serializer.js

@@ -1,8 +1,13 @@
 import { serializeUserSecurely } from '@growi/core/dist/models/serializers';
 
 function serializeInsecureUserAttributes(userGroupRelation) {
-  if (userGroupRelation.relatedUser != null && userGroupRelation.relatedUser._id != null) {
-    userGroupRelation.relatedUser = serializeUserSecurely(userGroupRelation.relatedUser);
+  if (
+    userGroupRelation.relatedUser != null &&
+    userGroupRelation.relatedUser._id != null
+  ) {
+    userGroupRelation.relatedUser = serializeUserSecurely(
+      userGroupRelation.relatedUser,
+    );
   }
   return userGroupRelation;
 }

+ 21 - 19
apps/app/src/server/models/share-link.ts

@@ -1,7 +1,5 @@
+import type { Document, Model } from 'mongoose';
 import mongoose, { Schema } from 'mongoose';
-import type {
-  Document, Model,
-} from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
 
@@ -9,37 +7,41 @@ import type { IShareLink } from '~/interfaces/share-link';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface ShareLinkDocument extends IShareLink, Document {
-  isExpired: () => boolean,
+  isExpired: () => boolean;
 }
 
 export type ShareLinkModel = Model<ShareLinkDocument>;
 
-
 /*
  * define schema
  */
-const schema = new Schema<ShareLinkDocument, ShareLinkModel>({
-  relatedPage: {
-    type: Schema.Types.ObjectId,
-    ref: 'Page',
-    required: true,
-    index: true,
+const schema = new Schema<ShareLinkDocument, ShareLinkModel>(
+  {
+    relatedPage: {
+      type: Schema.Types.ObjectId,
+      ref: 'Page',
+      required: true,
+      index: true,
+    },
+    expiredAt: { type: Date },
+    description: { type: String },
+  },
+  {
+    timestamps: { createdAt: true, updatedAt: false },
   },
-  expiredAt: { type: Date },
-  description: { type: String },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
+);
 schema.plugin(mongoosePaginate);
 schema.plugin(uniqueValidator);
 
-schema.methods.isExpired = function() {
+schema.methods.isExpired = function () {
   if (this.expiredAt == null) {
     return false;
   }
   return this.expiredAt.getTime() < new Date().getTime();
 };
 
-export default getOrCreateModel<ShareLinkDocument, ShareLinkModel>('ShareLink', schema);
+export default getOrCreateModel<ShareLinkDocument, ShareLinkModel>(
+  'ShareLink',
+  schema,
+);

+ 23 - 13
apps/app/src/server/models/slack-app-integration.js

@@ -1,12 +1,10 @@
-import crypto from 'crypto';
-
 import { defaultSupportedSlackEventActions } from '@growi/slack';
+import crypto from 'crypto';
 import mongoose from 'mongoose';
 
 import { configManager } from '../service/config-manager';
 import { getModelSafely } from '../util/mongoose-utils';
 
-
 const schema = new mongoose.Schema({
   tokenGtoP: { type: String, required: true, unique: true },
   tokenPtoG: { type: String, required: true, unique: true },
@@ -15,18 +13,23 @@ const schema = new mongoose.Schema({
   permissionsForSingleUseCommands: Map,
   permissionsForSlackEventActions: {
     type: Map,
-    default: new Map(defaultSupportedSlackEventActions.map(action => [action, false])),
+    default: new Map(
+      defaultSupportedSlackEventActions.map((action) => [action, false]),
+    ),
   },
 });
 
 class SlackAppIntegration {
-
   static generateAccessTokens(saltForGtoP, saltForPtoG) {
     const now = new Date().getTime();
     const hasher1 = crypto.createHash('sha512');
     const hasher2 = crypto.createHash('sha512');
-    const tokenGtoP = hasher1.update(`gtop-${saltForGtoP}-${now.toString()}`).digest('base64');
-    const tokenPtoG = hasher2.update(`ptog-${saltForPtoG}-${now.toString()}`).digest('base64');
+    const tokenGtoP = hasher1
+      .update(`gtop-${saltForGtoP}-${now.toString()}`)
+      .digest('base64');
+    const tokenPtoG = hasher2
+      .update(`ptog-${saltForPtoG}-${now.toString()}`)
+      .digest('base64');
     return [tokenGtoP, tokenPtoG];
   }
 
@@ -37,21 +40,28 @@ class SlackAppIntegration {
     let generateTokens;
 
     // get salt strings
-    const saltForGtoP = configManager.getConfig('slackbot:withProxy:saltForGtoP');
-    const saltForPtoG = configManager.getConfig('slackbot:withProxy:saltForPtoG');
+    const saltForGtoP = configManager.getConfig(
+      'slackbot:withProxy:saltForGtoP',
+    );
+    const saltForPtoG = configManager.getConfig(
+      'slackbot:withProxy:saltForPtoG',
+    );
 
     do {
-      generateTokens = this.generateAccessTokens(saltForGtoP, saltForPtoG);
+      generateTokens = SlackAppIntegration.generateAccessTokens(
+        saltForGtoP,
+        saltForPtoG,
+      );
       tokenGtoP = generateTokens[0];
       tokenPtoG = generateTokens[1];
       // eslint-disable-next-line no-await-in-loop
-      duplicateTokens = await this.findOne({ $or: [{ tokenGtoP }, { tokenPtoG }] });
+      duplicateTokens = await SlackAppIntegration.findOne({
+        $or: [{ tokenGtoP }, { tokenPtoG }],
+      });
     } while (duplicateTokens != null);
 
-
     return { tokenGtoP, tokenPtoG };
   }
-
 }
 
 const factory = (crowi) => {

+ 103 - 56
apps/app/src/server/models/subscription.ts

@@ -1,95 +1,142 @@
-import type {
-  Ref, IPage, IUser, ISubscription,
-} from '@growi/core';
-import {
-  SubscriptionStatusType, AllSubscriptionStatusType,
-} from '@growi/core';
-import {
-  type Types, type Document, type Model, Schema,
-} from 'mongoose';
+import type { IPage, ISubscription, IUser, Ref } from '@growi/core';
+import { AllSubscriptionStatusType, SubscriptionStatusType } from '@growi/core';
+import { type Document, type Model, Schema, type Types } from 'mongoose';
 
 import type { IPageBulkExportJob } from '~/features/page-bulk-export/interfaces/page-bulk-export';
 import type { SupportedTargetModelType } from '~/interfaces/activity';
-import { AllSupportedTargetModels, SupportedTargetModel } from '~/interfaces/activity';
+import {
+  AllSupportedTargetModels,
+  SupportedTargetModel,
+} from '~/interfaces/activity';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface SubscriptionDocument extends ISubscription, Document {}
 
 export interface SubscriptionModel extends Model<SubscriptionDocument> {
-  findByUserIdAndTargetId(userId: Types.ObjectId | string, targetId: Types.ObjectId | string): any
-  upsertSubscription(user: Ref<IUser>, targetModel: SupportedTargetModelType, target: Ref<IPage> | Ref<IUser> | Ref<IPageBulkExportJob>, status: string): any
-  subscribeByPageId(userId: Types.ObjectId, pageId: Types.ObjectId, status: string): any
-  getSubscription(target: Ref<IPage>): Promise<Ref<IUser>[]>
-  getUnsubscription(target: Ref<IPage>): Promise<Ref<IUser>[]>
-  getSubscriptions(targets: Ref<IPage>[]): Promise<Ref<IUser>[]>
+  findByUserIdAndTargetId(
+    userId: Types.ObjectId | string,
+    targetId: Types.ObjectId | string,
+  ): any;
+  upsertSubscription(
+    user: Ref<IUser>,
+    targetModel: SupportedTargetModelType,
+    target: Ref<IPage> | Ref<IUser> | Ref<IPageBulkExportJob>,
+    status: string,
+  ): any;
+  subscribeByPageId(
+    userId: Types.ObjectId,
+    pageId: Types.ObjectId,
+    status: string,
+  ): any;
+  getSubscription(target: Ref<IPage>): Promise<Ref<IUser>[]>;
+  getUnsubscription(target: Ref<IPage>): Promise<Ref<IUser>[]>;
+  getSubscriptions(targets: Ref<IPage>[]): Promise<Ref<IUser>[]>;
 }
 
-const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>({
-  user: {
-    type: Schema.Types.ObjectId,
-    ref: 'User',
-    index: true,
-    required: true,
-  },
-  targetModel: {
-    type: String,
-    required: true,
-    enum: AllSupportedTargetModels,
-  },
-  target: {
-    type: Schema.Types.ObjectId,
-    ref: 'Page',
-    refPath: 'targetModel',
-    required: true,
+const subscriptionSchema = new Schema<SubscriptionDocument, SubscriptionModel>(
+  {
+    user: {
+      type: Schema.Types.ObjectId,
+      ref: 'User',
+      index: true,
+      required: true,
+    },
+    targetModel: {
+      type: String,
+      required: true,
+      enum: AllSupportedTargetModels,
+    },
+    target: {
+      type: Schema.Types.ObjectId,
+      ref: 'Page',
+      refPath: 'targetModel',
+      required: true,
+    },
+    status: {
+      type: String,
+      required: true,
+      enum: AllSubscriptionStatusType,
+    },
   },
-  status: {
-    type: String,
-    required: true,
-    enum: AllSubscriptionStatusType,
+  {
+    timestamps: true,
   },
-}, {
-  timestamps: true,
-});
+);
 
-subscriptionSchema.methods.isSubscribing = function() {
+subscriptionSchema.methods.isSubscribing = function () {
   return this.status === SubscriptionStatusType.SUBSCRIBE;
 };
 
-subscriptionSchema.methods.isUnsubscribing = function() {
+subscriptionSchema.methods.isUnsubscribing = function () {
   return this.status === SubscriptionStatusType.UNSUBSCRIBE;
 };
 
-subscriptionSchema.statics.findByUserIdAndTargetId = function(userId, targetId) {
+subscriptionSchema.statics.findByUserIdAndTargetId = function (
+  userId,
+  targetId,
+) {
   return this.findOne({ user: userId, target: targetId });
 };
 
-subscriptionSchema.statics.upsertSubscription = function(
-    user: Ref<IUser>, targetModel: SupportedTargetModelType, target: Ref<IPage>, status: SubscriptionStatusType,
+subscriptionSchema.statics.upsertSubscription = function (
+  user: Ref<IUser>,
+  targetModel: SupportedTargetModelType,
+  target: Ref<IPage>,
+  status: SubscriptionStatusType,
 ) {
   const query = { user, targetModel, target };
   const doc = { ...query, status };
   const options = {
-    upsert: true, new: true, setDefaultsOnInsert: true, runValidators: true,
+    upsert: true,
+    new: true,
+    setDefaultsOnInsert: true,
+    runValidators: true,
   };
   return this.findOneAndUpdate(query, doc, options);
 };
 
-subscriptionSchema.statics.subscribeByPageId = function(userId, pageId, status) {
-  return this.upsertSubscription(userId, SupportedTargetModel.MODEL_PAGE, pageId, status);
+subscriptionSchema.statics.subscribeByPageId = function (
+  userId,
+  pageId,
+  status,
+) {
+  return this.upsertSubscription(
+    userId,
+    SupportedTargetModel.MODEL_PAGE,
+    pageId,
+    status,
+  );
 };
 
-subscriptionSchema.statics.getSubscription = async function(target: Ref<IPage>) {
-  return this.find({ target, status: SubscriptionStatusType.SUBSCRIBE }).distinct('user');
+subscriptionSchema.statics.getSubscription = async function (
+  target: Ref<IPage>,
+) {
+  return this.find({
+    target,
+    status: SubscriptionStatusType.SUBSCRIBE,
+  }).distinct('user');
 };
 
-subscriptionSchema.statics.getUnsubscription = async function(target: Ref<IPage>) {
-  return this.find({ target, status: SubscriptionStatusType.UNSUBSCRIBE }).distinct('user');
+subscriptionSchema.statics.getUnsubscription = async function (
+  target: Ref<IPage>,
+) {
+  return this.find({
+    target,
+    status: SubscriptionStatusType.UNSUBSCRIBE,
+  }).distinct('user');
 };
 
-subscriptionSchema.statics.getSubscriptions = async function(targets: Ref<IPage>[]) {
-  return this.find({ target: { $in: targets }, status: SubscriptionStatusType.SUBSCRIBE }).distinct('user');
+subscriptionSchema.statics.getSubscriptions = async function (
+  targets: Ref<IPage>[],
+) {
+  return this.find({
+    target: { $in: targets },
+    status: SubscriptionStatusType.SUBSCRIBE,
+  }).distinct('user');
 };
 
-export default getOrCreateModel<SubscriptionDocument, SubscriptionModel>('Subscription', subscriptionSchema);
+export default getOrCreateModel<SubscriptionDocument, SubscriptionModel>(
+  'Subscription',
+  subscriptionSchema,
+);

+ 18 - 14
apps/app/src/server/models/tag.ts

@@ -1,4 +1,4 @@
-import type { Types, Model } from 'mongoose';
+import type { Model, Types } from 'mongoose';
 import { Schema } from 'mongoose';
 
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
@@ -7,21 +7,19 @@ import { getOrCreateModel } from '../util/mongoose-utils';
 const mongoosePaginate = require('mongoose-paginate-v2');
 const uniqueValidator = require('mongoose-unique-validator');
 
-
 export interface TagDocument {
   _id: Types.ObjectId;
   name: string;
 }
 
-export type IdToNameMap = {[key: string] : string }
-export type IdToNamesMap = {[key: string] : string[] }
+export type IdToNameMap = { [key: string]: string };
+export type IdToNamesMap = { [key: string]: string[] };
 
-export interface TagModel extends Model<TagDocument>{
-  getIdToNameMap(tagIds: ObjectIdLike[]): IdToNameMap
-  findOrCreateMany(tagNames: string[]): Promise<TagDocument[]>
+export interface TagModel extends Model<TagDocument> {
+  getIdToNameMap(tagIds: ObjectIdLike[]): IdToNameMap;
+  findOrCreateMany(tagNames: string[]): Promise<TagDocument[]>;
 }
 
-
 const tagSchema = new Schema<TagDocument, TagModel>({
   name: {
     type: String,
@@ -32,8 +30,9 @@ const tagSchema = new Schema<TagDocument, TagModel>({
 tagSchema.plugin(mongoosePaginate);
 tagSchema.plugin(uniqueValidator);
 
-
-tagSchema.statics.getIdToNameMap = async function(tagIds: ObjectIdLike[]): Promise<IdToNameMap> {
+tagSchema.statics.getIdToNameMap = async function (
+  tagIds: ObjectIdLike[],
+): Promise<IdToNameMap> {
   const tags = await this.find({ _id: { $in: tagIds } });
 
   const idToNameMap = {};
@@ -44,12 +43,18 @@ tagSchema.statics.getIdToNameMap = async function(tagIds: ObjectIdLike[]): Promi
   return idToNameMap;
 };
 
-tagSchema.statics.findOrCreateMany = async function(tagNames: string[]): Promise<TagDocument[]> {
+tagSchema.statics.findOrCreateMany = async function (
+  tagNames: string[],
+): Promise<TagDocument[]> {
   const existTags = await this.find({ name: { $in: tagNames } });
-  const existTagNames = existTags.map((tag) => { return tag.name });
+  const existTagNames = existTags.map((tag) => {
+    return tag.name;
+  });
 
   // bulk insert
-  const tagsToCreate = tagNames.filter((tagName) => { return !existTagNames.includes(tagName) });
+  const tagsToCreate = tagNames.filter((tagName) => {
+    return !existTagNames.includes(tagName);
+  });
   await this.insertMany(
     tagsToCreate.map((tag) => {
       return { name: tag };
@@ -59,5 +64,4 @@ tagSchema.statics.findOrCreateMany = async function(tagNames: string[]): Promise
   return this.find({ name: { $in: tagNames } });
 };
 
-
 export default getOrCreateModel<TagDocument, TagModel>('Tag', tagSchema);

+ 19 - 12
apps/app/src/server/models/transfer-key.ts

@@ -1,4 +1,4 @@
-import type { Model, HydratedDocument } from 'mongoose';
+import type { HydratedDocument, Model } from 'mongoose';
 import { Schema } from 'mongoose';
 
 import type { ITransferKey } from '~/interfaces/transfer-key';
@@ -6,24 +6,31 @@ import type { ITransferKey } from '~/interfaces/transfer-key';
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 interface ITransferKeyMethods {
-  findOneActiveTransferKey(key: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
+  findOneActiveTransferKey(
+    key: string,
+  ): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null>;
 }
 
 type TransferKeyModel = Model<ITransferKey, any, ITransferKeyMethods>;
 
-const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>({
-  expireAt: { type: Date, default: () => new Date(), expires: '30m' },
-  keyString: { type: String, unique: true }, // original key string
-  key: { type: String, unique: true },
-}, {
-  timestamps: {
-    createdAt: true,
-    updatedAt: false,
+const schema = new Schema<ITransferKey, TransferKeyModel, ITransferKeyMethods>(
+  {
+    expireAt: { type: Date, default: () => new Date(), expires: '30m' },
+    keyString: { type: String, unique: true }, // original key string
+    key: { type: String, unique: true },
   },
-});
+  {
+    timestamps: {
+      createdAt: true,
+      updatedAt: false,
+    },
+  },
+);
 
 // TODO: validate createdAt
-schema.statics.findOneActiveTransferKey = async function(key: string): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null> {
+schema.statics.findOneActiveTransferKey = async function (
+  key: string,
+): Promise<HydratedDocument<ITransferKey, ITransferKeyMethods> | null> {
   return this.findOne({ key });
 };
 

+ 48 - 35
apps/app/src/server/models/update-post.ts

@@ -1,50 +1,56 @@
 /* eslint-disable @typescript-eslint/no-explicit-any */
 
-import type { Types, Model, Document } from 'mongoose';
+import type { Document, Model, Types } from 'mongoose';
 import { Schema } from 'mongoose';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
 export interface IUpdatePost {
-  pathPattern: string
-  patternPrefix: string
-  patternPrefix2: string
-  channel: string
-  provider: string
-  creator: Types.ObjectId
-  createdAt: Date
+  pathPattern: string;
+  patternPrefix: string;
+  patternPrefix2: string;
+  channel: string;
+  provider: string;
+  creator: Types.ObjectId;
+  createdAt: Date;
 }
 
 export interface UpdatePostDocument extends IUpdatePost, Document {}
 
 export interface UpdatePostModel extends Model<UpdatePostDocument> {
-  normalizeChannelName(channel): any
-  createPrefixesByPathPattern(pathPattern): any
-  getRegExpByPattern(pattern): any
-  findSettingsByPath(path): Promise<UpdatePostDocument[]>
-  findAll(offset?: number): Promise<UpdatePostDocument[]>
-  createUpdatePost(pathPattern: string, channel: string, creator: Types.ObjectId): Promise<UpdatePostDocument>
+  normalizeChannelName(channel): any;
+  createPrefixesByPathPattern(pathPattern): any;
+  getRegExpByPattern(pattern): any;
+  findSettingsByPath(path): Promise<UpdatePostDocument[]>;
+  findAll(offset?: number): Promise<UpdatePostDocument[]>;
+  createUpdatePost(
+    pathPattern: string,
+    channel: string,
+    creator: Types.ObjectId,
+  ): Promise<UpdatePostDocument>;
 }
 
 /**
  * This is the setting for notify to 3rd party tool (like Slack).
  */
-const updatePostSchema = new Schema<UpdatePostDocument, UpdatePostModel>({
-  pathPattern: { type: String, required: true },
-  patternPrefix: { type: String, required: true },
-  patternPrefix2: { type: String, required: true },
-  channel: { type: String, required: true },
-  provider: { type: String, required: true },
-  creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
-}, {
-  timestamps: true,
-});
-
-updatePostSchema.statics.normalizeChannelName = function(channel) {
-  return channel.replace(/(#|,)/g, '');
-};
-
-updatePostSchema.statics.createPrefixesByPathPattern = function(pathPattern) {
+const updatePostSchema = new Schema<UpdatePostDocument, UpdatePostModel>(
+  {
+    pathPattern: { type: String, required: true },
+    patternPrefix: { type: String, required: true },
+    patternPrefix2: { type: String, required: true },
+    channel: { type: String, required: true },
+    provider: { type: String, required: true },
+    creator: { type: Schema.Types.ObjectId, ref: 'User', index: true },
+  },
+  {
+    timestamps: true,
+  },
+);
+
+updatePostSchema.statics.normalizeChannelName = (channel) =>
+  channel.replace(/(#|,)/g, '');
+
+updatePostSchema.statics.createPrefixesByPathPattern = (pathPattern) => {
   const patternPrefix = ['*', '*'];
 
   // not begin with slash
@@ -64,7 +70,7 @@ updatePostSchema.statics.createPrefixesByPathPattern = function(pathPattern) {
   return patternPrefix;
 };
 
-updatePostSchema.statics.getRegExpByPattern = function(pattern) {
+updatePostSchema.statics.getRegExpByPattern = (pattern) => {
   let reg = pattern;
   if (!reg.match(/^\/.*/)) {
     reg = `/*${reg}*`;
@@ -76,7 +82,7 @@ updatePostSchema.statics.getRegExpByPattern = function(pattern) {
   return new RegExp(reg);
 };
 
-updatePostSchema.statics.findSettingsByPath = async function(path) {
+updatePostSchema.statics.findSettingsByPath = async function (path) {
   const prefixes = this.createPrefixesByPathPattern(path);
 
   const settings = await this.find({
@@ -100,11 +106,15 @@ updatePostSchema.statics.findSettingsByPath = async function(path) {
 };
 
 // eslint-disable-next-line @typescript-eslint/no-unused-vars
-updatePostSchema.statics.findAll = function(offset = 0) {
+updatePostSchema.statics.findAll = function (offset = 0) {
   return this.find().sort({ createdAt: 1 }).populate('creator').exec();
 };
 
-updatePostSchema.statics.createUpdatePost = async function(pathPattern, channel, creator) {
+updatePostSchema.statics.createUpdatePost = async function (
+  pathPattern,
+  channel,
+  creator,
+) {
   const provider = 'slack'; // now slack only
 
   const prefixes = this.createPrefixesByPathPattern(pathPattern);
@@ -119,4 +129,7 @@ updatePostSchema.statics.createUpdatePost = async function(pathPattern, channel,
   });
 };
 
-export default getOrCreateModel<UpdatePostDocument, UpdatePostModel>('UpdatePost', updatePostSchema);
+export default getOrCreateModel<UpdatePostDocument, UpdatePostModel>(
+  'UpdatePost',
+  updatePostSchema,
+);

+ 135 - 100
apps/app/src/server/models/user-group-relation.ts

@@ -1,8 +1,6 @@
-import {
-  getIdForRef, isPopulated,
-} from '@growi/core';
+import { getIdForRef, isPopulated } from '@growi/core';
 import type { IUserGroupRelation } from '@growi/core/dist/interfaces';
-import type { Model, Document } from 'mongoose';
+import type { Document, Model } from 'mongoose';
 import mongoose, { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 import uniqueValidator from 'mongoose-unique-validator';
@@ -11,48 +9,64 @@ import loggerFactory from '~/utils/logger';
 
 import type { ObjectIdLike } from '../interfaces/mongoose-utils';
 import { getOrCreateModel } from '../util/mongoose-utils';
-
 import type { UserGroupDocument } from './user-group';
 
 const logger = loggerFactory('growi:models:userGroupRelation');
 
+export interface UserGroupRelationDocument
+  extends IUserGroupRelation,
+    Document {}
 
-export interface UserGroupRelationDocument extends IUserGroupRelation, Document {}
-
-export interface UserGroupRelationModel extends Model<UserGroupRelationDocument> {
-  [x:string]: any, // for old methods
+export interface UserGroupRelationModel
+  extends Model<UserGroupRelationDocument> {
+  [x: string]: any; // for old methods
 
-  PAGE_ITEMS: 50,
+  PAGE_ITEMS: 50;
 
-  removeAllByUserGroups: (groupsToDelete: UserGroupDocument[]) => Promise<any>,
+  removeAllByUserGroups: (groupsToDelete: UserGroupDocument[]) => Promise<any>;
 
-  findAllUserIdsForUserGroups: (userGroupIds: ObjectIdLike[]) => Promise<string[]>,
+  findAllUserIdsForUserGroups: (
+    userGroupIds: ObjectIdLike[],
+  ) => Promise<string[]>;
 
-  findGroupsWithDescendantsByGroupAndUser: (group: UserGroupDocument, user) => Promise<UserGroupDocument[]>,
+  findGroupsWithDescendantsByGroupAndUser: (
+    group: UserGroupDocument,
+    user,
+  ) => Promise<UserGroupDocument[]>;
 
-  countByGroupIdsAndUser: (userGroupIds: ObjectIdLike[], userData) => Promise<number>
+  countByGroupIdsAndUser: (
+    userGroupIds: ObjectIdLike[],
+    userData,
+  ) => Promise<number>;
 
-  findAllGroupsForUser: (user) => Promise<UserGroupDocument[]>
+  findAllGroupsForUser: (user) => Promise<UserGroupDocument[]>;
 
-  findAllUserGroupIdsRelatedToUser: (user) => Promise<ObjectIdLike[]>
+  findAllUserGroupIdsRelatedToUser: (user) => Promise<ObjectIdLike[]>;
 }
 
 /*
  * define schema
  */
-const schema = new Schema<UserGroupRelationDocument, UserGroupRelationModel>({
-  relatedGroup: { type: Schema.Types.ObjectId, ref: 'UserGroup', required: true },
-  relatedUser: { type: Schema.Types.ObjectId, ref: 'User', required: true },
-}, {
-  timestamps: { createdAt: true, updatedAt: false },
-});
+const schema = new Schema<UserGroupRelationDocument, UserGroupRelationModel>(
+  {
+    relatedGroup: {
+      type: Schema.Types.ObjectId,
+      ref: 'UserGroup',
+      required: true,
+    },
+    relatedUser: { type: Schema.Types.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() {
+schema.statics.removeAllInvalidRelations = function () {
   return this.findAllRelation()
     .then((relations) => {
       // filter invalid documents
@@ -61,24 +75,22 @@ schema.statics.removeAllInvalidRelations = function() {
       });
     })
     .then((invalidRelations) => {
-      const ids = invalidRelations.map((relation) => { return relation._id });
+      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
+ *
+ * @static
+ * @returns {Promise<UserGroupRelation[]>}
+ * @memberof UserGroupRelation
+ */
+schema.statics.findAllRelation = function () {
+  return this.find().populate('relatedUser').populate('relatedGroup').exec();
 };
 
 /**
@@ -89,22 +101,20 @@ schema.statics.findAllRelation = function() {
  * @returns {Promise<UserGroupRelation[]>}
  * @memberof UserGroupRelation
  */
-schema.statics.findAllRelationForUserGroup = function(userGroup) {
+schema.statics.findAllRelationForUserGroup = function (userGroup) {
   logger.debug('findAllRelationForUserGroup is called', userGroup);
-  return this
-    .find({ relatedGroup: userGroup })
-    .populate('relatedUser')
-    .exec();
+  return this.find({ relatedGroup: userGroup }).populate('relatedUser').exec();
 };
 
-schema.statics.findAllUserIdsForUserGroups = async function(userGroupIds: ObjectIdLike[]): Promise<string[]> {
-  const relations = await this
-    .find({ relatedGroup: { $in: userGroupIds } })
+schema.statics.findAllUserIdsForUserGroups = async function (
+  userGroupIds: ObjectIdLike[],
+): Promise<string[]> {
+  const relations = await this.find({ relatedGroup: { $in: userGroupIds } })
     .select('relatedUser')
     .exec();
 
   // return unique ids
-  return [...new Set(relations.map(r => r.relatedUser.toString()))];
+  return [...new Set(relations.map((r) => r.relatedUser.toString()))];
 };
 
 /**
@@ -115,9 +125,8 @@ schema.statics.findAllUserIdsForUserGroups = async function(userGroupIds: Object
  * @returns {Promise<UserGroupRelation[]>}
  * @memberof UserGroupRelation
  */
-schema.statics.findAllRelationForUserGroups = function(userGroups) {
-  return this
-    .find({ relatedGroup: { $in: userGroups } })
+schema.statics.findAllRelationForUserGroups = function (userGroups) {
+  return this.find({ relatedGroup: { $in: userGroups } })
     .populate('relatedUser')
     .exec();
 };
@@ -130,12 +139,20 @@ schema.statics.findAllRelationForUserGroups = function(userGroups) {
  * @returns {Promise<UserGroupDocument[]>}
  * @memberof UserGroupRelation
  */
-schema.statics.findAllGroupsForUser = async function(user): Promise<UserGroupDocument[]> {
-  const userGroupRelations = await this.find({ relatedUser: user._id }).populate('relatedGroup');
+schema.statics.findAllGroupsForUser = async function (
+  user,
+): Promise<UserGroupDocument[]> {
+  const userGroupRelations = await this.find({
+    relatedUser: user._id,
+  }).populate('relatedGroup');
   const userGroups = userGroupRelations.map((relation) => {
-    return isPopulated(relation.relatedGroup) ? relation.relatedGroup as unknown as UserGroupDocument : null;
+    return isPopulated(relation.relatedGroup)
+      ? (relation.relatedGroup as unknown as UserGroupDocument)
+      : null;
   });
-  return userGroups.filter((group): group is NonNullable<UserGroupDocument> => group != null);
+  return userGroups.filter(
+    (group): group is NonNullable<UserGroupDocument> => group != null,
+  );
 };
 
 /**
@@ -145,12 +162,16 @@ schema.statics.findAllGroupsForUser = async function(user): Promise<UserGroupDoc
  * @param {User} user
  * @returns {Promise<ObjectId[]>}
  */
-schema.statics.findAllUserGroupIdsRelatedToUser = async function(user): Promise<ObjectIdLike[]> {
+schema.statics.findAllUserGroupIdsRelatedToUser = async function (
+  user,
+): Promise<ObjectIdLike[]> {
   const relations = await this.find({ relatedUser: user._id })
     .select('relatedGroup')
     .exec();
 
-  return relations.map((relation) => { return getIdForRef(relation.relatedGroup) });
+  return relations.map((relation) => {
+    return getIdForRef(relation.relatedGroup);
+  });
 };
 
 /**
@@ -161,7 +182,10 @@ schema.statics.findAllUserGroupIdsRelatedToUser = async function(user): Promise<
  * @param {User} userData find query param for relatedUser
  * @returns {Promise<number>}
  */
-schema.statics.countByGroupIdsAndUser = async function(userGroupIds: ObjectIdLike[], userData): Promise<number> {
+schema.statics.countByGroupIdsAndUser = async function (
+  userGroupIds: ObjectIdLike[],
+  userData,
+): Promise<number> {
   const query = {
     relatedGroup: { $in: userGroupIds },
     relatedUser: userData._id,
@@ -178,7 +202,7 @@ schema.statics.countByGroupIdsAndUser = async function(userGroupIds: ObjectIdLik
  * @returns {Promise<User>}
  * @memberof UserGroupRelation
  */
-schema.statics.findUserByNotRelatedGroup = function(userGroup, queryOptions) {
+schema.statics.findUserByNotRelatedGroup = function (userGroup, queryOptions) {
   const User = mongoose.model('User') as any;
   let searchWord = new RegExp(`${queryOptions.searchWord}`);
   switch (queryOptions.searchType) {
@@ -189,26 +213,27 @@ schema.statics.findUserByNotRelatedGroup = function(userGroup, queryOptions) {
       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 }) }
+  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,
-      };
-
-      logger.debug('findUserByNotRelatedGroup ', query);
-      return User.find(query).exec();
+  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,
+    };
+
+    logger.debug('findUserByNotRelatedGroup ', query);
+    return User.find(query).exec();
+  });
 };
 
 /**
@@ -220,18 +245,17 @@ schema.statics.findUserByNotRelatedGroup = function(userGroup, queryOptions) {
  * @returns {Promise<boolean>} is user related for group(or not)
  * @memberof UserGroupRelation
  */
-schema.statics.isRelatedUserForGroup = function(userGroup, user) {
+schema.statics.isRelatedUserForGroup = function (userGroup, user) {
   const query = {
     relatedGroup: userGroup.id,
     relatedUser: user.id,
   };
 
-  return this
-    .count(query)
+  return this.count(query)
     .exec()
     .then((count) => {
       // return true or false of the relation is exists(not count)
-      return (count > 0);
+      return count > 0;
     });
 };
 
@@ -244,14 +268,14 @@ schema.statics.isRelatedUserForGroup = function(userGroup, user) {
  * @returns {Promise<UserGroupRelation>} created relation
  * @memberof UserGroupRelation
  */
-schema.statics.createRelation = function(userGroup, user) {
+schema.statics.createRelation = function (userGroup, user) {
   return this.create({
     relatedGroup: userGroup.id,
     relatedUser: user.id,
   });
 };
 
-schema.statics.createRelations = async function(userGroupIds, user) {
+schema.statics.createRelations = async function (userGroupIds, user) {
   const documentsToInsertMany = userGroupIds.map((groupId) => {
     return {
       relatedGroup: groupId,
@@ -271,7 +295,9 @@ schema.statics.createRelations = async function(userGroupIds, user) {
  * @returns {Promise<any>}
  * @memberof UserGroupRelation
  */
-schema.statics.removeAllByUserGroups = function(groupsToDelete: UserGroupDocument[]) {
+schema.statics.removeAllByUserGroups = function (
+  groupsToDelete: UserGroupDocument[],
+) {
   return this.deleteMany({ relatedGroup: { $in: groupsToDelete } });
 };
 
@@ -283,25 +309,28 @@ schema.statics.removeAllByUserGroups = function(groupsToDelete: UserGroupDocumen
  * @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.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
+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);
+  return relations.map((relation) => relation.relatedUser);
 };
 
-schema.statics.createByGroupIdsAndUserIds = async function(groupIds, userIds) {
+schema.statics.createByGroupIdsAndUserIds = async function (groupIds, userIds) {
   const insertOperations: any[] = [];
 
   groupIds.forEach((groupId) => {
@@ -327,11 +356,14 @@ schema.statics.createByGroupIdsAndUserIds = async function(groupIds, userIds) {
  * @param {UserDocument} user
  * @returns UserGroupDocument[]
  */
-schema.statics.findGroupsWithDescendantsByGroupAndUser = async function(group: UserGroupDocument, user): Promise<UserGroupDocument[]> {
+schema.statics.findGroupsWithDescendantsByGroupAndUser = async function (
+  group: UserGroupDocument,
+  user,
+): Promise<UserGroupDocument[]> {
   const descendantGroups = [group];
 
-  const incrementGroupsRecursively = async(groups, user) => {
-    const groupIds = groups.map(g => g._id);
+  const incrementGroupsRecursively = async (groups, user) => {
+    const groupIds = groups.map((g) => g._id);
 
     const populatedRelations = await this.aggregate([
       {
@@ -359,7 +391,7 @@ schema.statics.findGroupsWithDescendantsByGroupAndUser = async function(group: U
       },
     ]);
 
-    const nextGroups = populatedRelations.map(d => d.relatedGroup);
+    const nextGroups = populatedRelations.map((d) => d.relatedGroup);
 
     // End
     const shouldEnd = nextGroups.length === 0;
@@ -378,4 +410,7 @@ schema.statics.findGroupsWithDescendantsByGroupAndUser = async function(group: U
   return descendantGroups;
 };
 
-export default getOrCreateModel<UserGroupRelationDocument, UserGroupRelationModel>('UserGroupRelation', schema);
+export default getOrCreateModel<
+  UserGroupRelationDocument,
+  UserGroupRelationModel
+>('UserGroupRelation', schema);

+ 50 - 30
apps/app/src/server/models/user-group.ts

@@ -1,36 +1,41 @@
 import type { IUserGroup } from '@growi/core';
-import type { Model, Document } from 'mongoose';
+import type { Document, Model } from 'mongoose';
 import { Schema } from 'mongoose';
 import mongoosePaginate from 'mongoose-paginate-v2';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface UserGroupDocument extends IUserGroup, Document {}
 
 export interface UserGroupModel extends Model<UserGroupDocument> {
-  [x:string]: any, // for old methods
+  [x: string]: any; // for old methods
 
-  PAGE_ITEMS: 10,
+  PAGE_ITEMS: 10;
 
-  findGroupsWithDescendantsRecursively: (groups: UserGroupDocument[], descendants?: UserGroupDocument[]) => Promise<UserGroupDocument[]>,
+  findGroupsWithDescendantsRecursively: (
+    groups: UserGroupDocument[],
+    descendants?: UserGroupDocument[],
+  ) => Promise<UserGroupDocument[]>;
 }
 
 /*
  * define schema
  */
-const schema = new Schema<UserGroupDocument, UserGroupModel>({
-  name: { type: String, required: true, unique: true },
-  parent: { type: Schema.Types.ObjectId, ref: 'UserGroup', index: true },
-  description: { type: String, default: '' },
-}, {
-  timestamps: true,
-});
+const schema = new Schema<UserGroupDocument, UserGroupModel>(
+  {
+    name: { type: String, required: true, unique: true },
+    parent: { type: Schema.Types.ObjectId, ref: 'UserGroup', index: true },
+    description: { type: String, default: '' },
+  },
+  {
+    timestamps: true,
+  },
+);
 schema.plugin(mongoosePaginate);
 
 const PAGE_ITEMS = 10;
 
-schema.statics.findWithPagination = function(opts) {
+schema.statics.findWithPagination = function (opts) {
   const query = { parent: null };
   const options = Object.assign({}, opts);
   if (options.page == null) {
@@ -40,20 +45,23 @@ schema.statics.findWithPagination = function(opts) {
     options.limit = PAGE_ITEMS;
   }
 
-  return this.paginate(query, options)
-    .catch((err) => {
-      // debug('Error on pagination:', err); TODO: add logger
-    });
+  return this.paginate(query, options).catch((err) => {
+    // debug('Error on pagination:', err); TODO: add logger
+  });
 };
 
-
-schema.statics.findChildrenByParentIds = async function(parentIds: string[], includeGrandChildren = false) {
+schema.statics.findChildrenByParentIds = async function (
+  parentIds: string[],
+  includeGrandChildren = false,
+) {
   const childUserGroups = await this.find({ parent: { $in: parentIds } });
 
   let grandChildUserGroups: UserGroupDocument[] | null = null;
   if (includeGrandChildren) {
-    const childUserGroupIds = childUserGroups.map(group => group._id);
-    grandChildUserGroups = await this.find({ parent: { $in: childUserGroupIds } });
+    const childUserGroupIds = childUserGroups.map((group) => group._id);
+    grandChildUserGroups = await this.find({
+      parent: { $in: childUserGroupIds },
+    });
   }
 
   return {
@@ -62,11 +70,11 @@ schema.statics.findChildrenByParentIds = async function(parentIds: string[], inc
   };
 };
 
-schema.statics.countUserGroups = function() {
+schema.statics.countUserGroups = function () {
   return this.estimatedDocumentCount();
 };
 
-schema.statics.createGroup = async function(name, description, parentId) {
+schema.statics.createGroup = async function (name, description, parentId) {
   let parent: UserGroupDocument | null = null;
   if (parentId != null) {
     parent = await this.findOne({ _id: parentId });
@@ -85,7 +93,10 @@ schema.statics.createGroup = async function(name, description, parentId) {
  * @param ancestors UserGroupDocument[]
  * @returns UserGroupDocument[]
  */
-schema.statics.findGroupsWithAncestorsRecursively = async function(group, ancestors = [group]) {
+schema.statics.findGroupsWithAncestorsRecursively = async function (
+  group,
+  ancestors = [group],
+) {
   if (group == null) {
     return ancestors;
   }
@@ -108,19 +119,25 @@ schema.statics.findGroupsWithAncestorsRecursively = async function(group, ancest
  * @param descendants UserGroupDocument[]
  * @returns UserGroupDocument[]
  */
-schema.statics.findGroupsWithDescendantsRecursively = async function(
-    groups: UserGroupDocument[], descendants: UserGroupDocument[] = groups,
+schema.statics.findGroupsWithDescendantsRecursively = async function (
+  groups: UserGroupDocument[],
+  descendants: UserGroupDocument[] = groups,
 ): Promise<UserGroupDocument[]> {
-  const nextGroups = await this.find({ parent: { $in: groups.map(g => g._id) } });
+  const nextGroups = await this.find({
+    parent: { $in: groups.map((g) => g._id) },
+  });
 
   if (nextGroups.length === 0) {
     return descendants;
   }
 
-  return this.findGroupsWithDescendantsRecursively(nextGroups, descendants.concat(nextGroups));
+  return this.findGroupsWithDescendantsRecursively(
+    nextGroups,
+    descendants.concat(nextGroups),
+  );
 };
 
-schema.statics.findGroupsWithDescendantsById = async function(groupId) {
+schema.statics.findGroupsWithDescendantsById = async function (groupId) {
   const root = await this.findOne({ _id: groupId });
   if (root == null) {
     throw Error('The root user group does not exist');
@@ -128,4 +145,7 @@ schema.statics.findGroupsWithDescendantsById = async function(groupId) {
   return this.findGroupsWithDescendantsRecursively([root]);
 };
 
-export default getOrCreateModel<UserGroupDocument, UserGroupModel>('UserGroup', schema);
+export default getOrCreateModel<UserGroupDocument, UserGroupModel>(
+  'UserGroup',
+  schema,
+);

+ 38 - 30
apps/app/src/server/models/user-registration-order.ts

@@ -1,55 +1,60 @@
 import crypto from 'crypto';
-
 import { addHours } from 'date-fns/addHours';
-import type { Model, Document } from 'mongoose';
-import {
-  Schema,
-} from 'mongoose';
+import type { Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 import uniqueValidator from 'mongoose-unique-validator';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface IUserRegistrationOrder {
-  token: string,
-  email: string,
-  isRevoked: boolean,
-  createdAt: Date,
-  expiredAt: Date,
+  token: string;
+  email: string;
+  isRevoked: boolean;
+  createdAt: Date;
+  expiredAt: Date;
 }
 
-export interface UserRegistrationOrderDocument extends IUserRegistrationOrder, Document {
-  isExpired(): boolean
-  revokeOneTimeToken(): Promise<void>
+export interface UserRegistrationOrderDocument
+  extends IUserRegistrationOrder,
+    Document {
+  isExpired(): boolean;
+  revokeOneTimeToken(): Promise<void>;
 }
 
-export interface UserRegistrationOrderModel extends Model<UserRegistrationOrderDocument> {
-  generateOneTimeToken(): string
-  createUserRegistrationOrder(email: string): UserRegistrationOrderDocument
+export interface UserRegistrationOrderModel
+  extends Model<UserRegistrationOrderDocument> {
+  generateOneTimeToken(): string;
+  createUserRegistrationOrder(email: string): UserRegistrationOrderDocument;
 }
 
 const expiredAt = (): Date => {
   return addHours(new Date(), 1);
 };
 
-const schema = new Schema<UserRegistrationOrderDocument, UserRegistrationOrderModel>({
-  token: { type: String, required: true, unique: true },
-  email: { type: String, required: true },
-  isRevoked: { type: Boolean, default: false, required: true },
-  expiredAt: { type: Date, default: expiredAt, required: true },
-}, {
-  timestamps: true,
-});
+const schema = new Schema<
+  UserRegistrationOrderDocument,
+  UserRegistrationOrderModel
+>(
+  {
+    token: { type: String, required: true, unique: true },
+    email: { type: String, required: true },
+    isRevoked: { type: Boolean, default: false, required: true },
+    expiredAt: { type: Date, default: expiredAt, required: true },
+  },
+  {
+    timestamps: true,
+  },
+);
 schema.plugin(uniqueValidator);
 
-schema.statics.generateOneTimeToken = function() {
+schema.statics.generateOneTimeToken = () => {
   const buf = crypto.randomBytes(256);
   const token = buf.toString('hex');
 
   return token;
 };
 
-schema.statics.createUserRegistrationOrder = async function(email) {
+schema.statics.createUserRegistrationOrder = async function (email) {
   let token;
   let duplicateToken;
 
@@ -64,13 +69,16 @@ schema.statics.createUserRegistrationOrder = async function(email) {
   return userRegistrationOrderData;
 };
 
-schema.methods.isExpired = function() {
+schema.methods.isExpired = function () {
   return this.expiredAt.getTime() < Date.now();
 };
 
-schema.methods.revokeOneTimeToken = async function() {
+schema.methods.revokeOneTimeToken = async function () {
   this.isRevoked = true;
   return this.save();
 };
 
-export default getOrCreateModel<UserRegistrationOrderDocument, UserRegistrationOrderModel>('UserRegistrationOrder', schema);
+export default getOrCreateModel<
+  UserRegistrationOrderDocument,
+  UserRegistrationOrderModel
+>('UserRegistrationOrder', schema);

+ 9 - 11
apps/app/src/server/models/user-ui-settings.ts

@@ -1,20 +1,16 @@
-import type { Ref, IUser } from '@growi/core';
-import type { Model, Document } from 'mongoose';
-import {
-  Schema,
-} from 'mongoose';
-
+import type { IUser, Ref } from '@growi/core';
+import type { Document, Model } from 'mongoose';
+import { Schema } from 'mongoose';
 
 import { SidebarContentsType } from '~/interfaces/ui';
 import type { IUserUISettings } from '~/interfaces/user-ui-settings';
 
 import { getOrCreateModel } from '../util/mongoose-utils';
 
-
 export interface UserUISettingsDocument extends IUserUISettings, Document {
-  user: Ref<IUser>,
+  user: Ref<IUser>;
 }
-export type UserUISettingsModel = Model<UserUISettingsDocument>
+export type UserUISettingsModel = Model<UserUISettingsDocument>;
 
 const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({
   user: { type: Schema.Types.ObjectId, ref: 'User', unique: true },
@@ -27,5 +23,7 @@ const schema = new Schema<UserUISettingsDocument, UserUISettingsModel>({
   preferCollapsedModeByUser: { type: Boolean, default: false },
 });
 
-
-export default getOrCreateModel<UserUISettingsDocument, UserUISettingsModel>('UserUISettings', schema);
+export default getOrCreateModel<UserUISettingsDocument, UserUISettingsModel>(
+  'UserUISettings',
+  schema,
+);

+ 230 - 154
apps/app/src/server/models/user.js

@@ -10,10 +10,8 @@ import loggerFactory from '~/utils/logger';
 import { aclService } from '../service/acl';
 import { configManager } from '../service/config-manager';
 import { getModelSafely } from '../util/mongoose-utils';
-
 import { Attachment } from './attachment';
 
-
 const crypto = require('crypto');
 
 const mongoose = require('mongoose');
@@ -24,7 +22,6 @@ const logger = loggerFactory('growi:models:user');
 
 /** @param {import('~/server/crowi').default | null} crowi Crowi instance */
 const factory = (crowi) => {
-
   const userModelExists = getModelSafely('User');
   if (userModelExists != null) {
     return userModelExists;
@@ -35,8 +32,9 @@ const factory = (crowi) => {
   const STATUS_SUSPENDED = 3;
   const STATUS_DELETED = 4;
   const STATUS_INVITED = 5;
-  const USER_FIELDS_EXCEPT_CONFIDENTIAL = '_id image isEmailPublished isGravatarEnabled googleId name username email introduction'
-  + ' status lang createdAt lastLoginAt admin imageUrlCached';
+  const USER_FIELDS_EXCEPT_CONFIDENTIAL =
+    '_id image isEmailPublished isGravatarEnabled googleId name username email introduction' +
+    ' status lang createdAt lastLoginAt admin imageUrlCached';
 
   const PAGE_ITEMS = 50;
 
@@ -48,53 +46,63 @@ const factory = (crowi) => {
     userEvent.on('activated', userEvent.onActivated);
   }
 
-
-  const userSchema = new mongoose.Schema({
-    userId: String,
-    image: String,
-    imageAttachment: { type: mongoose.Schema.Types.ObjectId, ref: 'Attachment' },
-    imageUrlCached: String,
-    isGravatarEnabled: { type: Boolean, default: false },
-    isEmailPublished: { type: Boolean, default: true },
-    googleId: String,
-    name: { type: String, index: true },
-    username: { type: String, required: true, unique: true },
-    email: { type: String, unique: true, sparse: true },
-    slackMemberId: { type: String, unique: true, sparse: true },
-    // === Crowi settings
-    // username: { type: String, index: true },
-    // email: { type: String, required: true, index: true },
-    // === crowi-plus (>= 2.1.0, <2.3.0) settings
-    // email: { type: String, required: true, unique: true },
-    introduction: String,
-    password: String,
-    apiToken: { type: String, index: true },
-    lang: {
-      type: String,
-      enum: i18n.locales,
-      default: 'en_US',
-    },
-    status: {
-      type: Number, required: true, default: STATUS_ACTIVE, index: true,
+  const userSchema = new mongoose.Schema(
+    {
+      userId: String,
+      image: String,
+      imageAttachment: {
+        type: mongoose.Schema.Types.ObjectId,
+        ref: 'Attachment',
+      },
+      imageUrlCached: String,
+      isGravatarEnabled: { type: Boolean, default: false },
+      isEmailPublished: { type: Boolean, default: true },
+      googleId: String,
+      name: { type: String, index: true },
+      username: { type: String, required: true, unique: true },
+      email: { type: String, unique: true, sparse: true },
+      slackMemberId: { type: String, unique: true, sparse: true },
+      // === Crowi settings
+      // username: { type: String, index: true },
+      // email: { type: String, required: true, index: true },
+      // === crowi-plus (>= 2.1.0, <2.3.0) settings
+      // email: { type: String, required: true, unique: true },
+      introduction: String,
+      password: String,
+      apiToken: { type: String, index: true },
+      lang: {
+        type: String,
+        enum: i18n.locales,
+        default: 'en_US',
+      },
+      status: {
+        type: Number,
+        required: true,
+        default: STATUS_ACTIVE,
+        index: true,
+      },
+      lastLoginAt: { type: Date, index: true },
+      admin: { type: Boolean, default: 0, index: true },
+      readOnly: { type: Boolean, default: 0 },
+      isInvitationEmailSended: { type: Boolean, default: false },
     },
-    lastLoginAt: { type: Date, index: true },
-    admin: { type: Boolean, default: 0, index: true },
-    readOnly: { type: Boolean, default: 0 },
-    isInvitationEmailSended: { type: Boolean, default: false },
-  }, {
-    timestamps: true,
-    toObject: {
-      transform: (doc, ret, opt) => {
-        return omitInsecureAttributes(ret);
+    {
+      timestamps: true,
+      toObject: {
+        transform: (doc, ret, opt) => {
+          return omitInsecureAttributes(ret);
+        },
       },
     },
-  });
+  );
   userSchema.plugin(mongoosePaginate);
   userSchema.plugin(uniqueValidator);
 
   function validateCrowi() {
     if (crowi == null) {
-      throw new Error('"crowi" is null. Init User model with "crowi" argument first.');
+      throw new Error(
+        '"crowi" is null. Init User model with "crowi" argument first.',
+      );
     }
   }
 
@@ -107,7 +115,9 @@ const factory = (crowi) => {
     }
 
     // status decided depends on registrationMode
-    const registrationMode = configManager.getConfig('security:registrationMode');
+    const registrationMode = configManager.getConfig(
+      'security:registrationMode',
+    );
     switch (registrationMode) {
       case aclService.labels.SECURITY_REGISTRATION_MODE_OPEN:
         return STATUS_ACTIVE;
@@ -120,7 +130,8 @@ const factory = (crowi) => {
   }
 
   function generateRandomTempPassword() {
-    const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!=-_';
+    const chars =
+      'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!=-_';
     let password = '';
     const len = 12;
 
@@ -148,20 +159,18 @@ const factory = (crowi) => {
 
   function generateApiToken(user) {
     const hasher = crypto.createHash('sha256');
-    hasher.update((new Date()).getTime() + user._id);
+    hasher.update(new Date().getTime() + user._id);
 
     return hasher.digest('base64');
   }
 
-  userSchema.methods.isUniqueEmail = async function() {
+  userSchema.methods.isUniqueEmail = async function () {
     const query = this.model('User').find();
 
-    const count = await query.count((
-      {
-        username: { $ne: this.username },
-        email: this.email,
-      }
-    ));
+    const count = await query.count({
+      username: { $ne: this.username },
+      email: this.email,
+    });
 
     if (count > 0) {
       return false;
@@ -169,66 +178,66 @@ const factory = (crowi) => {
     return true;
   };
 
-  userSchema.methods.isPasswordSet = function() {
+  userSchema.methods.isPasswordSet = function () {
     if (this.password) {
       return true;
     }
     return false;
   };
 
-  userSchema.methods.isPasswordValid = function(password) {
+  userSchema.methods.isPasswordValid = function (password) {
     return this.password === generatePassword(password);
   };
 
-  userSchema.methods.setPassword = function(password) {
+  userSchema.methods.setPassword = function (password) {
     this.password = generatePassword(password);
     return this;
   };
 
-  userSchema.methods.isEmailSet = function() {
+  userSchema.methods.isEmailSet = function () {
     if (this.email) {
       return true;
     }
     return false;
   };
 
-  userSchema.methods.updateLastLoginAt = function(lastLoginAt, callback) {
+  userSchema.methods.updateLastLoginAt = function (lastLoginAt, callback) {
     this.lastLoginAt = lastLoginAt;
     this.save((err, userData) => {
       return callback(err, userData);
     });
   };
 
-  userSchema.methods.updateIsGravatarEnabled = async function(isGravatarEnabled) {
+  userSchema.methods.updateIsGravatarEnabled = async function (
+    isGravatarEnabled,
+  ) {
     this.isGravatarEnabled = isGravatarEnabled;
     await this.updateImageUrlCached();
     const userData = await this.save();
     return userData;
   };
 
-  userSchema.methods.updatePassword = async function(password) {
+  userSchema.methods.updatePassword = async function (password) {
     this.setPassword(password);
     const userData = await this.save();
     return userData;
   };
 
-  userSchema.methods.updateApiToken = async function() {
-    const self = this;
-
-    self.apiToken = generateApiToken(this);
-    const userData = await self.save();
+  userSchema.methods.updateApiToken = async function () {
+    this.apiToken = generateApiToken(this);
+    const userData = await this.save();
     return userData;
   };
 
   // TODO: create UserService and transplant this method because image uploading depends on AttachmentService
-  userSchema.methods.updateImage = async function(attachment) {
+  userSchema.methods.updateImage = async function (attachment) {
     this.imageAttachment = attachment;
     await this.updateImageUrlCached();
     return this.save();
   };
 
   // TODO: create UserService and transplant this method because image deletion depends on AttachmentService
-  userSchema.methods.deleteImage = async function() {
+  userSchema.methods.deleteImage = async function () {
     validateCrowi();
 
     // the 'image' field became DEPRECATED in v3.3.8
@@ -244,11 +253,11 @@ const factory = (crowi) => {
     return this.save();
   };
 
-  userSchema.methods.updateImageUrlCached = async function() {
+  userSchema.methods.updateImageUrlCached = async function () {
     this.imageUrlCached = await this.generateImageUrlCached();
   };
 
-  userSchema.methods.generateImageUrlCached = async function() {
+  userSchema.methods.generateImageUrlCached = async function () {
     if (this.isGravatarEnabled) {
       return generateGravatarSrc(this.email);
     }
@@ -262,23 +271,29 @@ const factory = (crowi) => {
     return '/images/icons/user.svg';
   };
 
-  userSchema.methods.updateGoogleId = function(googleId, callback) {
+  userSchema.methods.updateGoogleId = function (googleId, callback) {
     this.googleId = googleId;
     this.save((err, userData) => {
       return callback(err, userData);
     });
   };
 
-  userSchema.methods.deleteGoogleId = function(callback) {
+  userSchema.methods.deleteGoogleId = function (callback) {
     return this.updateGoogleId(null, callback);
   };
 
-  userSchema.methods.activateInvitedUser = async function(username, name, password) {
+  userSchema.methods.activateInvitedUser = async function (
+    username,
+    name,
+    password,
+  ) {
     this.setPassword(password);
     this.name = name;
     this.username = username;
     this.status = STATUS_ACTIVE;
-    this.isEmailPublished = configManager.getConfig('customize:isEmailPublishedForNewUser');
+    this.isEmailPublished = configManager.getConfig(
+      'customize:isEmailPublishedForNewUser',
+    );
 
     this.save((err, userData) => {
       userEvent.emit('activated', userData);
@@ -289,58 +304,61 @@ const factory = (crowi) => {
     });
   };
 
-  userSchema.methods.grantAdmin = async function() {
+  userSchema.methods.grantAdmin = async function () {
     logger.debug('Grant Admin', this);
     this.admin = 1;
     return this.save();
   };
 
-  userSchema.methods.revokeAdmin = async function() {
+  userSchema.methods.revokeAdmin = async function () {
     logger.debug('Revove admin', this);
     this.admin = 0;
     return this.save();
   };
 
-  userSchema.methods.grantReadOnly = async function() {
+  userSchema.methods.grantReadOnly = async function () {
     logger.debug('Grant read only access', this);
     this.readOnly = 1;
     return this.save();
   };
 
-  userSchema.methods.revokeReadOnly = async function() {
+  userSchema.methods.revokeReadOnly = async function () {
     logger.debug('Revoke read only access', this);
     this.readOnly = 0;
     return this.save();
   };
 
-  userSchema.methods.asyncGrantAdmin = async function(callback) {
+  userSchema.methods.asyncGrantAdmin = async function (callback) {
     this.admin = 1;
     return this.save();
   };
 
-  userSchema.methods.statusActivate = async function() {
+  userSchema.methods.statusActivate = async function () {
     logger.debug('Activate User', this);
     this.status = STATUS_ACTIVE;
     const userData = await this.save();
     return userEvent.emit('activated', userData);
   };
 
-  userSchema.methods.statusSuspend = async function() {
+  userSchema.methods.statusSuspend = async function () {
     logger.debug('Suspend User', this);
     this.status = STATUS_SUSPENDED;
-    if (this.email === undefined || this.email === null) { // migrate old data
+    if (this.email === undefined || this.email === null) {
+      // migrate old data
       this.email = '-';
     }
-    if (this.name === undefined || this.name === null) { // migrate old data
+    if (this.name === undefined || this.name === null) {
+      // migrate old data
       this.name = `-${Date.now()}`;
     }
-    if (this.username === undefined || this.usename === null) { // migrate old data
+    if (this.username === undefined || this.usename === null) {
+      // migrate old data
       this.username = '-';
     }
     return this.save();
   };
 
-  userSchema.methods.statusDelete = async function() {
+  userSchema.methods.statusDelete = async function () {
     logger.debug('Delete User', this);
 
     const now = new Date();
@@ -357,7 +375,7 @@ const factory = (crowi) => {
     return this.save();
   };
 
-  userSchema.statics.getUserStatusLabels = function() {
+  userSchema.statics.getUserStatusLabels = () => {
     const userStatus = {};
     userStatus[STATUS_REGISTERED] = 'Approval Pending';
     userStatus[STATUS_ACTIVE] = 'Active';
@@ -368,7 +386,7 @@ const factory = (crowi) => {
     return userStatus;
   };
 
-  userSchema.statics.isEmailValid = function(email, callback) {
+  userSchema.statics.isEmailValid = (email, callback) => {
     validateCrowi();
 
     const whitelist = configManager.getConfig('security:registrationWhitelist');
@@ -383,7 +401,7 @@ const factory = (crowi) => {
     return true;
   };
 
-  userSchema.statics.findUsers = function(options, callback) {
+  userSchema.statics.findUsers = function (options, callback) {
     const sort = options.sort || { status: 1, createdAt: 1 };
 
     this.find()
@@ -395,7 +413,7 @@ const factory = (crowi) => {
       });
   };
 
-  userSchema.statics.findAllUsers = function(option) {
+  userSchema.statics.findAllUsers = function (option) {
     // eslint-disable-next-line no-param-reassign
     option = option || {};
 
@@ -408,12 +426,16 @@ const factory = (crowi) => {
     }
 
     return this.find()
-      .or(status.map((s) => { return { status: s } }))
+      .or(
+        status.map((s) => {
+          return { status: s };
+        }),
+      )
       .select(fields)
       .sort(sort);
   };
 
-  userSchema.statics.findUsersByIds = function(ids, option) {
+  userSchema.statics.findUsersByIds = function (ids, option) {
     // eslint-disable-next-line no-param-reassign
     option = option || {};
 
@@ -426,7 +448,7 @@ const factory = (crowi) => {
       .sort(sort);
   };
 
-  userSchema.statics.findAdmins = async function(option) {
+  userSchema.statics.findAdmins = async function (option) {
     const sort = option?.sort ?? { createdAt: -1 };
 
     let status = option?.status ?? [STATUS_ACTIVE];
@@ -434,25 +456,24 @@ const factory = (crowi) => {
       status = [status];
     }
 
-    return this.find({ admin: true, status: { $in: status } })
-      .sort(sort);
+    return this.find({ admin: true, status: { $in: status } }).sort(sort);
   };
 
-  userSchema.statics.findUserByUsername = function(username) {
+  userSchema.statics.findUserByUsername = function (username) {
     if (username == null) {
       return Promise.resolve(null);
     }
     return this.findOne({ username });
   };
 
-  userSchema.statics.findUserByApiToken = function(apiToken) {
+  userSchema.statics.findUserByApiToken = function (apiToken) {
     if (apiToken == null) {
       return Promise.resolve(null);
     }
     return this.findOne({ apiToken }).lean();
   };
 
-  userSchema.statics.findUserByGoogleId = function(googleId, callback) {
+  userSchema.statics.findUserByGoogleId = function (googleId, callback) {
     if (googleId == null) {
       callback(null, null);
     }
@@ -461,25 +482,30 @@ const factory = (crowi) => {
     });
   };
 
-  userSchema.statics.findUserByUsernameOrEmail = function(usernameOrEmail, password, callback) {
+  userSchema.statics.findUserByUsernameOrEmail = function (
+    usernameOrEmail,
+    password,
+    callback,
+  ) {
     this.findOne()
-      .or([
-        { username: usernameOrEmail },
-        { email: usernameOrEmail },
-      ])
+      .or([{ username: usernameOrEmail }, { email: usernameOrEmail }])
       .exec((err, userData) => {
         callback(err, userData);
       });
   };
 
-  userSchema.statics.findUserByEmailAndPassword = function(email, password, callback) {
+  userSchema.statics.findUserByEmailAndPassword = function (
+    email,
+    password,
+    callback,
+  ) {
     const hashedPassword = generatePassword(password);
     this.findOne({ email, password: hashedPassword }, (err, userData) => {
       callback(err, userData);
     });
   };
 
-  userSchema.statics.isUserCountExceedsUpperLimit = async function() {
+  userSchema.statics.isUserCountExceedsUpperLimit = async function () {
     const userUpperLimit = configManager.getConfig('security:userUpperLimit');
 
     const activeUsers = await this.countActiveUsers();
@@ -490,19 +516,18 @@ const factory = (crowi) => {
     return false;
   };
 
-  userSchema.statics.countActiveUsers = async function() {
+  userSchema.statics.countActiveUsers = async function () {
     return this.countListByStatus(STATUS_ACTIVE);
   };
 
-  userSchema.statics.countListByStatus = async function(status) {
-    const User = this;
+  userSchema.statics.countListByStatus = async function (status) {
     const conditions = { status };
 
     // TODO count は非推奨。mongoose のバージョンアップ後に countDocuments に変更する。
-    return User.count(conditions);
+    return this.count(conditions);
   };
 
-  userSchema.statics.isRegisterableUsername = async function(username) {
+  userSchema.statics.isRegisterableUsername = async function (username) {
     let usernameUsable = true;
 
     const userData = await this.findOne({ username });
@@ -512,7 +537,7 @@ const factory = (crowi) => {
     return usernameUsable;
   };
 
-  userSchema.statics.isRegisterableEmail = async function(email) {
+  userSchema.statics.isRegisterableEmail = async function (email) {
     let isEmailUsable = true;
 
     const userData = await this.findOne({ email });
@@ -522,8 +547,7 @@ const factory = (crowi) => {
     return isEmailUsable;
   };
 
-  userSchema.statics.isRegisterable = function(email, username, callback) {
-    const User = this;
+  userSchema.statics.isRegisterable = function (email, username, callback) {
     let emailUsable = true;
     let usernameUsable = true;
 
@@ -534,13 +558,16 @@ const factory = (crowi) => {
       }
 
       // email check
-      User.findOne({ email }, (err, userData) => {
+      this.findOne({ email }, (err, userData) => {
         if (userData) {
           emailUsable = false;
         }
 
         if (!emailUsable || !usernameUsable) {
-          return callback(false, { email: emailUsable, username: usernameUsable });
+          return callback(false, {
+            email: emailUsable,
+            username: usernameUsable,
+          });
         }
 
         return callback(true, {});
@@ -548,7 +575,7 @@ const factory = (crowi) => {
     });
   };
 
-  userSchema.statics.resetPasswordByRandomString = async function(id) {
+  userSchema.statics.resetPasswordByRandomString = async function (id) {
     const user = await this.findById(id);
 
     if (!user) {
@@ -562,9 +589,8 @@ const factory = (crowi) => {
     return newPassword;
   };
 
-  userSchema.statics.createUserByEmail = async function(email) {
-    const User = this;
-    const newUser = new User();
+  userSchema.statics.createUserByEmail = async function (email) {
+    const newUser = new this();
 
     /* eslint-disable newline-per-chained-call */
     const tmpUsername = `temp_${Math.random().toString(36).slice(-16)}`;
@@ -588,21 +614,25 @@ const factory = (crowi) => {
         password,
         user: newUserData,
       };
-    }
-    catch (err) {
+    } catch (err) {
       return {
         email,
       };
     }
   };
 
-  userSchema.statics.createUsersByEmailList = async function(emailList) {
-    const User = this;
-
+  userSchema.statics.createUsersByEmailList = async function (emailList) {
     // check exists and get list of try to create
-    const existingUserList = await User.find({ email: { $in: emailList }, userStatus: { $ne: STATUS_DELETED } });
-    const existingEmailList = existingUserList.map((user) => { return user.email });
-    const creationEmailList = emailList.filter((email) => { return existingEmailList.indexOf(email) === -1 });
+    const existingUserList = await this.find({
+      email: { $in: emailList },
+      userStatus: { $ne: STATUS_DELETED },
+    });
+    const existingEmailList = existingUserList.map((user) => {
+      return user.email;
+    });
+    const creationEmailList = emailList.filter((email) => {
+      return existingEmailList.indexOf(email) === -1;
+    });
 
     const createdUserList = [];
     const failedToCreateUserEmailList = [];
@@ -612,8 +642,7 @@ const factory = (crowi) => {
         // eslint-disable-next-line no-await-in-loop
         const createdUser = await this.createUserByEmail(email);
         createdUserList.push(createdUser);
-      }
-      catch (err) {
+      } catch (err) {
         logger.error(err);
         failedToCreateUserEmailList.push({
           email,
@@ -625,12 +654,20 @@ const factory = (crowi) => {
     return { createdUserList, existingEmailList, failedToCreateUserEmailList };
   };
 
-  userSchema.statics.createUserByEmailAndPasswordAndStatus = async function(name, username, email, password, lang, status, callback) {
-    const User = this;
-    const newUser = new User();
+  userSchema.statics.createUserByEmailAndPasswordAndStatus = async function (
+    name,
+    username,
+    email,
+    password,
+    lang,
+    status,
+    callback,
+  ) {
+    const newUser = new this();
 
     // check user upper limit
-    const isUserCountExceedsUpperLimit = await User.isUserCountExceedsUpperLimit();
+    const isUserCountExceedsUpperLimit =
+      await this.isUserCountExceedsUpperLimit();
     if (isUserCountExceedsUpperLimit) {
       const err = new UserUpperLimitException();
       return callback(err);
@@ -651,7 +688,9 @@ const factory = (crowi) => {
     }
 
     // Default email show/hide is up to the administrator
-    newUser.isEmailPublished = configManager.getConfig('customize:isEmailPublishedForNewUser');
+    newUser.isEmailPublished = configManager.getConfig(
+      'customize:isEmailPublishedForNewUser',
+    );
 
     const globalLang = configManager.getConfig('app:globalLang');
     if (globalLang != null) {
@@ -680,8 +719,23 @@ const factory = (crowi) => {
    * A wrapper function of createUserByEmailAndPasswordAndStatus with callback
    *
    */
-  userSchema.statics.createUserByEmailAndPassword = function(name, username, email, password, lang, callback) {
-    this.createUserByEmailAndPasswordAndStatus(name, username, email, password, lang, undefined, callback);
+  userSchema.statics.createUserByEmailAndPassword = function (
+    name,
+    username,
+    email,
+    password,
+    lang,
+    callback,
+  ) {
+    this.createUserByEmailAndPasswordAndStatus(
+      name,
+      username,
+      email,
+      password,
+      lang,
+      undefined,
+      callback,
+    );
   };
 
   /**
@@ -689,20 +743,33 @@ const factory = (crowi) => {
    *
    * @return {Promise<User>}
    */
-  userSchema.statics.createUser = function(name, username, email, password, lang, status) {
-    const User = this;
-
+  userSchema.statics.createUser = function (
+    name,
+    username,
+    email,
+    password,
+    lang,
+    status,
+  ) {
     return new Promise((resolve, reject) => {
-      User.createUserByEmailAndPasswordAndStatus(name, username, email, password, lang, status, (err, userData) => {
-        if (err) {
-          return reject(err);
-        }
-        return resolve(userData);
-      });
+      this.createUserByEmailAndPasswordAndStatus(
+        name,
+        username,
+        email,
+        password,
+        lang,
+        status,
+        (err, userData) => {
+          if (err) {
+            return reject(err);
+          }
+          return resolve(userData);
+        },
+      );
     });
   };
 
-  userSchema.statics.isExistUserByUserPagePath = async function(path) {
+  userSchema.statics.isExistUserByUserPagePath = async function (path) {
     const username = pagePathUtils.getUsernameByPath(path);
 
     if (username == null) {
@@ -713,7 +780,7 @@ const factory = (crowi) => {
     return user != null;
   };
 
-  userSchema.statics.updateIsInvitationEmailSended = async function(id) {
+  userSchema.statics.updateIsInvitationEmailSended = async function (id) {
     const user = await this.findById(id);
 
     if (user == null) {
@@ -728,7 +795,7 @@ const factory = (crowi) => {
     user.save();
   };
 
-  userSchema.statics.findUserBySlackMemberId = async function(slackMemberId) {
+  userSchema.statics.findUserBySlackMemberId = async function (slackMemberId) {
     const user = this.findOne({ slackMemberId });
     if (user == null) {
       throw new Error('User not found');
@@ -736,7 +803,9 @@ const factory = (crowi) => {
     return user;
   };
 
-  userSchema.statics.findUsersBySlackMemberIds = async function(slackMemberIds) {
+  userSchema.statics.findUsersBySlackMemberIds = async function (
+    slackMemberIds,
+  ) {
     const users = this.find({ slackMemberId: { $in: slackMemberIds } });
     if (users.length === 0) {
       throw new Error('No user found');
@@ -744,30 +813,36 @@ const factory = (crowi) => {
     return users;
   };
 
-  userSchema.statics.findUserByUsernameRegexWithTotalCount = async function(username, status, option) {
+  userSchema.statics.findUserByUsernameRegexWithTotalCount = async function (
+    username,
+    status,
+    option,
+  ) {
     const opt = option || {};
     const sortOpt = opt.sortOpt || { username: 1 };
     const offset = opt.offset || 0;
     const limit = opt.limit || 10;
 
-    const conditions = { username: { $regex: username, $options: 'i' }, status: { $in: status } };
+    const conditions = {
+      username: { $regex: username, $options: 'i' },
+      status: { $in: status },
+    };
 
     const users = await this.find(conditions)
       .sort(sortOpt)
       .skip(offset)
       .limit(limit);
 
-    const totalCount = (await this.find(conditions).distinct('username')).length;
+    const totalCount = (await this.find(conditions).distinct('username'))
+      .length;
 
     return { users, totalCount };
   };
 
   class UserUpperLimitException {
-
     constructor() {
       this.name = this.constructor.name;
     }
-
   }
 
   userSchema.statics.STATUS_REGISTERED = STATUS_REGISTERED;
@@ -775,7 +850,8 @@ const factory = (crowi) => {
   userSchema.statics.STATUS_SUSPENDED = STATUS_SUSPENDED;
   userSchema.statics.STATUS_DELETED = STATUS_DELETED;
   userSchema.statics.STATUS_INVITED = STATUS_INVITED;
-  userSchema.statics.USER_FIELDS_EXCEPT_CONFIDENTIAL = USER_FIELDS_EXCEPT_CONFIDENTIAL;
+  userSchema.statics.USER_FIELDS_EXCEPT_CONFIDENTIAL =
+    USER_FIELDS_EXCEPT_CONFIDENTIAL;
   userSchema.statics.PAGE_ITEMS = PAGE_ITEMS;
 
   return mongoose.model('User', userSchema);

+ 0 - 2
apps/app/src/server/models/vo/collection-progress.ts

@@ -1,5 +1,4 @@
 class CollectionProgress {
-
   collectionName: string;
 
   currentCount = 0;
@@ -13,7 +12,6 @@ class CollectionProgress {
   constructor(collectionName: string) {
     this.collectionName = collectionName;
   }
-
 }
 
 export default CollectionProgress;

+ 1 - 6
apps/app/src/server/models/vo/collection-progressing-status.ts

@@ -1,7 +1,6 @@
 import CollectionProgress from './collection-progress';
 
 class CollectionProgressingStatus {
-
   totalCount = 0;
 
   progressList: CollectionProgress[];
@@ -29,12 +28,8 @@ class CollectionProgressingStatus {
   }
 
   get currentCount(): number {
-    return this.progressList.reduce(
-      (acc, crr) => acc + crr.currentCount,
-      0,
-    );
+    return this.progressList.reduce((acc, crr) => acc + crr.currentCount, 0);
   }
-
 }
 
 export default CollectionProgressingStatus;

+ 2 - 3
apps/app/src/server/models/vo/g2g-transfer-error.ts

@@ -6,10 +6,10 @@ export const G2GTransferErrorCode = {
   FAILED_TO_RETRIEVE_FILE_METADATA: 'FAILED_TO_RETRIEVE_FILE_METADATA',
 } as const;
 
-export type G2GTransferErrorCode = typeof G2GTransferErrorCode[keyof typeof G2GTransferErrorCode];
+export type G2GTransferErrorCode =
+  (typeof G2GTransferErrorCode)[keyof typeof G2GTransferErrorCode];
 
 export class G2GTransferError extends ExtensibleCustomError {
-
   readonly id = 'G2GTransferError';
 
   code!: G2GTransferErrorCode;
@@ -18,7 +18,6 @@ export class G2GTransferError extends ExtensibleCustomError {
     super(message);
     this.code = code;
   }
-
 }
 
 export const isG2GTransferError = (err: any): err is G2GTransferError => {

+ 1 - 6
apps/app/src/server/models/vo/s2c-message.js

@@ -4,14 +4,10 @@ const { serializePageSecurely } = require('../serializers/page-serializer');
  * Server-to-client message VO
  */
 class S2cMessagePageUpdated {
-
-
   constructor(page, user) {
     const serializedPage = serializePageSecurely(page);
 
-    const {
-      _id, revision, updatedAt,
-    } = serializedPage;
+    const { _id, revision, updatedAt } = serializedPage;
 
     this.pageId = _id;
     this.revisionId = revision;
@@ -25,7 +21,6 @@ class S2cMessagePageUpdated {
       this.lastUpdateUsername = user.name;
     }
   }
-
 }
 
 module.exports = {

+ 1 - 3
apps/app/src/server/models/vo/s2s-message.js

@@ -2,7 +2,6 @@
  * Server-to-server message VO
  */
 class S2sMessage {
-
   constructor(eventName, body = {}) {
     this.eventName = eventName;
     for (const [key, value] of Object.entries(body)) {
@@ -18,12 +17,11 @@ class S2sMessage {
     const body = JSON.parse(messageString);
 
     if (body.eventName == null) {
-      throw new Error('message body must contain \'eventName\'');
+      throw new Error("message body must contain 'eventName'");
     }
 
     return new S2sMessage(body.eventName, body);
   }
-
 }
 
 module.exports = S2sMessage;

+ 0 - 2
apps/app/src/server/models/vo/search-error.ts

@@ -3,7 +3,6 @@ import ExtensibleCustomError from 'extensible-custom-error';
 import type { AllTermsKey } from '~/server/interfaces/search';
 
 export class SearchError extends ExtensibleCustomError {
-
   readonly id = 'SearchError';
 
   unavailableTermsKeys!: AllTermsKey[];
@@ -12,7 +11,6 @@ export class SearchError extends ExtensibleCustomError {
     super(message);
     this.unavailableTermsKeys = unavailableTermsKeys;
   }
-
 }
 
 export const isSearchError = (err: any): err is SearchError => {

+ 11 - 14
apps/app/src/server/models/vo/slack-command-handler-error.ts

@@ -2,36 +2,33 @@ import type { RespondBodyForResponseUrl } from '@growi/slack';
 import { markdownSectionBlock } from '@growi/slack/dist/utils/block-kit-builder';
 import ExtensibleCustomError from 'extensible-custom-error';
 
-export const generateDefaultRespondBodyForInternalServerError = (message: string): RespondBodyForResponseUrl => {
+export const generateDefaultRespondBodyForInternalServerError = (
+  message: string,
+): RespondBodyForResponseUrl => {
   return {
     text: message,
-    blocks: [
-      markdownSectionBlock(`*An error occured*\n ${message}`),
-    ],
+    blocks: [markdownSectionBlock(`*An error occured*\n ${message}`)],
   };
 };
 
 type Opts = {
-  responseUrl?: string,
-  respondBody?: RespondBodyForResponseUrl,
-}
+  responseUrl?: string;
+  respondBody?: RespondBodyForResponseUrl;
+};
 
 /**
  * Error class for slackbot service
  */
 export class SlackCommandHandlerError extends ExtensibleCustomError {
-
   responseUrl?: string;
 
   respondBody: RespondBodyForResponseUrl;
 
-  constructor(
-      message: string,
-      opts: Opts = {},
-  ) {
+  constructor(message: string, opts: Opts = {}) {
     super(message);
     this.responseUrl = opts.responseUrl;
-    this.respondBody = opts.respondBody || generateDefaultRespondBodyForInternalServerError(message);
+    this.respondBody =
+      opts.respondBody ||
+      generateDefaultRespondBodyForInternalServerError(message);
   }
-
 }

+ 0 - 2
apps/app/src/server/models/vo/v5-conversion-error.ts

@@ -3,7 +3,6 @@ import ExtensibleCustomError from 'extensible-custom-error';
 import type { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
 
 export class V5ConversionError extends ExtensibleCustomError {
-
   readonly id = 'V5ConversionError';
 
   code!: V5ConversionErrCode;
@@ -12,7 +11,6 @@ export class V5ConversionError extends ExtensibleCustomError {
     super(message);
     this.code = code;
   }
-
 }
 
 export const isV5ConversionError = (err: any): err is V5ConversionError => {

+ 0 - 1
biome.json

@@ -31,7 +31,6 @@
       "!apps/app/src/client",
       "!apps/app/src/features/openai",
       "!apps/app/src/server/middlewares",
-      "!apps/app/src/server/models",
       "!apps/app/src/server/routes",
       "!apps/app/src/server/service",
       "!apps/app/src/services",

Некоторые файлы не были показаны из-за большого количества измененных файлов