UNPKG

@paperbits/common

Version:
482 lines (371 loc) 18 kB
import * as Objects from "../objects"; import * as Utils from "../utils"; import * as Constants from "../constants"; import { LayoutContract } from "../layouts/layoutContract"; import { IObjectStorage, Query, Operator, Page } from "../persistence"; import { ILayoutService, LayoutMetadata, LayoutLocalizedContract } from "./"; import { Contract } from ".."; import { layoutTemplate } from "./layoutTemplate"; import { ILocaleService } from "../localization"; import { Logger } from "../logging"; const documentsPath = "files"; export interface PermalinkMatchToken { index: number; name: string; value?: string; } export interface PermalinkMatchResult { match: boolean; tokens: PermalinkMatchToken[]; } export class LayoutService implements ILayoutService { protected layoutsPath: string = "layouts"; constructor( private readonly objectStorage: IObjectStorage, private readonly localeService: ILocaleService, private readonly logger: Logger ) { } /** * Copies limited number of metadata properties. */ private copyMetadata(sourceMetadata: LayoutMetadata, targetMetadata: LayoutMetadata): LayoutMetadata { 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.permalinkTemplate = sourceMetadata.permalinkTemplate; return targetMetadata; } private localizedContractToContract(defaultLocale: string, currentLocale: string, requestedLocale: string, localizedPageContract: LayoutLocalizedContract): LayoutContract { const locales = localizedPageContract[Constants.localePrefix]; const metadata = (requestedLocale ? locales[requestedLocale] : locales[currentLocale]) || this.copyMetadata(locales[defaultLocale], {}); if (!metadata) { return null; } const contract: any = { key: localizedPageContract.key, ...metadata }; return contract; } public async getLayoutByKey(key: string, requestedLocale?: string): Promise<LayoutContract> { if (!key) { throw new Error(`Parameter "key" not specified.`); } const layoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(key); if (!layoutContract) { return null; } const defaultLocale = await this.localeService.getDefaultLocaleCode(); const currentLocale = await this.localeService.getCurrentLocaleCode(); return this.localizedContractToContract(defaultLocale, currentLocale, requestedLocale, layoutContract); } private convertPage(localizedPage: Page<LayoutLocalizedContract>, defaultLocale: string, searchLocale: string, requestedLocale: string): Page<LayoutContract> { const resultPage: Page<LayoutContract> = { value: localizedPage.value.map(x => this.localizedContractToContract(defaultLocale, searchLocale, requestedLocale, x)), takeNext: async (): Promise<Page<LayoutContract>> => { 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<LayoutContract>, requestedLocale?: string): Promise<Page<LayoutContract>> { 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<LayoutLocalizedContract>(this.layoutsPath, localizedQuery); return this.convertPage(pageOfResults, defaultLocale, searchLocale, requestedLocale); } catch (error) { throw new Error(`Unable to search layouts: ${error.stack || error.message}`); } } public async deleteLayout(layout: LayoutContract): Promise<void> { if (!layout) { throw new Error(`Parameter "layout" not specified.`); } const localizedLayoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layout.key); if (localizedLayoutContract.locales) { const contentKeys = Object.values(localizedLayoutContract.locales).map(x => x.contentKey); for (const contentKey of contentKeys) { await this.objectStorage.deleteObject(contentKey); } } await this.objectStorage.deleteObject(layout.key); this.logger.trackEvent("LayoutDeleted", { key: layout.key }); } public async createLayout(title: string, description: string, permalinkTemplate: string): Promise<LayoutContract> { const locale = await this.localeService.getDefaultLocaleCode(); const identifier = Utils.guid(); const layoutKey = `${this.layoutsPath}/${identifier}`; const contentKey = `${documentsPath}/${identifier}`; const localizedLayout: LayoutLocalizedContract = { key: layoutKey, locales: { [locale]: { title: title, description: description, permalinkTemplate: permalinkTemplate, contentKey: contentKey } } }; await this.objectStorage.addObject<LayoutLocalizedContract>(layoutKey, localizedLayout); await this.objectStorage.addObject<Contract>(contentKey, layoutTemplate); this.logger.trackEvent("LayoutAdded", { key: layoutKey, contentKey: contentKey }); const layoutContent: LayoutContract = { key: layoutKey, title: title, description: description, permalinkTemplate: permalinkTemplate, contentKey: contentKey }; return layoutContent; } public async updateLayout(layout: LayoutContract, requestedLocale?: string): Promise<void> { if (!layout) { throw new Error(`Parameter "layout" not specified.`); } if (!requestedLocale) { requestedLocale = await this.localeService.getCurrentLocaleCode(); } const layoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layout.key); if (!layoutContract) { throw new Error(`Could not update layout. Layout with key "${layout.key}" doesn't exist.`); } const existingLocaleMetadata = layoutContract.locales[requestedLocale] || <LayoutMetadata>{}; layoutContract.locales[requestedLocale] = this.copyMetadata(layout, existingLocaleMetadata); await this.objectStorage.updateObject<LayoutLocalizedContract>(layout.key, layoutContract); } public async getLayoutByPermalinkTemplate(permalinkTemplate: string): Promise<LayoutContract> { if (!permalinkTemplate) { throw new Error(`Parameter "permalinkTemplate" not specified.`); } const defaultLocale = await this.localeService.getDefaultLocaleCode(); const query = Query .from<LayoutContract>() .where(`locales/${defaultLocale}/permalinkTemplate`, Operator.equals, permalinkTemplate); const pageOfObjects = await this.objectStorage.searchObjects<LayoutContract>(this.layoutsPath, query); const result = pageOfObjects.value; const layouts = Object.keys(result).map(key => result[key]); return layouts.length > 0 ? layouts[0] : null; } private sort(patterns: string[]): string[] { const result = []; function compare(a, b) { if (a.score < b.score) { return 1; } if (a.score > b.score) { return -1; } return 0; } for (const pattern of patterns) { const segments = pattern.split("/").filter(x => !!x); let score = 0; for (let i = 0; i < segments.length; i++) { const segment = segments[i]; let weight; if (segment.startsWith("{")) { // variable weight = 2; } else if (segment === "*") { // wildcard weight = 1; } else { // constant weight = 3; } score += weight / (i + 1); } result.push({ score: score, pattern: pattern }); } return result.sort(compare).map(x => x.pattern); } private matchPermalink(permalink: string, template: string, locale?: string): PermalinkMatchResult { if (!template) { console.warn(`A layout with empty permalink template will be skipped.`); return { match: false, tokens: [] }; } if (locale) { const localePrefix = `/${locale}/`; if (permalink.startsWith(localePrefix)) { permalink = permalink.replace(localePrefix, "/"); } } const tokens: PermalinkMatchToken[] = []; const permalinkSegments: string[] = permalink.split("/"); const templateSegments: string[] = template.split("/"); if (permalinkSegments.length !== templateSegments.length && template.indexOf("*") === -1) { return { match: false, tokens: tokens }; } for (let i = 0; i < templateSegments.length; i++) { const permalinkSegment: string = permalinkSegments[i]; const templateSegment: string = templateSegments[i]; if (templateSegment === "*") { // wildcard if (permalinkSegment !== "" && permalinkSegment !== undefined) { return { match: true, tokens: tokens }; } else { return { match: false, tokens: [] }; } } else if (templateSegment.startsWith("{")) { // variable tokens.push({ index: i, name: templateSegment.replace(/{|}/g, "") }); } else if (permalinkSegment !== templateSegment) { // constant return { match: false, tokens: [] }; } } return { match: true, tokens: tokens }; } public async getLayoutByPermalink(permalink: string, requestedLocale?: string): Promise<LayoutContract> { if (!permalink) { return null; } const defaultLocale = await this.localeService.getDefaultLocaleCode(); const currentLocale = await this.localeService.getCurrentLocaleCode(); const query = Query .from<LayoutContract>(); const pageOfObjects = await this.objectStorage.searchObjects<LayoutLocalizedContract>(this.layoutsPath, query); const result = pageOfObjects.value; const layouts = Object.keys(result).map(key => result[key]); if (layouts && layouts.length) { let permalinkTemplates = layouts .map(x => x.locales[defaultLocale]?.permalinkTemplate) // We use permalinkTemplate from default locale only (for now). .filter(permalinkTemplate => !!permalinkTemplate); permalinkTemplates = this.sort(permalinkTemplates); const matchingTemplate = permalinkTemplates.find(template => { return this.matchPermalink(permalink, template, requestedLocale || currentLocale).match; }); const matchingLayout = layouts.find(x => x.locales[defaultLocale].permalinkTemplate === (matchingTemplate || "/")); if (!matchingLayout) { return null; } return this.localizedContractToContract(defaultLocale, currentLocale, requestedLocale, matchingLayout); } else { return null; } } public async getLayoutContent(layoutKey: string, requestedLocale?: string): Promise<Contract> { if (!layoutKey) { throw new Error(`Parameter "layoutKey" not specified.`); } if (!requestedLocale) { requestedLocale = await this.localeService.getCurrentLocaleCode(); } const defaultLocale = await this.localeService.getDefaultLocaleCode(); const localizedLayoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layoutKey); let layoutMetadata = localizedLayoutContract.locales[requestedLocale]; if (!layoutMetadata) { layoutMetadata = localizedLayoutContract.locales[defaultLocale]; } let layoutContent; if (layoutMetadata.contentKey) { layoutContent = await this.objectStorage.getObject<Contract>(layoutMetadata.contentKey); } else { const layoutDefaultLocaleMetadata = localizedLayoutContract.locales[defaultLocale]; layoutContent = await this.objectStorage.getObject<Contract>(layoutDefaultLocaleMetadata.contentKey); } layoutContent.type = "layout"; return layoutContent; } public async updateLayoutContent(layoutKey: string, content: Contract, requestedLocale?: string, changeDescription?: string): Promise<void> { if (!layoutKey) { throw new Error(`Parameter "layoutKey" not specified.`); } if (!content) { throw new Error(`Parameter "content" not specified.`); } const localizedLayoutContract = await this.objectStorage.getObject<LayoutLocalizedContract>(layoutKey); if (!localizedLayoutContract) { throw new Error(`Layout with key "${layoutKey}" not found.`); } if (!requestedLocale) { requestedLocale = await this.localeService.getCurrentLocaleCode(); } let layoutMetadata = localizedLayoutContract.locales[requestedLocale]; if (!layoutMetadata) { const defaultLocale = await this.localeService.getDefaultLocaleCode(); const defaultLayoutMetadata = localizedLayoutContract.locales[defaultLocale]; const identifier = Utils.guid(); layoutMetadata = this.copyMetadata(defaultLayoutMetadata, { contentKey: `${documentsPath}/${identifier}` }); localizedLayoutContract.locales[requestedLocale] = layoutMetadata; await this.objectStorage.updateObject(layoutKey, localizedLayoutContract); } else if (!layoutMetadata.contentKey) { const identifier = Utils.guid(); layoutMetadata.contentKey = `${documentsPath}/${identifier}`; await this.objectStorage.updateObject(layoutKey, layoutMetadata); } await this.objectStorage.updateObject(layoutMetadata.contentKey, content, changeDescription); } public async copyLayout(key: string): Promise<LayoutContract> { const originalLayout = await this.objectStorage.getObject<LayoutLocalizedContract>(key); const identifier = Utils.guid(); const targetKey = `${this.layoutsPath}/${identifier}`; const targetLayout: LayoutLocalizedContract = { key: targetKey, locales: {} }; const sourceLocales = Object.keys(originalLayout.locales); for (const locale of sourceLocales) { const sourceMetadata = originalLayout.locales[locale]; const sourceContentKey = sourceMetadata.contentKey; const sourceLayoutContent = await this.objectStorage.getObject<Contract>(sourceContentKey); const targetIdentifier = Utils.guid(); const targetContentKey = `${documentsPath}/${targetIdentifier}`; const targetMetadata: LayoutMetadata = { title: sourceMetadata.title + " (copy)", description: sourceMetadata.description, contentKey: targetContentKey, permalinkTemplate: null }; targetLayout.locales[locale] = targetMetadata; const targetLayoutContent = Objects.clone<Contract>(sourceLayoutContent); targetLayoutContent["key"] = targetContentKey; await this.objectStorage.addObject<Contract>(targetContentKey, targetLayoutContent); this.logger.trackEvent("LayoutCopied", { key: targetKey, sourceLayoutKey: key }); } await this.objectStorage.addObject<LayoutLocalizedContract>(targetKey, targetLayout); return this.getLayoutByKey(targetKey); } }