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

Merge pull request #8954 from weseek/support/typescriptize-revision-model

support: Typescriptize Revision model
Yuki Takei 1 год назад
Родитель
Сommit
6fdd2e6bec

+ 4 - 4
apps/app/src/client/components/PageHistory/PageRevisionTable.tsx

@@ -2,7 +2,7 @@ import React, {
   useEffect, useRef, useState,
 } from 'react';
 
-import type { IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
 import { useTranslation } from 'next-i18next';
 
 import { useSWRxInfinitePageRevisions } from '~/stores/page';
@@ -48,8 +48,8 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
     || (isValidating && data != null && typeof data[size - 1] === 'undefined');
   const isReachingEnd = (revisionPerPage === 0) || !!(data != null && data[data.length - 1]?.revisions.length < REVISIONS_PER_PAGE);
 
-  const [sourceRevision, setSourceRevision] = useState<IRevisionHasPageId>();
-  const [targetRevision, setTargetRevision] = useState<IRevisionHasPageId>();
+  const [sourceRevision, setSourceRevision] = useState<IRevisionHasId>();
+  const [targetRevision, setTargetRevision] = useState<IRevisionHasId>();
 
   const tbodyRef = useRef<HTMLTableSectionElement>(null);
 
@@ -96,7 +96,7 @@ export const PageRevisionTable = (props: PageRevisionTableProps): JSX.Element =>
   }, [isLoadingMore, isReachingEnd, setSize, size]);
 
 
-  const renderRow = (revision: IRevisionHasPageId, previousRevision: IRevisionHasPageId, latestRevision: IRevisionHasPageId,
+  const renderRow = (revision: IRevisionHasId, previousRevision: IRevisionHasId, latestRevision: IRevisionHasId,
       isOldestRevision: boolean, hasDiff: boolean) => {
 
     const revisionId = revision._id;

+ 3 - 3
apps/app/src/client/components/PageHistory/RevisionDiff.tsx

@@ -1,6 +1,6 @@
 import { useMemo } from 'react';
 
-import type { IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
 import { GrowiThemeSchemeType } from '@growi/core';
 import { returnPathForURL } from '@growi/core/dist/utils/path-utils';
 import { PresetThemesMetadatas } from '@growi/preset-themes';
@@ -26,8 +26,8 @@ import 'diff2html/bundles/css/diff2html.min.css';
 const moduleClass = styles['revision-diff-container'];
 
 type RevisioinDiffProps = {
-  currentRevision: IRevisionHasPageId,
-  previousRevision: IRevisionHasPageId,
+  currentRevision: IRevisionHasId,
+  previousRevision: IRevisionHasId,
   revisionDiffOpened: boolean,
   currentPageId: string,
   currentPagePath: string,

+ 3 - 3
apps/app/src/client/components/RevisionComparer/RevisionComparer.tsx

@@ -1,6 +1,6 @@
 import React, { useState } from 'react';
 
-import type { IRevisionHasPageId } from '@growi/core';
+import type { IRevisionHasId } from '@growi/core';
 import { pagePathUtils } from '@growi/core/dist/utils';
 import { useTranslation } from 'next-i18next';
 import { CopyToClipboard } from 'react-copy-to-clipboard';
@@ -22,8 +22,8 @@ const DropdownItemContents = ({ title, contents }) => (
 );
 
 type RevisionComparerProps = {
-  sourceRevision: IRevisionHasPageId
-  targetRevision: IRevisionHasPageId
+  sourceRevision: IRevisionHasId
+  targetRevision: IRevisionHasId
   currentPageId?: string
   currentPagePath: string
   onClose: () => void

+ 1 - 2
apps/app/src/migrations/20211227060705-revision-path-to-page-id-schema-migration--fixed-7549.js

@@ -4,6 +4,7 @@ import mongoose from 'mongoose';
 import streamToPromise from 'stream-to-promise';
 
 import getPageModel from '~/server/models/page';
+import { Revision } from '~/server/models/revision';
 import { createBatchStream } from '~/server/util/batch-stream';
 import { getModelSafely, getMongoUri, mongoOptions } from '~/server/util/mongoose-utils';
 import loggerFactory from '~/utils/logger';
@@ -18,7 +19,6 @@ module.exports = {
   async up(db, client) {
     mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
-    const Revision = getModelSafely('Revision') || require('~/server/models/revision')();
 
     const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
     const batchStrem = createBatchStream(LIMIT);
@@ -69,7 +69,6 @@ module.exports = {
   async down(db, client) {
     mongoose.connect(getMongoUri(), mongoOptions);
     const Page = getModelSafely('Page') || getPageModel();
-    const Revision = getModelSafely('Revision') || require('~/server/models/revision')();
 
     const pagesStream = await Page.find({ revision: { $ne: null } }, { _id: 1, path: 1 }).cursor({ batch_size: LIMIT });
     const batchStrem = createBatchStream(LIMIT);

+ 1 - 1
apps/app/src/server/models/index.ts

@@ -5,7 +5,6 @@ export const modelsDependsOnCrowi = {
   Page,
   PageTagRelation: require('./page-tag-relation'),
   User: require('./user'),
-  Revision: require('./revision'),
   Bookmark: require('./bookmark'),
   GlobalNotificationSetting: GlobalNotificationSettingFactory,
   GlobalNotificationMailSetting: require('./GlobalNotificationSetting/GlobalNotificationMailSetting'),
@@ -17,6 +16,7 @@ export const modelsDependsOnCrowi = {
 export * from './attachment';
 export * as Activity from './activity';
 export * as PageRedirect from './page-redirect';
+export * from './revision';
 export * as ShareLink from './share-link';
 export * as Tag from './tag';
 export * as UserGroup from './user-group';

+ 0 - 71
apps/app/src/server/models/revision.js

@@ -1,71 +0,0 @@
-import { allOrigin } from '@growi/core';
-
-import loggerFactory from '~/utils/logger';
-
-// disable no-return-await for model functions
-/* eslint-disable no-return-await */
-
-module.exports = function(crowi) {
-  // eslint-disable-next-line no-unused-vars
-  const logger = loggerFactory('growi:models:revision');
-
-  const mongoose = require('mongoose');
-  const mongoosePaginate = require('mongoose-paginate-v2');
-
-  // allow empty strings
-  mongoose.Schema.Types.String.checkRequired(v => v != null);
-
-  const ObjectId = mongoose.Schema.Types.ObjectId;
-  const revisionSchema = new mongoose.Schema({
-    // OBSOLETE path: { type: String, required: true, index: true }
-    pageId: { type: ObjectId, required: true, index: true },
-    body: {
-      type: String,
-      required: true,
-      get: (data) => {
-      // replace CR/CRLF to LF above v3.1.5
-      // see https://github.com/weseek/growi/issues/463
-        return data ? data.replace(/\r\n?/g, '\n') : '';
-      },
-    },
-    format: { type: String, default: 'markdown' },
-    author: { type: ObjectId, ref: 'User' },
-    hasDiffToPrev: { type: Boolean },
-    origin: { type: String, enum: allOrigin },
-  }, {
-    timestamps: { createdAt: true, updatedAt: false },
-  });
-  revisionSchema.plugin(mongoosePaginate);
-
-  revisionSchema.statics.updateRevisionListByPageId = async function(pageId, updateData) {
-    return this.updateMany({ pageId }, { $set: updateData });
-  };
-
-  revisionSchema.statics.prepareRevision = function(pageData, body, previousBody, user, origin, options) {
-    const Revision = this;
-
-    if (!options) {
-      // eslint-disable-next-line no-param-reassign
-      options = {};
-    }
-    const format = options.format || 'markdown';
-
-    if (!user._id) {
-      throw new Error('Error: user should have _id');
-    }
-
-    const newRevision = new Revision();
-    newRevision.pageId = pageData._id;
-    newRevision.body = body;
-    newRevision.format = format;
-    newRevision.author = user._id;
-    newRevision.origin = origin;
-    if (pageData.revision != null) {
-      newRevision.hasDiffToPrev = body !== previousBody;
-    }
-
-    return newRevision;
-  };
-
-  return mongoose.model('Revision', revisionSchema);
-};

+ 82 - 0
apps/app/src/server/models/revision.ts

@@ -0,0 +1,82 @@
+import type {
+  HasObjectId,
+  IRevision,
+  Origin,
+} from '@growi/core';
+import { allOrigin } from '@growi/core';
+import {
+  Schema, Types, type Document, type Model,
+} 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 {
+}
+
+type UpdateRevisionListByPageId = (pageId: string, updateData: Partial<IRevision>) => Promise<void>;
+type PrepareRevision = (
+  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,
+}
+
+// Use this to allow empty strings to pass the `required` validator
+Schema.Types.String.checkRequired(v => typeof v === 'string');
+
+const revisionSchema = new Schema<IRevisionDocument, IRevisionModel>({
+  pageId: {
+    type: 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/weseek/growi/issues/463
+      return data ? data.replace(/\r\n?/g, '\n') : '';
+    },
+  },
+  format: { type: String, default: 'markdown' },
+  author: { type: 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) {
+  await this.updateMany({ pageId }, { $set: updateData });
+};
+revisionSchema.statics.updateRevisionListByPageId = updateRevisionListByPageId;
+
+const prepareRevision: PrepareRevision = function(this: IRevisionModel, pageData, body, previousBody, user, origin, options = { format: 'markdown' }) {
+  if (!user._id) {
+    throw new Error('Error: user should have _id');
+  }
+
+  const newRevision = new this();
+  newRevision.pageId = pageData._id;
+  newRevision.body = body;
+  newRevision.format = options.format;
+  newRevision.author = user._id;
+  newRevision.origin = origin;
+  if (pageData.revision != null) {
+    newRevision.hasDiffToPrev = body !== previousBody;
+  }
+
+  return newRevision;
+};
+revisionSchema.statics.prepareRevision = prepareRevision;
+
+export const Revision = getOrCreateModel<IRevisionDocument, IRevisionModel>('Revision', revisionSchema);

+ 2 - 2
apps/app/src/server/routes/apiv3/page/index.ts

@@ -15,8 +15,9 @@ import type { IPageGrantData } from '~/interfaces/page';
 import { generateAddActivityMiddleware } from '~/server/middlewares/add-activity';
 import { apiV3FormValidator } from '~/server/middlewares/apiv3-form-validator';
 import { excludeReadOnlyUser } from '~/server/middlewares/exclude-read-only-user';
-import { GlobalNotificationSettingEvent } from '~/server/models';
+import { GlobalNotificationSettingEvent } from '~/server/models/GlobalNotificationSetting';
 import type { PageModel } from '~/server/models/page';
+import { Revision } from '~/server/models/revision';
 import Subscription from '~/server/models/subscription';
 import { configManager } from '~/server/service/config-manager';
 import type { IPageGrantService } from '~/server/service/page-grant';
@@ -757,7 +758,6 @@ module.exports = (crowi) => {
 
       const revisionIdForFind = revisionId || page.revision;
 
-      const Revision = crowi.model('Revision');
       revision = await Revision.findById(revisionIdForFind);
       pagePath = page.path;
 

+ 1 - 1
apps/app/src/server/routes/apiv3/revisions.js

@@ -1,5 +1,6 @@
 import { ErrorV3 } from '@growi/core/dist/models';
 
+import { Revision } from '~/server/models/revision';
 import loggerFactory from '~/utils/logger';
 
 import { apiV3FormValidator } from '../../middlewares/apiv3-form-validator';
@@ -64,7 +65,6 @@ module.exports = (crowi) => {
   const loginRequired = require('../../middlewares/login-required')(crowi, true);
 
   const {
-    Revision,
     Page,
     User,
   } = crowi.models;

+ 1 - 1
apps/app/src/server/routes/tag.js

@@ -2,6 +2,7 @@ import { SupportedAction } from '~/interfaces/activity';
 import Tag from '~/server/models/tag';
 
 import PageTagRelation from '../models/page-tag-relation';
+import { Revision } from '../models/revision';
 import ApiResponse from '../util/apiResponse';
 
 /**
@@ -139,7 +140,6 @@ module.exports = function(crowi, app) {
   api.update = async function(req, res) {
     const Page = crowi.model('Page');
     const User = crowi.model('User');
-    const Revision = crowi.model('Revision');
     const tagEvent = crowi.event('tag');
     const pageId = req.body.pageId;
     const tags = req.body.tags;

+ 5 - 15
apps/app/src/server/service/page/index.ts

@@ -49,11 +49,12 @@ import loggerFactory from '~/utils/logger';
 import { prepareDeleteConfigValuesForCalc } from '~/utils/page-delete-config';
 
 import type { ObjectIdLike } from '../../interfaces/mongoose-utils';
-import { Attachment } from '../../models';
+import { Attachment } from '../../models/attachment';
 import { PathAlreadyExistsError } from '../../models/errors';
 import type { PageOperationDocument } from '../../models/page-operation';
 import PageOperation from '../../models/page-operation';
 import PageRedirect from '../../models/page-redirect';
+import { Revision } from '../../models/revision';
 import { serializePageSecurely } from '../../models/serializers/page-serializer';
 import ShareLink from '../../models/share-link';
 import Subscription from '../../models/subscription';
@@ -832,7 +833,6 @@ class PageService implements IPageService {
 
   private async renamePageV4(page, newPagePath, user, options) {
     const Page = this.crowi.model('Page');
-    const Revision = this.crowi.model('Revision');
     const {
       isRecursively = false,
       createRedirectPage = false,
@@ -1348,7 +1348,6 @@ class PageService implements IPageService {
     }
 
     const Page = this.crowi.model('Page');
-    const Revision = this.crowi.model('Revision');
 
     const pageIds = pages.map(page => page._id);
     const revisions = await Revision.find({ pageId: { $in: pageIds } });
@@ -1356,7 +1355,7 @@ class PageService implements IPageService {
     // Mapping to set to the body of the new revision
     const pageIdRevisionMapping = {};
     revisions.forEach((revision) => {
-      pageIdRevisionMapping[revision.pageId] = revision;
+      pageIdRevisionMapping[getIdForRef(revision.pageId)] = revision;
     });
 
     // key: oldPageId, value: newPageId
@@ -1404,7 +1403,6 @@ class PageService implements IPageService {
 
   private async duplicateDescendantsV4(pages, user, oldPagePathPrefix, newPagePathPrefix) {
     const Page = this.crowi.model('Page');
-    const Revision = this.crowi.model('Revision');
 
     const pageIds = pages.map(page => page._id);
     const revisions = await Revision.find({ pageId: { $in: pageIds } });
@@ -1412,7 +1410,7 @@ class PageService implements IPageService {
     // Mapping to set to the body of the new revision
     const pageIdRevisionMapping = {};
     revisions.forEach((revision) => {
-      pageIdRevisionMapping[revision.pageId] = revision;
+      pageIdRevisionMapping[getIdForRef(revision.pageId)] = revision;
     });
 
     // key: oldPageId, value: newPageId
@@ -1709,7 +1707,6 @@ class PageService implements IPageService {
 
   private async deletePageV4(page, user, options = {}, isRecursively = false) {
     const Page = mongoose.model('Page') as PageModel;
-    const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
 
     const newPath = Page.getDeletedPageName(page.path);
     const isTrashed = isTrashPage(page.path);
@@ -1872,7 +1869,6 @@ class PageService implements IPageService {
   async deleteCompletelyOperation(pageIds, pagePaths): Promise<void> {
     // Delete Attachments, Revisions, Pages and emit delete
     const Page = this.crowi.model('Page');
-    const Revision = this.crowi.model('Revision');
 
     const { attachmentService } = this.crowi;
     const attachments = await Attachment.find({ page: { $in: pageIds } });
@@ -3839,7 +3835,6 @@ class PageService implements IPageService {
     let savedPage = await page.save();
 
     // Create revision
-    const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
     const newRevision = Revision.prepareRevision(savedPage, body, null, user, options.origin);
     savedPage = await pushRevision(savedPage, newRevision, user);
     await savedPage.populateDataToShowRevision();
@@ -3901,7 +3896,6 @@ class PageService implements IPageService {
    */
   private async createV4(path, body, user, options: any = {}) {
     const Page = mongoose.model('Page') as unknown as PageModel;
-    const Revision = mongoose.model('Revision') as any; // TODO: TypeScriptize model
 
     const format = options.format || 'markdown';
     const grantUserGroupIds = options.grantUserGroupIds || null;
@@ -4037,8 +4031,7 @@ class PageService implements IPageService {
     let savedPage = await page.save();
 
     // Create revision
-    const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
-    const dummyUser = { _id: new mongoose.Types.ObjectId() };
+    const dummyUser: HasObjectId = { _id: new mongoose.Types.ObjectId().toString() };
     const newRevision = Revision.prepareRevision(savedPage, body, null, dummyUser);
     savedPage = await pushRevision(savedPage, newRevision, dummyUser);
 
@@ -4146,7 +4139,6 @@ class PageService implements IPageService {
       options: IOptionsForUpdate = {},
   ): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
-    const Revision = mongoose.model('Revision') as any; // TODO: Typescriptize model
 
     const wasOnTree = pageData.parent != null || isTopPage(pageData.path);
     const isV5Compatible = this.crowi.configManager.getConfig('crowi', 'app:isV5Compatible');
@@ -4290,7 +4282,6 @@ class PageService implements IPageService {
 
   async updatePageV4(pageData: PageDocument, body, previousBody, user, options: IOptionsForUpdate = {}): Promise<PageDocument> {
     const Page = mongoose.model('Page') as unknown as PageModel;
-    const Revision = mongoose.model('Revision') as any; // TODO: TypeScriptize model
 
     // use the previous data if absent
     const grant = options.grant || pageData.grant;
@@ -4463,7 +4454,6 @@ class PageService implements IPageService {
       return false;
     }
 
-    const Revision = mongoose.model<IRevisionHasId>('Revision');
     const revision = await Revision.findOne({ pageId }).sort({ createdAt: -1 });
 
     if (revision == null) {

+ 7 - 10
packages/core/src/interfaces/revision.ts

@@ -1,5 +1,6 @@
 import type { Ref } from './common';
 import type { HasObjectId } from './has-object-id';
+import type { IPage } from './page';
 import type { IUser } from './user';
 
 export const Origin = {
@@ -12,24 +13,20 @@ export type Origin = typeof Origin[keyof typeof Origin];
 export const allOrigin = Object.values(Origin);
 
 export type IRevision = {
+  pageId: Ref<IPage>,
   body: string,
   author: Ref<IUser>,
-  hasDiffToPrev: boolean;
+  format: string,
+  hasDiffToPrev?: boolean;
+  origin?: Origin,
   createdAt: Date,
   updatedAt: Date,
-  origin?: Origin,
 }
 
 export type IRevisionHasId = IRevision & HasObjectId;
 
-type HasPageId = {
-  pageId: string,
-};
-
-export type IRevisionHasPageId = IRevisionHasId & HasPageId;
-
 export type IRevisionsForPagination = {
-  revisions: IRevisionHasPageId[], // revisions in one pagination
+  revisions: IRevisionHasId[], // revisions in one pagination
   totalCounts: number // total counts
 }
 export type HasRevisionShortbody = {
@@ -37,7 +34,7 @@ export type HasRevisionShortbody = {
 }
 
 export type SWRInfinitePageRevisionsResponse = {
-  revisions: IRevisionHasPageId[],
+  revisions: IRevisionHasId[],
   totalCount: number,
   offset: number,
 }