|
|
@@ -268,6 +268,20 @@ class PageService {
|
|
|
return page.grant !== Page.GRANT_RESTRICTED && page.grant !== Page.GRANT_SPECIFIED;
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * Remove all empty pages at leaf position by page whose parent will change or which will be deleted.
|
|
|
+ * @param page Page whose parent will change or which will be deleted
|
|
|
+ */
|
|
|
+ async removeLeafEmptyPages(page): Promise<void> {
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+
|
|
|
+ // delete leaf empty pages
|
|
|
+ const shouldDeleteLeafEmptyPages = !(await Page.exists({ parent: page.parent, _id: { $ne: page._id } }));
|
|
|
+ if (shouldDeleteLeafEmptyPages) {
|
|
|
+ await Page.removeLeafEmptyPagesById(page.parent);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
/**
|
|
|
* Generate read stream to operate descendants of the specified page path
|
|
|
* @param {string} targetPagePath
|
|
|
@@ -356,15 +370,39 @@ class PageService {
|
|
|
update.lastUpdateUser = user;
|
|
|
update.updatedAt = new Date();
|
|
|
}
|
|
|
- const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
|
|
|
|
|
|
+ // *************************
|
|
|
+ // * before rename target page
|
|
|
+ // *************************
|
|
|
+ const oldPageParentId = page.parent; // this is used to update descendantCount of old page's ancestors
|
|
|
+
|
|
|
+ // *************************
|
|
|
+ // * rename target page
|
|
|
+ // *************************
|
|
|
+ const renamedPage = await Page.findByIdAndUpdate(page._id, { $set: update }, { new: true });
|
|
|
this.pageEvent.emit('rename', page, user);
|
|
|
|
|
|
+ // *************************
|
|
|
+ // * after rename target page
|
|
|
+ // *************************
|
|
|
+ // rename descendants and update descendantCount asynchronously
|
|
|
+ this.resumableRenameDescendants(page, newPagePath, user, options, shouldUseV4Process, renamedPage, oldPageParentId);
|
|
|
+
|
|
|
+ return renamedPage;
|
|
|
+ }
|
|
|
+
|
|
|
+ async resumableRenameDescendants(page, newPagePath, user, options, shouldUseV4Process, renamedPage, oldPageParentId) {
|
|
|
// TODO: resume
|
|
|
// update descendants first
|
|
|
- this.renameDescendantsWithStream(page, newPagePath, user, options, shouldUseV4Process);
|
|
|
+ await this.renameDescendantsWithStream(page, newPagePath, user, options, shouldUseV4Process);
|
|
|
|
|
|
- return renamedPage;
|
|
|
+ // reduce ancestore's descendantCount
|
|
|
+ const nToReduce = -1 * ((page.isEmpty ? 0 : 1) + page.descendantCount);
|
|
|
+ await this.updateDescendantCountOfAncestors(oldPageParentId, nToReduce, true);
|
|
|
+
|
|
|
+ // increase ancestore's descendantCount
|
|
|
+ const nToIncrease = (renamedPage.isEmpty ? 0 : 1) + page.descendantCount;
|
|
|
+ await this.updateDescendantCountOfAncestors(renamedPage._id, nToIncrease, false);
|
|
|
}
|
|
|
|
|
|
// !!renaming always include descendant pages!!
|
|
|
@@ -676,9 +714,17 @@ class PageService {
|
|
|
|
|
|
newPagePath = this.crowi.xss.process(newPagePath); // eslint-disable-line no-param-reassign
|
|
|
|
|
|
- const createdPage = await (Page.create as CreateMethod)(
|
|
|
- newPagePath, page.revision.body, user, options,
|
|
|
- );
|
|
|
+ let createdPage;
|
|
|
+
|
|
|
+ if (page.isEmpty) {
|
|
|
+ const parent = await Page.getParentAndFillAncestors(newPagePath);
|
|
|
+ createdPage = await Page.createEmptyPage(newPagePath, parent);
|
|
|
+ }
|
|
|
+ else {
|
|
|
+ createdPage = await (Page.create as CreateMethod)(
|
|
|
+ newPagePath, page.revision.body, user, options,
|
|
|
+ );
|
|
|
+ }
|
|
|
|
|
|
// take over tags
|
|
|
const originTags = await page.findRelatedTagsById();
|
|
|
@@ -694,12 +740,16 @@ class PageService {
|
|
|
|
|
|
// TODO: resume
|
|
|
if (isRecursively) {
|
|
|
- this.duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process);
|
|
|
+ this.resumableDuplicateDescendants(page, newPagePath, user, shouldUseV4Process, createdPage._id);
|
|
|
}
|
|
|
-
|
|
|
return result;
|
|
|
}
|
|
|
|
|
|
+ async resumableDuplicateDescendants(page, newPagePath, user, shouldUseV4Process, createdPageId) {
|
|
|
+ const descendantCountAppliedToAncestors = await this.duplicateDescendantsWithStream(page, newPagePath, user, shouldUseV4Process);
|
|
|
+ await this.updateDescendantCountOfAncestors(createdPageId, descendantCountAppliedToAncestors, false);
|
|
|
+ }
|
|
|
+
|
|
|
async duplicateV4(page, newPagePath, user, isRecursively) {
|
|
|
const Page = this.crowi.model('Page');
|
|
|
const PageTagRelation = mongoose.model('PageTagRelation') as any; // TODO: Typescriptize model
|
|
|
@@ -803,14 +853,7 @@ class PageService {
|
|
|
pageIdMapping[page._id] = newPageId;
|
|
|
|
|
|
let newPage;
|
|
|
- if (page.isEmpty) {
|
|
|
- newPage = {
|
|
|
- _id: newPageId,
|
|
|
- path: newPagePath,
|
|
|
- isEmpty: true,
|
|
|
- };
|
|
|
- }
|
|
|
- else {
|
|
|
+ if (!page.isEmpty) {
|
|
|
newPage = {
|
|
|
_id: newPageId,
|
|
|
path: newPagePath,
|
|
|
@@ -821,14 +864,11 @@ class PageService {
|
|
|
lastUpdateUser: user._id,
|
|
|
revision: revisionId,
|
|
|
};
|
|
|
+ newRevisions.push({
|
|
|
+ _id: revisionId, pageId: newPageId, body: pageIdRevisionMapping[page._id].body, author: user._id, format: 'markdown',
|
|
|
+ });
|
|
|
}
|
|
|
-
|
|
|
newPages.push(newPage);
|
|
|
-
|
|
|
- newRevisions.push({
|
|
|
- _id: revisionId, pageId: newPageId, body: pageIdRevisionMapping[page._id].body, author: user._id, format: 'markdown',
|
|
|
- });
|
|
|
-
|
|
|
});
|
|
|
|
|
|
await Page.insertMany(newPages, { ordered: false });
|
|
|
@@ -898,11 +938,13 @@ class PageService {
|
|
|
const normalizeParentAndDescendantCountOfDescendants = this.normalizeParentAndDescendantCountOfDescendants.bind(this);
|
|
|
const pageEvent = this.pageEvent;
|
|
|
let count = 0;
|
|
|
+ let nNonEmptyDuplicatedPages = 0;
|
|
|
const writeStream = new Writable({
|
|
|
objectMode: true,
|
|
|
async write(batch, encoding, callback) {
|
|
|
try {
|
|
|
count += batch.length;
|
|
|
+ nNonEmptyDuplicatedPages += batch.filter(page => !page.isEmpty).length;
|
|
|
await duplicateDescendants(batch, user, pathRegExp, newPagePathPrefix, shouldUseV4Process);
|
|
|
logger.debug(`Adding pages progressing: (count=${count})`);
|
|
|
}
|
|
|
@@ -938,6 +980,9 @@ class PageService {
|
|
|
.pipe(createBatchStream(BULK_REINDEX_SIZE))
|
|
|
.pipe(writeStream);
|
|
|
|
|
|
+ await streamToPromise(writeStream);
|
|
|
+
|
|
|
+ return nNonEmptyDuplicatedPages;
|
|
|
}
|
|
|
|
|
|
private async duplicateDescendantsWithStreamV4(page, newPagePath, user) {
|
|
|
@@ -976,6 +1021,9 @@ class PageService {
|
|
|
.pipe(createBatchStream(BULK_REINDEX_SIZE))
|
|
|
.pipe(writeStream);
|
|
|
|
|
|
+ await streamToPromise(writeStream);
|
|
|
+
|
|
|
+ return count;
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
@@ -1014,10 +1062,8 @@ class PageService {
|
|
|
// update descendantCount of ancestors'
|
|
|
await this.updateDescendantCountOfAncestors(page.parent, -1, true);
|
|
|
|
|
|
- const shouldDeleteLeafEmptyPages = !shouldReplace;
|
|
|
- if (shouldDeleteLeafEmptyPages) {
|
|
|
- // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
|
|
|
- }
|
|
|
+ // delete leaf empty pages
|
|
|
+ await this.removeLeafEmptyPages(page);
|
|
|
}
|
|
|
|
|
|
let deletedPage;
|
|
|
@@ -1050,7 +1096,8 @@ class PageService {
|
|
|
if (page.parent != null) {
|
|
|
await this.updateDescendantCountOfAncestors(page.parent, (deletedDescendantCount + 1) * -1, true);
|
|
|
|
|
|
- // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
|
|
|
+ // delete leaf empty pages
|
|
|
+ await this.removeLeafEmptyPages(page);
|
|
|
}
|
|
|
})();
|
|
|
}
|
|
|
@@ -1281,10 +1328,11 @@ class PageService {
|
|
|
|
|
|
if (!isRecursively) {
|
|
|
await this.updateDescendantCountOfAncestors(page.parent, -1, true);
|
|
|
-
|
|
|
- // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
|
|
|
}
|
|
|
|
|
|
+ // delete leaf empty pages
|
|
|
+ await this.removeLeafEmptyPages(page);
|
|
|
+
|
|
|
if (!page.isEmpty && !preventEmitting) {
|
|
|
this.pageEvent.emit('deleteCompletely', page, user);
|
|
|
}
|
|
|
@@ -1299,8 +1347,6 @@ class PageService {
|
|
|
if (page.parent != null) {
|
|
|
await this.updateDescendantCountOfAncestors(page.parent, (deletedDescendantCount + 1) * -1, true);
|
|
|
}
|
|
|
-
|
|
|
- // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
|
|
|
})();
|
|
|
}
|
|
|
|
|
|
@@ -1460,7 +1506,8 @@ class PageService {
|
|
|
if (page.parent != null) {
|
|
|
await this.updateDescendantCountOfAncestors(page.parent, revertedDescendantCount + 1, true);
|
|
|
|
|
|
- // TODO https://redmine.weseek.co.jp/issues/87667 : delete leaf empty pages here
|
|
|
+ // delete leaf empty pages
|
|
|
+ await this.removeLeafEmptyPages(page);
|
|
|
}
|
|
|
})();
|
|
|
}
|
|
|
@@ -1844,7 +1891,7 @@ class PageService {
|
|
|
}
|
|
|
|
|
|
// TODO: use socket to send status to the client
|
|
|
- async v5InitialMigration(grant) {
|
|
|
+ async normalizeAllPublicPages() {
|
|
|
// const socket = this.crowi.socketIoService.getAdminSocket();
|
|
|
|
|
|
let isUnique;
|
|
|
@@ -1870,7 +1917,8 @@ class PageService {
|
|
|
|
|
|
// then migrate
|
|
|
try {
|
|
|
- await this.normalizeParentRecursively(grant, null, true);
|
|
|
+ const Page = mongoose.model('Page') as unknown as PageModel;
|
|
|
+ await this.normalizeParentRecursively(Page.GRANT_PUBLIC, null, true);
|
|
|
}
|
|
|
catch (err) {
|
|
|
logger.error('V5 initial miration failed.', err);
|
|
|
@@ -2134,7 +2182,8 @@ class PageService {
|
|
|
objectMode: true,
|
|
|
async write(pageDocuments, encoding, callback) {
|
|
|
for await (const document of pageDocuments) {
|
|
|
- await Page.recountDescendantCountOfSelfAndDescendants(document._id);
|
|
|
+ const descendantCount = await Page.recountDescendantCount(document._id);
|
|
|
+ await Page.findByIdAndUpdate(document._id, { descendantCount });
|
|
|
}
|
|
|
callback();
|
|
|
},
|