2022-03-05 22:57:23 +04:00
|
|
|
import Page, { PageData } from '../models/page';
|
|
|
|
import Alias from '../models/alias';
|
2022-06-22 07:09:08 -07:00
|
|
|
import PagesOrder from './pagesOrder';
|
|
|
|
import PageOrder from '../models/pageOrder';
|
|
|
|
import HttpException from "../exceptions/httpException";
|
2022-03-05 22:57:23 +04:00
|
|
|
|
|
|
|
type PageDataFields = keyof PageData;
|
2018-08-17 13:58:44 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @class Pages
|
|
|
|
* @classdesc Pages controller
|
|
|
|
*/
|
|
|
|
class Pages {
|
|
|
|
/**
|
|
|
|
* Fields required for page model creation
|
|
|
|
*
|
|
|
|
* @returns {['title', 'body']}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static get REQUIRED_FIELDS(): Array<PageDataFields> {
|
2018-10-04 22:08:21 +03:00
|
|
|
return [ 'body' ];
|
2018-08-17 13:58:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Find and return page model with passed id
|
|
|
|
*
|
|
|
|
* @param {string} id - page id
|
|
|
|
* @returns {Promise<Page>}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static async get(id: string): Promise<Page> {
|
|
|
|
const page = await Page.get(id);
|
2018-08-17 13:58:44 +03:00
|
|
|
|
|
|
|
if (!page._id) {
|
|
|
|
throw new Error('Page with given id does not exist');
|
|
|
|
}
|
|
|
|
|
|
|
|
return page;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Return all pages
|
|
|
|
*
|
|
|
|
* @returns {Promise<Page[]>}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static async getAll(): Promise<Page[]> {
|
|
|
|
return Page.getAll();
|
2018-08-17 13:58:44 +03:00
|
|
|
}
|
|
|
|
|
2019-01-14 17:53:10 +03:00
|
|
|
/**
|
|
|
|
* Return all pages without children of passed page
|
|
|
|
*
|
|
|
|
* @param {string} parent - id of current page
|
|
|
|
* @returns {Promise<Page[]>}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static async getAllExceptChildren(parent: string): Promise<Page[]> {
|
2020-05-09 05:38:25 +03:00
|
|
|
const pagesAvailable = this.removeChildren(await Pages.getAll(), parent);
|
2019-01-14 17:53:10 +03:00
|
|
|
|
2022-03-05 22:57:23 +04:00
|
|
|
const nullFilteredPages: Page[] = [];
|
|
|
|
|
|
|
|
pagesAvailable.forEach(async item => {
|
|
|
|
if (item instanceof Page) {
|
|
|
|
nullFilteredPages.push(item);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
return nullFilteredPages;
|
2019-01-14 17:53:10 +03:00
|
|
|
}
|
|
|
|
|
2022-06-22 07:09:08 -07:00
|
|
|
/**
|
|
|
|
* Group all pages by their parents
|
|
|
|
* If the pageId is passed, it excludes passed page from result pages
|
|
|
|
*
|
|
|
|
* @param {string} pageId - pageId to exclude from result pages
|
|
|
|
* @returns {Page[]}
|
|
|
|
*/
|
|
|
|
public static async groupByParent(pageId = ''): Promise<Page[]> {
|
|
|
|
const result: Page[] = [];
|
|
|
|
const orderGroupedByParent: Record<string, string[]> = {};
|
|
|
|
const rootPageOrder = await PagesOrder.getRootPageOrder();
|
|
|
|
const childPageOrder = await PagesOrder.getChildPageOrder();
|
|
|
|
const orphanPageOrder: PageOrder[] = [];
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If there is no root and child page order, then it returns an empty array
|
|
|
|
*/
|
|
|
|
if (!rootPageOrder || (!rootPageOrder && childPageOrder.length <= 0)) {
|
|
|
|
return [];
|
|
|
|
}
|
|
|
|
|
|
|
|
const pages = (await this.getAll()).reduce((map, _page) => {
|
|
|
|
map.set(_page._id, _page);
|
|
|
|
|
|
|
|
return map;
|
|
|
|
}, new Map);
|
|
|
|
const idsOfRootPages = rootPageOrder.order;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* It groups root pages and 1 level pages by its parent
|
|
|
|
*/
|
|
|
|
idsOfRootPages.reduce((prev, curr, idx) => {
|
|
|
|
const childPages:PageOrder[] = [];
|
|
|
|
|
|
|
|
childPageOrder.forEach((pageOrder, _idx) => {
|
|
|
|
if (pageOrder.page === curr) {
|
|
|
|
childPages.push(pageOrder);
|
|
|
|
childPageOrder.splice(_idx, 1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
const hasChildPage = childPages.length > 0;
|
|
|
|
|
|
|
|
prev[curr] = [];
|
|
|
|
prev[curr].push(curr);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* It attaches 1 level page id to its parent page id
|
|
|
|
*/
|
|
|
|
if (hasChildPage) {
|
|
|
|
prev[curr].push(...childPages[0].order);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If non-attached childPages which is not 1 level page still remains,
|
|
|
|
* It is stored as an orphan page so that it can be processed in the next statements
|
|
|
|
*/
|
|
|
|
if (idx === idsOfRootPages.length - 1 && childPageOrder.length > 0) {
|
|
|
|
orphanPageOrder.push(...childPageOrder);
|
|
|
|
}
|
|
|
|
|
|
|
|
return prev;
|
|
|
|
}, orderGroupedByParent);
|
|
|
|
|
|
|
|
let count = 0;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* It groups remained ungrouped pages by its parent
|
|
|
|
*/
|
|
|
|
while (orphanPageOrder.length > 0) {
|
|
|
|
if (count >= 1000) {
|
|
|
|
throw new HttpException(500, `Page cannot be processed`);
|
|
|
|
}
|
|
|
|
|
|
|
|
orphanPageOrder.forEach((orphanOrder, idx) => {
|
|
|
|
// It loops each of grouped orders formatted as [root page id(1): corresponding child pages id(2)]
|
|
|
|
Object.entries(orderGroupedByParent).forEach(([parentPageId, value]) => {
|
|
|
|
// If (2) contains orphanOrder's parent id(page)
|
|
|
|
if (orphanOrder.page && orphanOrder.order && value.includes(orphanOrder.page)) {
|
|
|
|
// Append orphanOrder's id(order) into its parent id
|
|
|
|
orderGroupedByParent[parentPageId].splice(value.indexOf(orphanOrder.page) + 1, 0, ...orphanOrder.order);
|
|
|
|
// Finally, remove orphanOrder from orphanPageOrder
|
|
|
|
orphanPageOrder.splice(idx, 1);
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
count += 1;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* It converts grouped pages(object) to array
|
|
|
|
*/
|
|
|
|
Object.values(orderGroupedByParent).flatMap(arr => [ ...arr ])
|
|
|
|
.forEach(arr => {
|
|
|
|
result.push(pages.get(arr));
|
|
|
|
});
|
|
|
|
|
|
|
|
/**
|
|
|
|
* If the pageId passed, it excludes itself from result pages
|
|
|
|
* Otherwise just returns result itself
|
|
|
|
*/
|
|
|
|
if (pageId) {
|
|
|
|
return this.removeChildren(result, pageId).reduce((prev, curr) => {
|
|
|
|
if (curr instanceof Page) {
|
|
|
|
prev.push(curr);
|
|
|
|
}
|
|
|
|
|
|
|
|
return prev;
|
|
|
|
}, Array<Page>());
|
|
|
|
} else {
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-14 17:53:10 +03:00
|
|
|
/**
|
|
|
|
* Set all children elements to null
|
|
|
|
*
|
2022-03-05 22:57:23 +04:00
|
|
|
* @param {Array<Page|null>} [pagesAvailable] - Array of all pages
|
2019-01-14 17:53:10 +03:00
|
|
|
* @param {string} parent - id of parent page
|
|
|
|
* @returns {Array<?Page>}
|
|
|
|
*/
|
2022-06-22 07:09:08 -07:00
|
|
|
public static removeChildren(pagesAvailable: Array<Page | null>, parent: string | undefined): Array<Page | null> {
|
2019-01-14 17:53:10 +03:00
|
|
|
pagesAvailable.forEach(async (item, index) => {
|
|
|
|
if (item === null || item._parent !== parent) {
|
|
|
|
return;
|
|
|
|
}
|
|
|
|
pagesAvailable[index] = null;
|
|
|
|
pagesAvailable = Pages.removeChildren(pagesAvailable, item._id);
|
|
|
|
});
|
2020-05-09 05:38:25 +03:00
|
|
|
|
2019-01-14 17:53:10 +03:00
|
|
|
return pagesAvailable;
|
|
|
|
}
|
|
|
|
|
2018-08-17 13:58:44 +03:00
|
|
|
/**
|
|
|
|
* Create new page model and save it in the database
|
|
|
|
*
|
2022-03-05 22:57:23 +04:00
|
|
|
* @param {PageData} data - info about page
|
2018-08-17 13:58:44 +03:00
|
|
|
* @returns {Promise<Page>}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static async insert(data: PageData): Promise<Page> {
|
2018-10-04 22:08:21 +03:00
|
|
|
try {
|
|
|
|
Pages.validate(data);
|
2018-08-17 13:58:44 +03:00
|
|
|
|
2022-03-05 22:57:23 +04:00
|
|
|
const page = new Page(data);
|
2018-08-17 13:58:44 +03:00
|
|
|
|
2019-01-25 02:23:00 +03:00
|
|
|
const insertedPage = await page.save();
|
|
|
|
|
|
|
|
if (insertedPage.uri) {
|
|
|
|
const alias = new Alias({
|
|
|
|
id: insertedPage._id,
|
2020-05-09 05:38:25 +03:00
|
|
|
type: Alias.types.PAGE,
|
2019-01-25 02:23:00 +03:00
|
|
|
}, insertedPage.uri);
|
|
|
|
|
|
|
|
alias.save();
|
|
|
|
}
|
|
|
|
|
|
|
|
return insertedPage;
|
2022-03-05 22:57:23 +04:00
|
|
|
} catch (e) {
|
|
|
|
throw new Error('validationError');
|
2018-10-04 22:08:21 +03:00
|
|
|
}
|
2018-08-17 13:58:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Update page with given id in the database
|
|
|
|
*
|
|
|
|
* @param {string} id - page id
|
2022-03-05 22:57:23 +04:00
|
|
|
* @param {PageData} data - info about page
|
2018-08-17 13:58:44 +03:00
|
|
|
* @returns {Promise<Page>}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static async update(id: string, data: PageData): Promise<Page> {
|
|
|
|
const page = await Page.get(id);
|
2019-01-25 02:23:00 +03:00
|
|
|
const previousUri = page.uri;
|
2018-08-17 13:58:44 +03:00
|
|
|
|
|
|
|
if (!page._id) {
|
|
|
|
throw new Error('Page with given id does not exist');
|
|
|
|
}
|
|
|
|
|
2019-01-25 02:23:00 +03:00
|
|
|
if (data.uri && !data.uri.match(/^[a-z0-9'-]+$/i)) {
|
|
|
|
throw new Error('Uri has unexpected characters');
|
|
|
|
}
|
|
|
|
|
2018-08-17 13:58:44 +03:00
|
|
|
page.data = data;
|
2019-01-25 02:23:00 +03:00
|
|
|
const updatedPage = await page.save();
|
|
|
|
|
|
|
|
if (updatedPage.uri !== previousUri) {
|
|
|
|
if (updatedPage.uri) {
|
|
|
|
const alias = new Alias({
|
|
|
|
id: updatedPage._id,
|
2020-05-09 05:38:25 +03:00
|
|
|
type: Alias.types.PAGE,
|
2019-01-25 02:23:00 +03:00
|
|
|
}, updatedPage.uri);
|
|
|
|
|
|
|
|
alias.save();
|
|
|
|
}
|
|
|
|
|
2022-03-05 22:57:23 +04:00
|
|
|
if (previousUri) {
|
|
|
|
Alias.markAsDeprecated(previousUri);
|
|
|
|
}
|
2019-01-25 02:23:00 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
return updatedPage;
|
2018-08-17 13:58:44 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Remove page with given id from the database
|
|
|
|
*
|
|
|
|
* @param {string} id - page id
|
|
|
|
* @returns {Promise<Page>}
|
|
|
|
*/
|
2022-03-05 22:57:23 +04:00
|
|
|
public static async remove(id: string): Promise<Page> {
|
|
|
|
const page = await Page.get(id);
|
2018-08-17 13:58:44 +03:00
|
|
|
|
|
|
|
if (!page._id) {
|
|
|
|
throw new Error('Page with given id does not exist');
|
|
|
|
}
|
|
|
|
|
2022-03-05 22:57:23 +04:00
|
|
|
if (page.uri) {
|
|
|
|
const alias = await Alias.get(page.uri);
|
2019-01-25 06:19:37 +03:00
|
|
|
|
2022-03-05 22:57:23 +04:00
|
|
|
await alias.destroy();
|
|
|
|
}
|
2019-01-25 06:19:37 +03:00
|
|
|
|
2018-08-17 13:58:44 +03:00
|
|
|
return page.destroy();
|
|
|
|
}
|
2022-03-05 22:57:23 +04:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Check PageData object for required fields
|
|
|
|
*
|
|
|
|
* @param {PageData} data - info about page
|
|
|
|
* @throws {Error} - validation error
|
|
|
|
*/
|
|
|
|
private static validate(data: PageData): void {
|
|
|
|
const allRequiredFields = Pages.REQUIRED_FIELDS.every(field => typeof data[field] !== 'undefined');
|
|
|
|
|
|
|
|
if (!allRequiredFields) {
|
|
|
|
throw new Error('Some of required fields is missed');
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasBlocks = data.body && data.body.blocks && Array.isArray(data.body.blocks) && data.body.blocks.length > 0;
|
|
|
|
|
|
|
|
if (!hasBlocks) {
|
|
|
|
throw new Error('Page body is invalid');
|
|
|
|
}
|
|
|
|
|
|
|
|
const hasHeaderAsFirstBlock = data.body.blocks[0].type === 'header';
|
|
|
|
|
|
|
|
if (!hasHeaderAsFirstBlock) {
|
|
|
|
throw new Error('First page Block must be a Header');
|
|
|
|
}
|
|
|
|
|
|
|
|
const headerIsNotEmpty = data.body.blocks[0].data.text.replace('<br>', '').trim() !== '';
|
|
|
|
|
|
|
|
if (!headerIsNotEmpty) {
|
|
|
|
throw new Error('Please, fill page Header');
|
|
|
|
}
|
|
|
|
}
|
2018-08-17 13:58:44 +03:00
|
|
|
}
|
|
|
|
|
2022-03-05 22:57:23 +04:00
|
|
|
export default Pages;
|