import { getOrCreateModel } from '@growi/core'; import { addSeconds } from 'date-fns'; import mongoose, { Schema, Model, Document, QueryOptions, FilterQuery, } from 'mongoose'; import { IPageForResuming, IUserForResuming, IOptionsForResuming, } from '~/server/models/interfaces/page-operation'; import loggerFactory from '../../utils/logger'; import { ObjectIdLike } from '../interfaces/mongoose-utils'; const TIME_TO_ADD_SEC = 10; const logger = loggerFactory('growi:models:page-operation'); type IObjectId = mongoose.Types.ObjectId; const ObjectId = mongoose.Schema.Types.ObjectId; export const PageActionType = { Rename: 'Rename', Duplicate: 'Duplicate', Delete: 'Delete', DeleteCompletely: 'DeleteCompletely', Revert: 'Revert', NormalizeParent: 'NormalizeParent', } as const; export type PageActionType = typeof PageActionType[keyof typeof PageActionType]; export const PageActionStage = { Main: 'Main', Sub: 'Sub', } as const; export type PageActionStage = typeof PageActionStage[keyof typeof PageActionStage]; /* * Main Schema */ export interface IPageOperation { actionType: PageActionType, actionStage: PageActionStage, fromPath: string, toPath?: string, page: IPageForResuming, user: IUserForResuming, options?: IOptionsForResuming, incForUpdatingDescendantCount?: number, unprocessableExpiryDate: Date, isProcessable(): boolean } export interface PageOperationDocument extends IPageOperation, Document {} export type PageOperationDocumentHasId = PageOperationDocument & { _id: ObjectIdLike }; export interface PageOperationModel extends Model { findByIdAndUpdatePageActionStage(pageOpId: ObjectIdLike, stage: PageActionStage): Promise findMainOps(filter?: FilterQuery, projection?: any, options?: QueryOptions): Promise deleteByActionTypes(deleteTypeList: PageActionType[]): Promise extendExpiryDate(operationId: ObjectIdLike): Promise } const pageSchemaForResuming = new Schema({ _id: { type: ObjectId, ref: 'Page', index: true }, parent: { type: ObjectId, ref: 'Page' }, descendantCount: { type: Number }, isEmpty: { type: Boolean }, path: { type: String, required: true, index: true }, revision: { type: ObjectId, ref: 'Revision' }, status: { type: String }, grant: { type: Number }, grantedUsers: [{ type: ObjectId, ref: 'User' }], grantedGroup: { type: ObjectId, ref: 'UserGroup' }, creator: { type: ObjectId, ref: 'User' }, lastUpdateUser: { type: ObjectId, ref: 'User' }, }); const userSchemaForResuming = new Schema({ _id: { type: ObjectId, ref: 'User', required: true }, }); const optionsSchemaForResuming = new Schema({ createRedirectPage: { type: Boolean }, updateMetadata: { type: Boolean }, prevDescendantCount: { type: Number }, }, { _id: false }); const schema = new Schema({ actionType: { type: String, enum: PageActionType, required: true, index: true, }, actionStage: { type: String, enum: PageActionStage, required: true, index: true, }, fromPath: { type: String, required: true, index: true }, toPath: { type: String, index: true }, page: { type: pageSchemaForResuming, required: true }, user: { type: userSchemaForResuming, required: true }, options: { type: optionsSchemaForResuming }, incForUpdatingDescendantCount: { type: Number }, unprocessableExpiryDate: { type: Date, default: () => addSeconds(new Date(), 10) }, }); schema.statics.findByIdAndUpdatePageActionStage = async function( pageOpId: ObjectIdLike, stage: PageActionStage, ): Promise { return this.findByIdAndUpdate(pageOpId, { $set: { actionStage: stage }, }, { new: true }); }; schema.statics.findMainOps = async function( filter?: FilterQuery, projection?: any, options?: QueryOptions, ): Promise { return this.find( { ...filter, actionStage: PageActionStage.Main }, projection, options, ); }; schema.statics.deleteByActionTypes = async function( actionTypes: PageActionType[], ): Promise { await this.deleteMany({ actionType: { $in: 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 { const date = addSeconds(new Date(), TIME_TO_ADD_SEC); await this.findByIdAndUpdate(operationId, { unprocessableExpiryDate: date }); }; schema.methods.isProcessable = function(): boolean { const { unprocessableExpiryDate } = this; return unprocessableExpiryDate == null || (unprocessableExpiryDate != null && new Date() > unprocessableExpiryDate); }; export default getOrCreateModel('PageOperation', schema);