@paperbits/common
Version:
Paperbits common components.
381 lines (293 loc) • 14.9 kB
text/typescript
import * as Utils from "../utils";
import * as Objects from "../objects";
import * as Constants from "../constants";
import { PageContract, PageMetadata, PageLocalizedContract, IPageService } from ".";
import { IObjectStorage, Operator, Query, Page } from "../persistence";
import { IBlockService } from "../blocks";
import { Contract } from "../contract";
import { ILocaleService } from "../localization";
import { Logger } from "../logging";
const documentsPath = "files";
const templateBlockKey = "blocks/new-page-template";
export class PageService implements IPageService {
protected pagesPath: string = "pages";
constructor(
private readonly objectStorage: IObjectStorage,
private readonly blockService: IBlockService,
private readonly localeService: ILocaleService,
private readonly logger: Logger
) {
}
/**
* Copies limited number of metadata properties.
*/
private copyMetadata(sourceMetadata: PageMetadata, targetMetadata: PageMetadata): PageMetadata {
if (!sourceMetadata) {
throw new Error(`Parameter "sourceMetadata" not specified.`);
}
if (!targetMetadata) {
throw new Error(`Parameter "targetMetadata" not specified.`);
}
targetMetadata.title = sourceMetadata.title;
targetMetadata.description = sourceMetadata.description;
targetMetadata.keywords = sourceMetadata.keywords;
targetMetadata.permalink = sourceMetadata.permalink;
targetMetadata.jsonLd = sourceMetadata.jsonLd;
targetMetadata.socialShareData = sourceMetadata.socialShareData;
return targetMetadata;
}
private syncLocalePermalinks(defaultLocaleCode: string, page: PageLocalizedContract): void {
const defaultLocale = page.locales[defaultLocaleCode];
if (!defaultLocale) {
throw new Error(`Page contract doesn't contain the default locale.`);
}
Object.keys(page.locales).forEach(locale => {
if (locale === defaultLocaleCode) {
return; // skipping update for default locale
}
page.locales[locale].permalink = defaultLocale.permalink;
});
}
private localizedContractToContract(defaultLocale: string, currentLocale: string, requestedLocale: string, localizedPageContract: PageLocalizedContract): PageContract {
const locales = localizedPageContract[Constants.localePrefix];
const pageMetadata = (requestedLocale
? locales[requestedLocale]
: locales[currentLocale])
|| this.copyMetadata(locales[defaultLocale], {});
if (!pageMetadata) {
return null;
}
const pageContract: any = {
key: localizedPageContract.key,
...pageMetadata
};
return pageContract;
}
public async getPageByPermalink(permalink: string, requestedLocale: string = null): Promise<PageContract> {
if (!permalink) {
throw new Error(`Parameter "permalink" not specified.`);
}
const defaultLocale = await this.localeService.getDefaultLocaleCode();
const currentLocale = await this.localeService.getCurrentLocaleCode();
const permalinkProperty = `${Constants.localePrefix}/${defaultLocale}/permalink`; // We use permalink from default locale only (for now).
const query = Query
.from<PageContract>()
.where(permalinkProperty, Operator.equals, permalink);
try {
const pageOfObjects = await this.objectStorage.searchObjects<PageLocalizedContract>(this.pagesPath, query);
const result = pageOfObjects.value;
if (!result) {
return null;
}
const pages: any[] = Object.values(result);
if (pages.length === 0) {
return null;
}
const firstPage = pages[0];
return this.localizedContractToContract(defaultLocale, currentLocale, requestedLocale, firstPage);
}
catch (error) {
throw new Error(`Unable to search pages: ${error.stack || error.message}`);
}
}
public async getPageByKey(key: string, requestedLocale?: string): Promise<PageContract> {
if (!key) {
throw new Error(`Parameter "key" not specified.`);
}
const pageContract = await this.objectStorage.getObject<PageLocalizedContract>(key);
if (!pageContract) {
return null;
}
const defaultLocale = await this.localeService.getDefaultLocaleCode();
const currentLocale = await this.localeService.getCurrentLocaleCode();
return this.localizedContractToContract(defaultLocale, currentLocale, requestedLocale, pageContract);
}
private convertPage(localizedPage: Page<PageLocalizedContract>, defaultLocale: string, searchLocale: string, requestedLocale: string): Page<PageContract> {
const resultPage: Page<PageContract> = {
value: localizedPage.value.map(x => this.localizedContractToContract(defaultLocale, searchLocale, requestedLocale, x)),
takeNext: async (): Promise<Page<PageContract>> => {
const nextLocalizedPage = await localizedPage.takeNext();
return this.convertPage(nextLocalizedPage, defaultLocale, searchLocale, requestedLocale);
}
};
if (!localizedPage.takeNext) {
resultPage.takeNext = null;
}
return resultPage;
}
public async search(query: Query<PageContract>, requestedLocale?: string): Promise<Page<PageContract>> {
if (!query) {
throw new Error(`Parameter "query" not specified.`);
}
const defaultLocale = await this.localeService.getDefaultLocaleCode();
const currentLocale = await this.localeService.getCurrentLocaleCode();
const searchLocale = requestedLocale || currentLocale;
const localizedQuery = Utils.localizeQuery(query, searchLocale);
try {
const pageOfResults = await this.objectStorage.searchObjects<PageLocalizedContract>(this.pagesPath, localizedQuery);
return this.convertPage(pageOfResults, defaultLocale, searchLocale, requestedLocale);
}
catch (error) {
throw new Error(`Unable to search pages: ${error.stack || error.message}`);
}
}
public async deletePage(page: PageContract): Promise<void> {
if (!page) {
throw new Error(`Parameter "page" not specified.`);
}
try {
const localizedPageContract = await this.objectStorage.getObject<PageLocalizedContract>(page.key);
if (localizedPageContract.locales) {
const contentKeys = Object
.values(localizedPageContract.locales)
.map(locale => locale.contentKey)
.filter(locale => !!locale);
for (const contentKey of contentKeys) {
await this.objectStorage.deleteObject(contentKey);
}
}
await this.objectStorage.deleteObject(page.key);
this.logger.trackEvent("PageDeleted", { key: page.key });
}
catch (error) {
throw new Error(`Unable to delete page ${page.title}: ${error.stack || error.message}`);
}
}
public async createPage(permalink: string, title: string, description: string, keywords: string): Promise<PageContract> {
const locale = await this.localeService.getDefaultLocaleCode();
const identifier = Utils.guid();
const pageKey = `${this.pagesPath}/${identifier}`;
const contentKey = `${documentsPath}/${identifier}`;
const localizedPage: PageLocalizedContract = {
key: pageKey,
locales: {
[locale]: {
title: title,
description: description,
keywords: keywords,
permalink: permalink,
contentKey: contentKey
}
}
};
await this.objectStorage.addObject<PageLocalizedContract>(pageKey, localizedPage);
const template = await this.blockService.getBlockContent(templateBlockKey);
template["key"] = contentKey; // rewriting own key
await this.objectStorage.addObject<Contract>(contentKey, template);
this.logger.trackEvent("PageAdded", { key: pageKey, contentKey: contentKey });
const pageContent: PageContract = {
key: pageKey,
title: title,
description: description,
keywords: keywords,
permalink: permalink,
contentKey: contentKey
};
return pageContent;
}
public async copyPage(key: string): Promise<PageContract> {
const originalPage = await this.objectStorage.getObject<PageLocalizedContract>(key);
const identifier = Utils.guid();
const targetKey = `${this.pagesPath}/${identifier}`;
const targetPage: PageLocalizedContract = {
key: targetKey,
locales: {}
};
const sourceLocales = Object.keys(originalPage.locales);
for (const locale of sourceLocales) {
const sourceMetadata = originalPage.locales[locale];
const sourceContentKey = sourceMetadata.contentKey;
const sourcePageContent = await this.objectStorage.getObject<Contract>(sourceContentKey);
const targetIdentifier = Utils.guid();
const targetContentKey = `${documentsPath}/${targetIdentifier}`;
const targetMetadata: PageMetadata = {
title: sourceMetadata.title + " (copy)",
description: sourceMetadata.description,
keywords: sourceMetadata.keywords,
contentKey: targetContentKey,
permalink: sourceMetadata.permalink + "-copy",
jsonLd: sourceMetadata.jsonLd,
socialShareData: sourceMetadata.socialShareData
};
targetPage.locales[locale] = targetMetadata;
const targetPageContent = Objects.clone<Contract>(sourcePageContent);
targetPageContent["key"] = targetContentKey;
await this.objectStorage.addObject<Contract>(targetContentKey, targetPageContent);
this.logger.trackEvent("PageCopied", { key: targetKey, sourcePageKey: key });
}
await this.objectStorage.addObject<PageLocalizedContract>(targetKey, targetPage);
return this.getPageByKey(targetKey);
}
public async updatePage(page: PageContract, requestedLocale?: string): Promise<void> {
if (!page) {
throw new Error(`Parameter "page" not specified.`);
}
if (!requestedLocale) {
requestedLocale = await this.localeService.getCurrentLocaleCode();
}
const pageContract = await this.objectStorage.getObject<PageLocalizedContract>(page.key);
if (!pageContract) {
throw new Error(`Could not update page. Page with key "${page.key}" doesn't exist.`);
}
const existingLocaleMetadata = pageContract.locales[requestedLocale] || <PageMetadata>{};
pageContract.locales[requestedLocale] = this.copyMetadata(page, existingLocaleMetadata);
const defaultLocale = await this.localeService.getDefaultLocaleCode();
this.syncLocalePermalinks(defaultLocale, pageContract);
await this.objectStorage.updateObject<PageLocalizedContract>(page.key, pageContract);
}
public async getPageContent(pageKey: string, requestedLocale?: string): Promise<Contract> {
if (!pageKey) {
throw new Error(`Parameter "pageKey" not specified.`);
}
if (!requestedLocale) {
requestedLocale = await this.localeService.getCurrentLocaleCode();
}
const defaultLocale = await this.localeService.getDefaultLocaleCode();
const localizedPageContract = await this.objectStorage.getObject<PageLocalizedContract>(pageKey);
let pageMetadata = localizedPageContract.locales[requestedLocale];
if (!pageMetadata) {
pageMetadata = localizedPageContract.locales[defaultLocale];
}
let pageContent: Contract;
if (pageMetadata.contentKey) {
pageContent = await this.objectStorage.getObject<Contract>(pageMetadata.contentKey);
}
else {
const pageDefaultLocaleMetadata = localizedPageContract.locales[defaultLocale];
pageContent = await this.objectStorage.getObject<Contract>(pageDefaultLocaleMetadata.contentKey);
}
return pageContent;
}
public async updatePageContent(pageKey: string, content: Contract, requestedLocale?: string, changeDescription?: string): Promise<void> {
if (!pageKey) {
throw new Error(`Parameter "pageKey" not specified.`);
}
if (!content) {
throw new Error(`Parameter "content" not specified.`);
}
const localizedPageContract = await this.objectStorage.getObject<PageLocalizedContract>(pageKey);
if (!localizedPageContract) {
throw new Error(`Page with key "${pageKey}" not found.`);
}
if (!requestedLocale) {
requestedLocale = await this.localeService.getCurrentLocaleCode();
}
let pageMetadata = localizedPageContract.locales[requestedLocale];
if (!pageMetadata) {
const defaultLocale = await this.localeService.getDefaultLocaleCode();
const defaultPageMetadata = localizedPageContract.locales[defaultLocale];
const identifier = Utils.guid();
pageMetadata = this.copyMetadata(defaultPageMetadata, {
contentKey: `${documentsPath}/${identifier}`
});
localizedPageContract.locales[requestedLocale] = pageMetadata;
await this.objectStorage.updateObject(pageKey, localizedPageContract);
}
else if (!pageMetadata.contentKey) {
const identifier = Utils.guid();
pageMetadata.contentKey = `${documentsPath}/${identifier}`;
await this.objectStorage.updateObject(pageKey, pageMetadata);
}
await this.objectStorage.updateObject(pageMetadata.contentKey, content, changeDescription);
}
}