UNPKG

sitecore-jss-rule-engine-nextjs

Version:

363 lines (320 loc) 11 kB
import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql'; import { debug } from '@sitecore-jss/sitecore-jss'; import {getScPersonalizedVariantIds, getScPersonalizedRewrite} from '../../lib/personalizationUtils' //@ts-ignore import { getRuleEngineInstance } from 'sitecore-jss-rule-engine' /** @private */ export const languageError = 'The list of languages cannot be empty'; export const siteError = 'The service needs a site name'; /** * @param {string} siteName to inject into error text * @private */ export function getSiteEmptyError(siteName: string) { return `Site "${siteName}" does not exist or site item tree is missing`; } const languageEmptyError = 'The language must be a non-empty string'; /** * GQL query made dynamic based on schema differences between SXP and XM Cloud * @param {boolean} usesPersonalize flag to detrmine which variation of a query to run * @returns GraphQL query to fetch site paths with */ const defaultQuery = () => /* GraphQL */ ` query SitecorePersonalizeSitemapQuery( $siteName: String! $language: String! $includedPaths: [String] $excludedPaths: [String] $pageSize: Int = 100 $after: String ) { site { siteInfo(site: $siteName) { routes( language: $language includedPaths: $includedPaths excludedPaths: $excludedPaths first: $pageSize after: $after ){ total pageInfo { endCursor hasNext } results { path: routePath route: route{ personalizationRule:field(name:"PersonalizationRules"){ value }, personalizeOnEdge:field(name:"PersonalizeOnEdge"){ value }, isStaticRender:field(name:"IsStaticRender"){ value } } } } } } }`; /** * type for input variables for the site routes query */ interface SiteRouteQueryVariables { /** * Required. The name of the site being queried. */ siteName: string; /** * Required. The language to return routes/pages for. */ language: string; /** * Optional. Only paths starting with these provided prefixes will be returned. */ includedPaths?: string[]; /** * Optional. Paths starting with these provided prefixes will be excluded from returned results. */ excludedPaths?: string[]; /** common variable for all GraphQL queries * it will be used for every type of query to regulate result batch size * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. * @default 100 */ pageSize?: number; } /** * Schema of data returned in response to a "site" query request * @template T The type of objects being requested. */ export interface SiteRouteQueryResult<T> { site: { siteInfo: { routes: { /** * Data needed to paginate the site results */ pageInfo: PageInfo; results: T[]; }; }; }; } /** * The schema of data returned in response to a routes list query request */ export type RouteListQueryResult = { path: string; route?: { personalizationRule: { value: string; }; personalizeOnEdge: { value: string; }; isStaticRender: { value: string; } }; }; /** * Configuration options for @see GraphQLSitemapService instances */ export interface BaseGraphQLSitemapServiceConfig extends Omit<SiteRouteQueryVariables, 'language' | 'siteName'> { /** * Your Graphql endpoint */ endpoint: string; /** * The API key to use for authentication. */ apiKey: string; /** * A flag for whether to include personalized routes in service output - only works on XM Cloud * turned off by default */ includePersonalizedRoutes?: boolean; } /** * Object model of a site page item. */ export type StaticPath = { params: { path: string[]; isStaticRender: boolean; }; locale?: string; }; /** * Service that fetches the list of site pages using Sitecore's GraphQL API. * Used to handle a single site * This list is used for SSG and Export functionality. * @mixes SearchQueryService<PageListQueryResult> */ export abstract class BasePersonalizeGraphQLSitemapService { private _graphQLClient: GraphQLClient; /** * GraphQL client accessible by descendant classes when needed */ protected get graphQLClient() { return this._graphQLClient; } /** * Gets the default query used for fetching the list of site pages */ protected get query(): string { return defaultQuery(); } /** * Creates an instance of graphQL sitemap service with the provided options * @param {GraphQLSitemapServiceConfig} options instance */ constructor(public options: BaseGraphQLSitemapServiceConfig) { this._graphQLClient = this.getGraphQLClient(); } /** * Fetch sitemap which could be used for generation of static pages during `next export`. * The `locale` parameter will be used in the item query, but since i18n is not supported, * the output paths will not include a `language` property. * @param {string} locale which application supports * @returns an array of @see StaticPath objects */ async fetchExportSitemap(locale: string): Promise<StaticPath[]> { const formatPath = (path: string[], _language: string, isStaticRender: boolean) => ({ params: { path, isStaticRender } }); return this.fetchSitemap([locale], formatPath); } /** * Fetch sitemap which could be used for generation of static pages using SSG mode * @param {string[]} locales locales which application supports * @returns an array of @see StaticPath objects */ async fetchSSGSitemap(locales: string[]): Promise<StaticPath[]> { const formatPath = (path: string[], locale: string, isStaticRender: boolean) => ({ params: { path, isStaticRender }, locale }); return this.fetchSitemap(locales, formatPath); } protected async getTranformedPaths( siteName: string, languages: string[], formatStaticPath: (path: string[], language: string, isStaticRender: boolean) => StaticPath ) { const paths = new Array<StaticPath>(); for (const language of languages) { if (language === '') { throw new RangeError(languageEmptyError); } debug.sitemap('fetching sitemap data for %s %s', language, siteName); const results = await this.fetchLanguageSitePaths(language, siteName); const transformedPaths = await this.transformLanguageSitePaths( results, formatStaticPath, language ); paths.push(...transformedPaths); } return paths; } protected async transformLanguageSitePaths( sitePaths: RouteListQueryResult[], formatStaticPath: (path: string[], language: string, isStaticRender: boolean) => StaticPath, language: string ): Promise<StaticPath[]> { const formatPath = (path: string, isStaticRender: boolean) => { return formatStaticPath(path.replace(/^\/|\/$/g, '').split('/'), language, isStaticRender); } const aggregatedPaths: StaticPath[] = []; sitePaths.forEach((item) => { if (!item) return; aggregatedPaths.push(formatPath(item.path, item.route?.isStaticRender?.value == "1")); // check for type safety's sake - personalize may be empty depending on query type if (item.route?.personalizationRule?.value?.length && item.route?.personalizeOnEdge?.value == "1") { const ruleEngineInstance = getRuleEngineInstance(); const ruleEngineContext = ruleEngineInstance.getRuleEngineContext(); const parsedRule = ruleEngineInstance.parseRuleXml(item.route?.personalizationRule?.value, ruleEngineContext); const personalizationVariationIds = getScPersonalizedVariantIds(parsedRule); if(personalizationVariationIds) { aggregatedPaths.push( ...(personalizationVariationIds.map((varId:string) => formatPath(getScPersonalizedRewrite(item.path, varId), item?.route?.isStaticRender?.value == "1") ) || {}) ); } } }); return aggregatedPaths; } protected async fetchLanguageSitePaths( language: string, siteName: string ): Promise<RouteListQueryResult[]> { const args: SiteRouteQueryVariables = { siteName: siteName, language: language, pageSize: this.options.pageSize, includedPaths: this.options.includedPaths, excludedPaths: this.options.excludedPaths, }; let results: RouteListQueryResult[] = []; let hasNext = true; let after = ''; while (hasNext) { const fetchResponse = await this.graphQLClient.request< SiteRouteQueryResult<RouteListQueryResult> >(this.query, { ...args, after, }); if (!fetchResponse?.site?.siteInfo) { throw new RangeError(getSiteEmptyError(siteName)); } else { results = results.concat(fetchResponse.site.siteInfo.routes?.results); hasNext = fetchResponse.site.siteInfo.routes?.pageInfo.hasNext; after = fetchResponse.site.siteInfo.routes?.pageInfo.endCursor; } } return results; } /** * Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default * library for fetching graphql data (@see GraphQLRequestClient). Override this method if you * want to use something else. * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.sitemap, }); } /** * Fetch a flat list of all pages that belong to the specificed site and have a * version in the specified language(s). * @param {string[]} languages Fetch pages that have versions in this language(s). * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. * @returns list of pages * @throws {RangeError} if the list of languages is empty. * @throws {RangeError} if the any of the languages is an empty string. */ protected abstract fetchSitemap( languages: string[], formatStaticPath: (path: string[], language: string, isStaticRender: boolean) => StaticPath ): Promise<StaticPath[]>; }