|
|
@@ -8,6 +8,7 @@ import streamToPromise from 'stream-to-promise';
|
|
|
|
|
|
import { SUPPORTED_TARGET_MODEL_TYPE, SUPPORTED_ACTION_TYPE } from '~/interfaces/activity';
|
|
|
import { Ref } from '~/interfaces/common';
|
|
|
+import { V5ConversionErrCode } from '~/interfaces/errors/v5-conversion-error';
|
|
|
import { HasObjectId } from '~/interfaces/has-object-id';
|
|
|
import {
|
|
|
IPage, IPageInfo, IPageInfoForEntity, IPageWithMeta,
|
|
|
@@ -16,7 +17,7 @@ import {
|
|
|
PageDeleteConfigValue, IPageDeleteConfigValueToProcessValidation,
|
|
|
} from '~/interfaces/page-delete-config';
|
|
|
import { IUserHasId } from '~/interfaces/user';
|
|
|
-import { SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
|
|
|
+import { PageMigrationErrorData, SocketEventName, UpdateDescCountRawData } from '~/interfaces/websocket';
|
|
|
import { stringifySnapshot } from '~/models/serializers/in-app-notification-snapshot/page';
|
|
|
import {
|
|
|
CreateMethod, PageCreateOptions, PageModel, PageDocument,
|
|
|
@@ -31,6 +32,7 @@ import PageOperation, { PageActionStage, PageActionType } from '../models/page-o
|
|
|
import { PageRedirectModel } from '../models/page-redirect';
|
|
|
import { serializePageSecurely } from '../models/serializers/page-serializer';
|
|
|
import Subscription from '../models/subscription';
|
|
|
+import { V5ConversionError } from '../models/vo/v5-conversion-error';
|
|
|
|
|
|
const debug = require('debug')('growi:services:page');
|
|
|
|
|
|
@@ -2251,24 +2253,99 @@ class PageService {
|
|
|
await inAppNotificationService.emitSocketIo(targetUsers);
|
|
|
}
|
|
|
|
|
|
- async normalizeParentByPageIds(pageIds: ObjectIdLike[], user, isRecursively: boolean): Promise<void> {
|
|
|
+ async normalizeParentByPath(path: string, user): Promise<void> {
|
|
|
const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
|
|
|
- if (isRecursively) {
|
|
|
- const pages = await Page.findByIdsAndViewer(pageIds, user, null);
|
|
|
+ const pages = await Page.findByPathAndViewer(path, user, null, false);
|
|
|
+ if (pages == null || !Array.isArray(pages)) {
|
|
|
+ throw Error('Something went wrong while converting pages.');
|
|
|
+ }
|
|
|
+ if (pages.length === 0) {
|
|
|
+ throw new V5ConversionError(`Could not find the page "${path}" to convert.`, V5ConversionErrCode.PAGE_NOT_FOUND);
|
|
|
+ }
|
|
|
+ if (pages.length > 1) {
|
|
|
+ throw new V5ConversionError(
|
|
|
+ `There are more than two pages at the path "${path}". Please rename or delete the page first.`,
|
|
|
+ V5ConversionErrCode.DUPLICATE_PAGES_FOUND,
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
- // DO NOT await !!
|
|
|
- this.normalizeParentRecursivelyByPages(pages, user);
|
|
|
+ const page = pages[0];
|
|
|
+ const {
|
|
|
+ grant, grantedUsers: grantedUserIds, grantedGroup: grantedGroupId,
|
|
|
+ } = page;
|
|
|
|
|
|
- return;
|
|
|
+ /*
|
|
|
+ * UserGroup & Owner validation
|
|
|
+ */
|
|
|
+ let isGrantNormalized = false;
|
|
|
+ try {
|
|
|
+ const shouldCheckDescendants = true;
|
|
|
+
|
|
|
+ isGrantNormalized = await this.crowi.pageGrantService.isGrantNormalized(user, path, grant, grantedUserIds, grantedGroupId, shouldCheckDescendants);
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ logger.error(`Failed to validate grant of page at "${path}"`, err);
|
|
|
+ throw err;
|
|
|
+ }
|
|
|
+ if (!isGrantNormalized) {
|
|
|
+ throw new V5ConversionError(
|
|
|
+ 'This page cannot be migrated since the selected grant or grantedGroup is not assignable to this page.',
|
|
|
+ V5ConversionErrCode.GRANT_INVALID,
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ let pageOp;
|
|
|
+ try {
|
|
|
+ pageOp = await PageOperation.create({
|
|
|
+ actionType: PageActionType.NormalizeParent,
|
|
|
+ actionStage: PageActionStage.Main,
|
|
|
+ page,
|
|
|
+ user,
|
|
|
+ fromPath: page.path,
|
|
|
+ toPath: page.path,
|
|
|
+ });
|
|
|
+ }
|
|
|
+ catch (err) {
|
|
|
+ logger.error('Failed to create PageOperation document.', err);
|
|
|
+ throw err;
|
|
|
}
|
|
|
|
|
|
+ // no await
|
|
|
+ this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
|
|
|
+ }
|
|
|
+
|
|
|
+ async normalizeParentByPageIdsRecursively(pageIds: ObjectIdLike[], user): Promise<void> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ const pages = await Page.findByIdsAndViewer(pageIds, user, null);
|
|
|
+
|
|
|
+ if (pages == null || pages.length === 0) {
|
|
|
+ throw Error('pageIds is null or 0 length.');
|
|
|
+ }
|
|
|
+
|
|
|
+ if (pages.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
|
|
|
+ throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
|
|
|
+ }
|
|
|
+
|
|
|
+ this.normalizeParentRecursivelyByPages(pages, user);
|
|
|
+
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ async normalizeParentByPageIds(pageIds: ObjectIdLike[], user): Promise<void> {
|
|
|
+ const Page = await mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ const socket = this.crowi.socketIoService.getDefaultSocket();
|
|
|
+
|
|
|
for await (const pageId of pageIds) {
|
|
|
const page = await Page.findById(pageId);
|
|
|
if (page == null) {
|
|
|
continue;
|
|
|
}
|
|
|
|
|
|
+ const errorData: PageMigrationErrorData = { paths: [page.path] };
|
|
|
+
|
|
|
try {
|
|
|
const canOperate = await this.crowi.pageOperationService.canOperate(false, page.path, page.path);
|
|
|
if (!canOperate) {
|
|
|
@@ -2278,14 +2355,16 @@ class PageService {
|
|
|
const normalizedPage = await this.normalizeParentByPage(page, user);
|
|
|
|
|
|
if (normalizedPage == null) {
|
|
|
+ socket.emit(SocketEventName.PageMigrationError, errorData);
|
|
|
logger.error(`Failed to update descendantCount of page of id: "${pageId}"`);
|
|
|
}
|
|
|
}
|
|
|
catch (err) {
|
|
|
+ socket.emit(SocketEventName.PageMigrationError, errorData);
|
|
|
logger.error('Something went wrong while normalizing parent.', err);
|
|
|
- // socket.emit('normalizeParentByPageIds', { error: err.message }); TODO: use socket to tell user
|
|
|
}
|
|
|
}
|
|
|
+ socket.emit(SocketEventName.PageMigrationSuccess);
|
|
|
}
|
|
|
|
|
|
private async normalizeParentByPage(page, user) {
|
|
|
@@ -2348,14 +2427,7 @@ class PageService {
|
|
|
/*
|
|
|
* Main Operation
|
|
|
*/
|
|
|
- if (pages == null || pages.length === 0) {
|
|
|
- logger.error('pageIds is null or 0 length.');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- if (pages.length > LIMIT_FOR_MULTIPLE_PAGE_OP) {
|
|
|
- throw Error(`The maximum number of pageIds allowed is ${LIMIT_FOR_MULTIPLE_PAGE_OP}.`);
|
|
|
- }
|
|
|
+ const socket = this.crowi.socketIoService.getDefaultSocket();
|
|
|
|
|
|
const pagesToNormalize = omitDuplicateAreaPageFromPages(pages);
|
|
|
|
|
|
@@ -2365,25 +2437,29 @@ class PageService {
|
|
|
[normalizablePages, nonNormalizablePages] = await this.crowi.pageGrantService.separateNormalizableAndNotNormalizablePages(user, pagesToNormalize);
|
|
|
}
|
|
|
catch (err) {
|
|
|
+ socket.emit(SocketEventName.PageMigrationError);
|
|
|
throw err;
|
|
|
}
|
|
|
|
|
|
if (normalizablePages.length === 0) {
|
|
|
- // socket.emit('normalizeParentRecursivelyByPages', { error: err.message }); TODO: use socket to tell user
|
|
|
+ socket.emit(SocketEventName.PageMigrationError);
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
if (nonNormalizablePages.length !== 0) {
|
|
|
- // TODO: iterate nonNormalizablePages and send socket error to client so that the user can know which path failed to migrate
|
|
|
- // socket.emit('normalizeParentRecursivelyByPages', { error: err.message }); TODO: use socket to tell user
|
|
|
+ const nonNormalizablePagePaths: string[] = nonNormalizablePages.map(p => p.path);
|
|
|
+ socket.emit(SocketEventName.PageMigrationError, { paths: nonNormalizablePagePaths });
|
|
|
+ logger.debug('Some pages could not be converted.', nonNormalizablePagePaths);
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
* Main Operation (s)
|
|
|
*/
|
|
|
+ const errorPagePaths: string[] = [];
|
|
|
for await (const page of normalizablePages) {
|
|
|
const canOperate = await this.crowi.pageOperationService.canOperate(true, page.path, page.path);
|
|
|
if (!canOperate) {
|
|
|
+ errorPagePaths.push(page.path);
|
|
|
throw Error(`Cannot operate normalizeParentRecursiively to path "${page.path}" right now.`);
|
|
|
}
|
|
|
|
|
|
@@ -2395,6 +2471,7 @@ class PageService {
|
|
|
const existingPage = await builder.query.exec();
|
|
|
|
|
|
if (existingPage?.parent != null) {
|
|
|
+ errorPagePaths.push(page.path);
|
|
|
throw Error('This page has already converted.');
|
|
|
}
|
|
|
|
|
|
@@ -2410,6 +2487,7 @@ class PageService {
|
|
|
});
|
|
|
}
|
|
|
catch (err) {
|
|
|
+ errorPagePaths.push(page.path);
|
|
|
logger.error('Failed to create PageOperation document.', err);
|
|
|
throw err;
|
|
|
}
|
|
|
@@ -2418,10 +2496,17 @@ class PageService {
|
|
|
await this.normalizeParentRecursivelyMainOperation(page, user, pageOp._id);
|
|
|
}
|
|
|
catch (err) {
|
|
|
+ errorPagePaths.push(page.path);
|
|
|
logger.err('Failed to run normalizeParentRecursivelyMainOperation.', err);
|
|
|
throw err;
|
|
|
}
|
|
|
}
|
|
|
+ if (errorPagePaths.length === 0) {
|
|
|
+ socket.emit(SocketEventName.PageMigrationSuccess);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ socket.emit(SocketEventName.PageMigrationError, { paths: errorPagePaths });
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
async normalizeParentRecursivelyMainOperation(page, user, pageOpId: ObjectIdLike): Promise<void> {
|
|
|
@@ -2469,7 +2554,11 @@ class PageService {
|
|
|
|
|
|
const { prevDescendantCount } = options;
|
|
|
const newDescendantCount = pageAfterUpdatingDescendantCount.descendantCount;
|
|
|
- const inc = (newDescendantCount - prevDescendantCount) + 1;
|
|
|
+ let inc = newDescendantCount - prevDescendantCount;
|
|
|
+ const isAlreadyConverted = page.parent != null;
|
|
|
+ if (!isAlreadyConverted) {
|
|
|
+ inc += 1;
|
|
|
+ }
|
|
|
await this.updateDescendantCountOfAncestors(page._id, inc, false);
|
|
|
}
|
|
|
catch (err) {
|