|
|
@@ -43,7 +43,7 @@ type TargetAndAncestorsResult = {
|
|
|
export type CreateMethod = (path: string, body: string, user, options) => Promise<PageDocument & { _id: any }>
|
|
|
export interface PageModel extends Model<PageDocument> {
|
|
|
[x: string]: any; // for obsolete methods
|
|
|
- createEmptyPagesByPaths(paths: string[], publicOnly?: boolean): Promise<void>
|
|
|
+ createEmptyPagesByPaths(paths: string[], onlyMigratedAsExistingPages?: boolean, publicOnly?: boolean): Promise<void>
|
|
|
getParentAndFillAncestors(path: string): Promise<PageDocument & { _id: any }>
|
|
|
findByIdsAndViewer(pageIds: string[], user, userGroups?, includeEmpty?: boolean): Promise<PageDocument[]>
|
|
|
findByPathAndViewer(path: string | null, user, userGroups?, useFindOne?: boolean, includeEmpty?: boolean): Promise<PageDocument[]>
|
|
|
@@ -121,9 +121,12 @@ const generateChildrenRegExp = (path: string): RegExp => {
|
|
|
/*
|
|
|
* Create empty pages if the page in paths didn't exist
|
|
|
*/
|
|
|
-schema.statics.createEmptyPagesByPaths = async function(paths: string[], publicOnly = false): Promise<void> {
|
|
|
+schema.statics.createEmptyPagesByPaths = async function(paths: string[], onlyMigratedAsExistingPages = true, publicOnly = false): Promise<void> {
|
|
|
// find existing parents
|
|
|
const builder = new PageQueryBuilder(this.find(publicOnly ? { grant: GRANT_PUBLIC } : {}, { _id: 0, path: 1 }), true);
|
|
|
+ if (onlyMigratedAsExistingPages) {
|
|
|
+ builder.addConditionAsMigrated();
|
|
|
+ }
|
|
|
const existingPages = await builder
|
|
|
.addConditionToListByPathsArray(paths)
|
|
|
.query
|
|
|
@@ -167,7 +170,7 @@ schema.statics.createEmptyPage = async function(
|
|
|
* @param exPage a page document to be replaced
|
|
|
* @returns Promise<void>
|
|
|
*/
|
|
|
-schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?, deleteExPageIfEmpty = false): Promise<void> {
|
|
|
+schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?, deleteExPageIfEmpty = false) {
|
|
|
// find parent
|
|
|
const parent = await this.findOne({ _id: exPage.parent });
|
|
|
if (parent == null) {
|
|
|
@@ -207,6 +210,8 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
|
|
|
await this.deleteOne({ _id: exPage._id });
|
|
|
logger.warn('Deleted empty page since it was replaced with another page.');
|
|
|
}
|
|
|
+
|
|
|
+ return this.findById(newTarget._id);
|
|
|
};
|
|
|
|
|
|
/**
|
|
|
@@ -217,9 +222,15 @@ schema.statics.replaceTargetWithPage = async function(exPage, pageToReplaceWith?
|
|
|
*/
|
|
|
schema.statics.getParentAndFillAncestors = async function(path: string): Promise<PageDocument> {
|
|
|
const parentPath = nodePath.dirname(path);
|
|
|
- const parent = await this.findOne({ path: parentPath }); // find the oldest parent which must always be the true parent
|
|
|
- if (parent != null) {
|
|
|
- return parent;
|
|
|
+
|
|
|
+ const builder1 = new PageQueryBuilder(this.find({ path: parentPath }), true);
|
|
|
+ const pagesCanBeParent = await builder1
|
|
|
+ .addConditionAsMigrated()
|
|
|
+ .query
|
|
|
+ .exec();
|
|
|
+
|
|
|
+ if (pagesCanBeParent.length >= 1) {
|
|
|
+ return pagesCanBeParent[0]; // the earliest page will be the result
|
|
|
}
|
|
|
|
|
|
/*
|
|
|
@@ -231,8 +242,8 @@ schema.statics.getParentAndFillAncestors = async function(path: string): Promise
|
|
|
await this.createEmptyPagesByPaths(ancestorPaths);
|
|
|
|
|
|
// find ancestors
|
|
|
- const builder = new PageQueryBuilder(this.find(), true);
|
|
|
- const ancestors = await builder
|
|
|
+ const builder2 = new PageQueryBuilder(this.find(), true);
|
|
|
+ const ancestors = await builder2
|
|
|
.addConditionToListByPathsArray(ancestorPaths)
|
|
|
.addConditionToSortPagesByDescPath()
|
|
|
.query
|
|
|
@@ -244,15 +255,14 @@ schema.statics.getParentAndFillAncestors = async function(path: string): Promise
|
|
|
// bulkWrite to update ancestors
|
|
|
const nonRootAncestors = ancestors.filter(page => !isTopPage(page.path));
|
|
|
const operations = nonRootAncestors.map((page) => {
|
|
|
- const { path } = page;
|
|
|
- const parentPath = nodePath.dirname(path);
|
|
|
+ const parentPath = nodePath.dirname(page.path);
|
|
|
return {
|
|
|
updateOne: {
|
|
|
filter: {
|
|
|
- path,
|
|
|
+ _id: page._id,
|
|
|
},
|
|
|
update: {
|
|
|
- parent: ancestorsMap.get(parentPath),
|
|
|
+ parent: ancestorsMap.get(parentPath)._id,
|
|
|
},
|
|
|
},
|
|
|
};
|
|
|
@@ -315,9 +325,8 @@ schema.statics.findTargetAndAncestorsByPathOrId = async function(pathOrId: strin
|
|
|
if (!hasSlash(pathOrId)) {
|
|
|
const _id = pathOrId;
|
|
|
const page = await this.findOne({ _id });
|
|
|
- if (page == null) throw new Error('Page not found.');
|
|
|
|
|
|
- path = page.path;
|
|
|
+ path = page == null ? '/' : page.path;
|
|
|
}
|
|
|
else {
|
|
|
path = pathOrId;
|
|
|
@@ -423,40 +432,6 @@ async function pushRevision(pageData, newRevision, user) {
|
|
|
return pageData.save();
|
|
|
}
|
|
|
|
|
|
-/**
|
|
|
- * return aggregate condition to get following pages
|
|
|
- * - page that has the same path as the provided path
|
|
|
- * - pages that are descendants of the above page
|
|
|
- * pages without parent will be ignored
|
|
|
- */
|
|
|
-schema.statics.getAggrConditionForPageWithProvidedPathAndDescendants = function(path:string) {
|
|
|
- let match;
|
|
|
- if (isTopPage(path)) {
|
|
|
- match = {
|
|
|
- // https://regex101.com/r/Kip2rV/1
|
|
|
- $match: { $or: [{ path: { $regex: '^/.*' }, parent: { $ne: null } }, { path: '/' }] },
|
|
|
- };
|
|
|
- }
|
|
|
- else {
|
|
|
- match = {
|
|
|
- // https://regex101.com/r/mJvGrG/1
|
|
|
- $match: { path: { $regex: `^${path}(/.*|$)` }, parent: { $ne: null } },
|
|
|
- };
|
|
|
- }
|
|
|
- return [
|
|
|
- match,
|
|
|
- {
|
|
|
- $project: {
|
|
|
- path: 1,
|
|
|
- parent: 1,
|
|
|
- field_length: { $strLenCP: '$path' },
|
|
|
- },
|
|
|
- },
|
|
|
- { $sort: { field_length: -1 } },
|
|
|
- { $project: { field_length: 0 } },
|
|
|
- ];
|
|
|
-};
|
|
|
-
|
|
|
/**
|
|
|
* add/subtract descendantCount of pages with provided paths by increment.
|
|
|
* increment can be negative number
|
|
|
@@ -512,6 +487,9 @@ schema.statics.recountDescendantCount = async function(id: ObjectIdLike):Promise
|
|
|
schema.statics.findAncestorsUsingParentRecursively = async function(pageId: ObjectIdLike, shouldIncludeTarget: boolean) {
|
|
|
const self = this;
|
|
|
const target = await this.findById(pageId);
|
|
|
+ if (target == null) {
|
|
|
+ throw Error('Target not found');
|
|
|
+ }
|
|
|
|
|
|
async function findAncestorsRecursively(target, ancestors = shouldIncludeTarget ? [target] : []) {
|
|
|
const parent = await self.findOne({ _id: target.parent });
|
|
|
@@ -531,44 +509,41 @@ schema.statics.findAncestorsUsingParentRecursively = async function(pageId: Obje
|
|
|
* @param pageId ObjectIdLike
|
|
|
* @returns Promise<void>
|
|
|
*/
|
|
|
-schema.statics.removeLeafEmptyPagesById = async function(pageId: ObjectIdLike): Promise<void> {
|
|
|
+schema.statics.removeLeafEmptyPagesRecursively = async function(pageId: ObjectIdLike): Promise<void> {
|
|
|
const self = this;
|
|
|
|
|
|
- const initialLeafPage = await this.findById(pageId);
|
|
|
+ const initialPage = await this.findById(pageId);
|
|
|
|
|
|
- if (initialLeafPage == null) {
|
|
|
+ if (initialPage == null) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- if (!initialLeafPage.isEmpty) {
|
|
|
+ if (!initialPage.isEmpty) {
|
|
|
return;
|
|
|
}
|
|
|
|
|
|
- async function generatePageIdsToRemove(page, pageIds: ObjectIdLike[]): Promise<ObjectIdLike[]> {
|
|
|
- const nextPage = await self.findById(page.parent);
|
|
|
-
|
|
|
- if (nextPage == null) {
|
|
|
+ async function generatePageIdsToRemove(childPage, page, pageIds: ObjectIdLike[] = []): Promise<ObjectIdLike[]> {
|
|
|
+ if (!page.isEmpty) {
|
|
|
return pageIds;
|
|
|
}
|
|
|
|
|
|
- // delete leaf empty pages
|
|
|
- const isNextPageEmpty = nextPage.isEmpty;
|
|
|
-
|
|
|
- if (!isNextPageEmpty) {
|
|
|
+ const isChildrenOtherThanTargetExist = await self.exists({ _id: { $ne: childPage?._id }, parent: page._id });
|
|
|
+ if (isChildrenOtherThanTargetExist) {
|
|
|
return pageIds;
|
|
|
}
|
|
|
|
|
|
- const isSiblingsExist = await self.exists({ parent: nextPage.parent, _id: { $ne: nextPage._id } });
|
|
|
- if (isSiblingsExist) {
|
|
|
+ pageIds.push(page._id);
|
|
|
+
|
|
|
+ const nextPage = await self.findById(page.parent);
|
|
|
+
|
|
|
+ if (nextPage == null) {
|
|
|
return pageIds;
|
|
|
}
|
|
|
|
|
|
- return generatePageIdsToRemove(nextPage, [...pageIds, nextPage._id]);
|
|
|
+ return generatePageIdsToRemove(page, nextPage, pageIds);
|
|
|
}
|
|
|
|
|
|
- const initialPageIdsToRemove = [initialLeafPage._id];
|
|
|
-
|
|
|
- const pageIdsToRemove = await generatePageIdsToRemove(initialLeafPage, initialPageIdsToRemove);
|
|
|
+ const pageIdsToRemove = await generatePageIdsToRemove(null, initialPage);
|
|
|
|
|
|
await this.deleteMany({ _id: { $in: pageIdsToRemove } });
|
|
|
};
|
|
|
@@ -592,6 +567,10 @@ schema.statics.normalizeDescendantCountById = async function(pageId) {
|
|
|
return this.updateOne({ _id: pageId }, { $set: { descendantCount: sumChildrenDescendantCount + sumChildPages } }, { new: true });
|
|
|
};
|
|
|
|
|
|
+schema.statics.takeOffFromTree = async function(pageId: ObjectIdLike) {
|
|
|
+ return this.findByIdAndUpdate(pageId, { $set: { parent: null } });
|
|
|
+};
|
|
|
+
|
|
|
export type PageCreateOptions = {
|
|
|
format?: string
|
|
|
grantUserGroupId?: ObjectIdLike
|
|
|
@@ -608,7 +587,7 @@ export default (crowi: Crowi): any => {
|
|
|
}
|
|
|
|
|
|
schema.statics.create = async function(path: string, body: string, user, options: PageCreateOptions = {}) {
|
|
|
- if (crowi.pageGrantService == null || crowi.configManager == null || crowi.pageService == null) {
|
|
|
+ if (crowi.pageGrantService == null || crowi.configManager == null || crowi.pageService == null || crowi.pageOperationService == null) {
|
|
|
throw Error('Crowi is not setup');
|
|
|
}
|
|
|
|
|
|
@@ -618,6 +597,11 @@ export default (crowi: Crowi): any => {
|
|
|
return this.createV4(path, body, user, options);
|
|
|
}
|
|
|
|
|
|
+ const canOperate = await crowi.pageOperationService.canOperate(false, null, path);
|
|
|
+ if (!canOperate) {
|
|
|
+ throw Error(`Cannot operate create to path "${path}" right now.`);
|
|
|
+ }
|
|
|
+
|
|
|
const Page = this;
|
|
|
const Revision = crowi.model('Revision');
|
|
|
const {
|
|
|
@@ -699,8 +683,6 @@ export default (crowi: Crowi): any => {
|
|
|
|
|
|
let savedPage = await page.save();
|
|
|
|
|
|
- await crowi.pageService.updateDescendantCountOfAncestors(page._id, 1, false);
|
|
|
-
|
|
|
/*
|
|
|
* After save
|
|
|
*/
|
|
|
@@ -721,6 +703,9 @@ export default (crowi: Crowi): any => {
|
|
|
|
|
|
pageEvent.emit('create', savedPage, user);
|
|
|
|
|
|
+ // update descendantCount asynchronously
|
|
|
+ await crowi.pageService.updateDescendantCountOfAncestors(savedPage._id, 1, false);
|
|
|
+
|
|
|
return savedPage;
|
|
|
};
|
|
|
|