diff --git a/CHANGELOG.md b/CHANGELOG.md index c88f1a1ea3..1d2845403b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,10 @@ Our versioning strategy is as follows: ### 🎉 New Features & Improvements +* `[templates/nextjs]` `[sitecore-jss-nextjs]` `[sitecore-jss]` ([#1640](https://github.com/Sitecore/jss/pull/1640)) Sitecore Edge Platform and Context support: + * Introducing the _clientFactory_ property. This property can be utilized by GraphQL-based services, the previously used _endpoint_ and _apiKey_ properties are deprecated. The _clientFactory_ serves as the central hub for executing GraphQL requests within the application. + * New SITECORE_EDGE_CONTEXT_ID environment variable has been added. + * The JSS_APP_NAME environment variable has been updated and is now referred to as SITE_NAME. * `[templates/nextjs]` Enable client-only BYOC component imports. Client-only components can be imported through src/byoc/index.client.ts. Hybrid (server render with client hydration) components can be imported through src/byoc/index.hybrid.ts. BYOC scaffold logic is also moved from nextjs-sxa addon into base template ([#1628](https://github.com/Sitecore/jss/pull/1628)[#1636](https://github.com/Sitecore/jss/pull/1636)) * `[templates/nextjs]` Import SitecoreForm component into sample nextjs app ([#1628](https://github.com/Sitecore/jss/pull/1628)) * `[sitecore-jss-nextjs]` (Vercel/Sitecore) Deployment Protection Bypass support for editing integration. ([#1634](https://github.com/Sitecore/jss/pull/1634)) diff --git a/packages/create-sitecore-jss/src/templates/nextjs-multisite/scripts/config/plugins/multisite.ts b/packages/create-sitecore-jss/src/templates/nextjs-multisite/scripts/config/plugins/multisite.ts index fbe10ee36f..a22b2f849b 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-multisite/scripts/config/plugins/multisite.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-multisite/scripts/config/plugins/multisite.ts @@ -1,6 +1,8 @@ import chalk from 'chalk'; -import { ConfigPlugin, JssConfig } from '..'; import { GraphQLSiteInfoService, SiteInfo } from '@sitecore-jss/sitecore-jss-nextjs'; +import { createGraphQLClientFactory } from 'lib/graphql-client-factory/create'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; /** * This plugin will set the "sites" config prop. @@ -13,19 +15,16 @@ class MultisitePlugin implements ConfigPlugin { async exec(config: JssConfig) { let sites: SiteInfo[] = []; - const endpoint = config.graphQLEndpoint; - const apiKey = config.sitecoreApiKey; + const endpoint = config.sitecoreEdgeContextId ? config.sitecoreEdgeUrl : config.graphQLEndpoint; - if (!endpoint || !apiKey) { - console.warn( - chalk.yellow('Skipping site information fetch (missing GraphQL endpoint or API key).') - ); + if (!endpoint) { + console.warn(chalk.yellow('Skipping site information fetch (missing GraphQL endpoint).')); } else { console.log(`Fetching site information from ${endpoint}`); + try { const siteInfoService = new GraphQLSiteInfoService({ - endpoint, - apiKey, + clientFactory: createGraphQLClientFactory(config), }); sites = await siteInfoService.fetchSiteInfo(); } catch (error) { diff --git a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/page-props-factory/plugins/site.ts b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/page-props-factory/plugins/site.ts index 81702d8d0e..e21b029050 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/page-props-factory/plugins/site.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/page-props-factory/plugins/site.ts @@ -19,7 +19,7 @@ class SitePlugin implements Plugin { : context.params.path ?? '/'; // Get site name (from path) - const siteData = getSiteRewriteData(path, config.jssAppName); + const siteData = getSiteRewriteData(path, config.siteName); // Resolve site by name props.site = siteResolver.getByName(siteData.siteName); diff --git a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index 2c258b0e50..e9f900a493 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-multisite/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -8,14 +8,14 @@ import config from 'temp/config'; import { SitemapFetcherPlugin } from '..'; import { GetStaticPathsContext } from 'next'; import { siteResolver } from 'lib/site-resolver'; +import clientFactory from 'lib/graphql-client-factory'; class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { _graphqlSitemapService: MultisiteGraphQLSitemapService; constructor() { this._graphqlSitemapService = new MultisiteGraphQLSitemapService({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, + clientFactory, sites: [...new Set(siteResolver.sites.map((site: SiteInfo) => site.name))], }); } diff --git a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/components/CdpPageView.tsx b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/components/CdpPageView.tsx index 28f7c8d3fc..190c0af1d7 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/components/CdpPageView.tsx +++ b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/components/CdpPageView.tsx @@ -68,7 +68,7 @@ const CdpPageView = (): JSX.Element => { return; } - const siteInfo = siteResolver.getByName(site?.name || config.jssAppName); + const siteInfo = siteResolver.getByName(site?.name || config.siteName); const language = route.itemLanguage || config.defaultLanguage; const scope = process.env.NEXT_PUBLIC_PERSONALIZE_SCOPE; diff --git a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts index 2234ee2901..44387365a1 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/middleware/plugins/personalize.ts @@ -1,7 +1,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { PersonalizeMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/middleware'; import { MiddlewarePlugin } from '..'; -import config from 'temp/config'; +import clientFactory from 'lib/graphql-client-factory'; import { siteResolver } from 'lib/site-resolver'; /** @@ -24,8 +24,7 @@ class PersonalizePlugin implements MiddlewarePlugin { this.personalizeMiddleware = new PersonalizeMiddleware({ // Configuration for your Sitecore Experience Edge endpoint edgeConfig: { - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, + clientFactory, timeout: (process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT && parseInt(process.env.PERSONALIZE_MIDDLEWARE_EDGE_TIMEOUT)) || @@ -52,7 +51,7 @@ class PersonalizePlugin implements MiddlewarePlugin { // Site resolver implementation siteResolver, // Personalize middleware will use PosResolver.resolve(site, language) (same as CdpPageView) by default to get point of sale. - // You can also pass a custom point of sale resolver into middleware to override it like so: + // You can also pass a custom point of sale resolver into middleware to override it like so: // getPointOfSale: (site, language) => { ... } }); } diff --git a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/site-resolver/plugins/default.ts b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/site-resolver/plugins/default.ts index 6fcb0af050..5244f8e268 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/site-resolver/plugins/default.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-personalize/src/lib/site-resolver/plugins/default.ts @@ -13,7 +13,7 @@ class DefaultPlugin implements SiteResolverPlugin { exec(sites: SiteInfo[]): SiteInfo[] { // Add default/configured site sites.unshift({ - name: config.jssAppName, + name: config.siteName, language: config.defaultLanguage, hostName: '*', pointOfSale, diff --git a/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/config/plugins/disconnected.ts b/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/config/plugins/disconnected.ts index f540878906..4946f0a8b6 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/config/plugins/disconnected.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/config/plugins/disconnected.ts @@ -1,7 +1,8 @@ import 'dotenv/config'; import chalk from 'chalk'; import { constants } from '@sitecore-jss/sitecore-jss-nextjs'; -import { ConfigPlugin, JssConfig } from '..'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; /** * This plugin will override the "sitecoreApiHost" config prop diff --git a/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/disconnected-mode-proxy.ts b/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/disconnected-mode-proxy.ts index 240b8b2ca2..3f509d5301 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/disconnected-mode-proxy.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-styleguide/scripts/disconnected-mode-proxy.ts @@ -18,7 +18,7 @@ const touchToReloadFilePath = 'src/temp/config.js'; const serverOptions = { appRoot: path.join(__dirname, '..'), - appName: config.jssAppName, + appName: config.siteName, // Prevent require of ./sitecore/definitions/config.js, because ts-node is running requireArg: null, watchPaths: ['./data'], diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/scripts/config/plugins/sxa.ts b/packages/create-sitecore-jss/src/templates/nextjs-sxa/scripts/config/plugins/sxa.ts index d2fd261159..a1c78d89af 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-sxa/scripts/config/plugins/sxa.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/scripts/config/plugins/sxa.ts @@ -1,7 +1,8 @@ -import { ConfigPlugin, JssConfig } from '..'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; /** - * This plugin will set configuration specific for SXA. + * This plugin will set configuration specific for SXA. */ class SXAPlugin implements ConfigPlugin { // should come before fallback diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/middleware/plugins/redirects.ts b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/middleware/plugins/redirects.ts index c1333ea780..58df7acf58 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/middleware/plugins/redirects.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/lib/middleware/plugins/redirects.ts @@ -1,8 +1,8 @@ import { NextRequest, NextResponse } from 'next/server'; import { RedirectsMiddleware } from '@sitecore-jss/sitecore-jss-nextjs/middleware'; -import config from 'temp/config'; import { MiddlewarePlugin } from '..'; import { siteResolver } from 'lib/site-resolver'; +import clientFactory from 'lib/graphql-client-factory'; class RedirectsPlugin implements MiddlewarePlugin { private redirectsMiddleware: RedirectsMiddleware; @@ -10,8 +10,8 @@ class RedirectsPlugin implements MiddlewarePlugin { constructor() { this.redirectsMiddleware = new RedirectsMiddleware({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, + // Client factory implementation + clientFactory, // These are all the locales you support in your application. // These should match those in your next.config.js (i18n.locales). locales: ['en'], diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/404.tsx b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/404.tsx index aef20616ea..cf1fef2292 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/404.tsx +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/404.tsx @@ -10,6 +10,7 @@ import { componentBuilder } from 'temp/componentBuilder'; import Layout from 'src/Layout'; import { GetStaticProps } from 'next'; import { siteResolver } from 'lib/site-resolver'; +import clientFactory from 'lib/graphql-client-factory'; const Custom404 = (props: SitecorePageProps): JSX.Element => { if (!(props && props.layoutData)) { @@ -27,10 +28,9 @@ const Custom404 = (props: SitecorePageProps): JSX.Element => { }; export const getStaticProps: GetStaticProps = async (context) => { - const site = siteResolver.getByName(config.jssAppName); + const site = siteResolver.getByName(config.siteName); const errorPagesService = new GraphQLErrorPagesService({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, + clientFactory, siteName: site.name, language: context.locale || config.defaultLanguage, retries: diff --git a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/500.tsx b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/500.tsx index 15d33bc326..dc0ac5adf4 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/500.tsx +++ b/packages/create-sitecore-jss/src/templates/nextjs-sxa/src/pages/500.tsx @@ -10,6 +10,7 @@ import { componentBuilder } from 'temp/componentBuilder'; import { GetStaticProps } from 'next'; import config from 'temp/config'; import { siteResolver } from 'lib/site-resolver'; +import clientFactory from 'lib/graphql-client-factory'; /** * Rendered in case if we have 500 error @@ -43,10 +44,9 @@ const Custom500 = (props: SitecorePageProps): JSX.Element => { }; export const getStaticProps: GetStaticProps = async (context) => { - const site = siteResolver.getByName(config.jssAppName); + const site = siteResolver.getByName(config.siteName); const errorPagesService = new GraphQLErrorPagesService({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, + clientFactory, siteName: site.name, language: context.locale || context.defaultLocale || config.defaultLanguage, retries: diff --git a/packages/create-sitecore-jss/src/templates/nextjs/.env b/packages/create-sitecore-jss/src/templates/nextjs/.env index d4dba6806e..17c41a657f 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/.env +++ b/packages/create-sitecore-jss/src/templates/nextjs/.env @@ -17,6 +17,16 @@ PUBLIC_URL=http://localhost:3000 # We recommend an alphanumeric value of at least 16 characters. JSS_EDITING_SECRET= +# ===== Sitecore Edge Platform ====== + +# Your unified Sitecore Edge Context Id. +SITECORE_EDGE_CONTEXT_ID= + +# ================================ + +# ====== Sitecore Preview / Delivery Edge ====== +# (Sitecore Edge Proxy environment variables should be set empty, otherwise they will be prioritized and applied) + # Your Sitecore API key is needed to build the app. Typically, the API key is # defined in `scjssconfig.json` (as `sitecore.apiKey`). This file may not exist # when building locally (if you've never run `jss setup`), or when building in a @@ -36,8 +46,12 @@ SITECORE_API_HOST= # the resolved Sitecore API hostname + the `graphQLEndpointPath` defined in your `package.json`. GRAPH_QL_ENDPOINT= -# Your JSS app name (also used as the default site name). Overrides 'config.appName' defined in a package.json -JSS_APP_NAME= +# ================================ + +# Your Sitecore site name. +# Uses your `package.json` config `appName` if empty. +# When using the Next.js Multisite add-on, the value of the variable represents the default/configured site. +SITE_NAME= # Your default app language. DEFAULT_LANGUAGE= diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/index.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/index.ts index 0212c23393..683c6df2e1 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/index.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/index.ts @@ -1,18 +1,6 @@ // eslint-disable-next-line @typescript-eslint/no-var-requires const plugins = require('scripts/temp/config-plugins'); - -/** - * JSS configuration object - */ -export interface JssConfig extends Record { - sitecoreApiKey?: string; - sitecoreApiHost?: string; - jssAppName?: string; - graphQLEndpointPath?: string; - defaultLanguage?: string; - graphQLEndpoint?: string; - layoutServiceConfigurationName?: string; -} +import { JssConfig } from 'lib/config'; export interface ConfigPlugin { /** diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/computed.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/computed.ts index 0669af129e..dd38b780a5 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/computed.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/computed.ts @@ -1,4 +1,5 @@ -import { ConfigPlugin, JssConfig } from '..'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; /** * This plugin will set computed config props. diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/fallback.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/fallback.ts index 8385943fc9..ba3b08c00a 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/fallback.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/fallback.ts @@ -1,4 +1,6 @@ -import { ConfigPlugin, JssConfig } from '..'; +import chalk from 'chalk'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; /** * This config will set fallback values for properties that were left empty @@ -9,10 +11,19 @@ class FallbackPlugin implements ConfigPlugin { order = 100; async exec(config: JssConfig) { + if (config.sitecoreApiKey && config.sitecoreEdgeContextId) { + console.log( + chalk.yellow( + "You have configured both 'sitecoreApiKey' and 'sitecoreEdgeContextId' values. The 'sitecoreEdgeContextId' is used instead." + ) + ); + } + return Object.assign({}, config, { defaultLanguage: config.defaultLanguage || 'en', sitecoreApiKey: config.sitecoreApiKey || 'no-api-key-set', layoutServiceConfigurationName: config.layoutServiceConfigurationName || 'default', + sitecoreEdgeUrl: config.sitecoreEdgeUrl || 'https://edge-platform.sitecorecloud.io', }); } } diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/package-json.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/package-json.ts index ce49ee56d5..f411d9d283 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/package-json.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/package-json.ts @@ -1,4 +1,5 @@ -import { ConfigPlugin, JssConfig } from '..'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; import packageConfig from 'package.json'; /** @@ -11,7 +12,7 @@ class PackageJsonPlugin implements ConfigPlugin { if (!packageConfig.config) return config; return Object.assign({}, config, { - jssAppName: config.jssAppName || packageConfig.config.appName, + siteName: config.siteName || packageConfig.config.appName, graphQLEndpointPath: config.graphQLEndpointPath || packageConfig.config.graphQLEndpointPath, defaultLanguage: config.defaultLanguage || packageConfig.config.language, }); diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/scjssconfig.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/scjssconfig.ts index e957f2758b..d1368da595 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/scjssconfig.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/config/plugins/scjssconfig.ts @@ -1,4 +1,5 @@ -import { ConfigPlugin, JssConfig } from '..'; +import { JssConfig } from 'lib/config'; +import { ConfigPlugin } from '..'; /** * This plugin will set config props based on scjssconfig.json. diff --git a/packages/create-sitecore-jss/src/templates/nextjs/scripts/generate-config.ts b/packages/create-sitecore-jss/src/templates/nextjs/scripts/generate-config.ts index e8c18ad937..67c7c5b0f8 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/scripts/generate-config.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/scripts/generate-config.ts @@ -2,7 +2,8 @@ import 'dotenv/config'; import fs from 'fs'; import path from 'path'; import { constantCase } from 'constant-case'; -import { JssConfig, jssConfigFactory } from './config'; +import { JssConfig } from 'lib/config'; +import { jssConfigFactory } from './config'; /* CONFIG GENERATION @@ -13,7 +14,10 @@ import { JssConfig, jssConfigFactory } from './config'; const defaultConfig: JssConfig = { sitecoreApiKey: process.env[`${constantCase('sitecoreApiKey')}`], sitecoreApiHost: process.env[`${constantCase('sitecoreApiHost')}`], - jssAppName: process.env[`${constantCase('jssAppName')}`], + sitecoreEdgeUrl: process.env[`${constantCase('sitecoreEdgeUrl')}`], + sitecoreEdgeContextId: process.env[`${constantCase('sitecoreEdgeContextId')}`], + siteName: + process.env[`${constantCase('siteName')}`] || process.env[`${constantCase('jssAppName')}`], graphQLEndpointPath: process.env[`${constantCase('graphQLEndpointPath')}`], defaultLanguage: process.env[`${constantCase('defaultLanguage')}`], graphQLEndpoint: process.env[`${constantCase('graphQLEndpoint')}`], diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/byoc/index.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/byoc/index.ts index 606450504f..c9bf7377bd 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/byoc/index.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/byoc/index.ts @@ -4,6 +4,15 @@ // Import your client-only components via client-bundle. Nextjs's dynamic() call will ensure they are only rendered client-side import dynamic from 'next/dynamic'; +import * as FEAAS from '@sitecore-feaas/clientside/react'; +import config from 'temp/config'; + +// Set context properties to be available within BYOC components +FEAAS.setContextProperties({ + sitecoreEdgeUrl: config.sitecoreEdgeUrl, + sitecoreEdgeContextId: config.sitecoreEdgeContextId, +}); + const ClientBundle = dynamic(() => import('./index.client'), { ssr: false, }); diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/config.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/config.ts new file mode 100644 index 0000000000..88ec0aaa07 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/config.ts @@ -0,0 +1,14 @@ +/* + * Represents the type of config object available within the generated temp/config.js + */ +export interface JssConfig extends Record { + sitecoreApiKey?: string; + sitecoreApiHost?: string; + sitecoreEdgeUrl?: string; + sitecoreEdgeContextId?: string; + siteName?: string; + graphQLEndpointPath?: string; + defaultLanguage?: string; + graphQLEndpoint?: string; + layoutServiceConfigurationName?: string; +} diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts index 9f187dfecb..963f814194 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/dictionary-service-factory.ts @@ -5,6 +5,7 @@ import { constants, } from '@sitecore-jss/sitecore-jss-nextjs'; import config from 'temp/config'; +import clientFactory from 'lib/graphql-client-factory'; /** * Factory responsible for creating a DictionaryService instance @@ -17,9 +18,8 @@ export class DictionaryServiceFactory { create(siteName: string): DictionaryService { return process.env.FETCH_WITH === constants.FETCH_WITH.GRAPHQL ? new GraphQLDictionaryService({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, siteName, + clientFactory, /* The Dictionary Service needs a root item ID in order to fetch dictionary phrases for the current app. When not provided, the service will attempt to figure out the root item for the current JSS App using GraphQL and app name. diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/graphql-client-factory/create.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/graphql-client-factory/create.ts new file mode 100644 index 0000000000..e2b2918626 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/graphql-client-factory/create.ts @@ -0,0 +1,32 @@ +import { + GraphQLRequestClientFactoryConfig, + getEdgeProxyContentUrl, + GraphQLRequestClient, +} from '@sitecore-jss/sitecore-jss-nextjs'; +import { JssConfig } from 'lib/config'; + +/** + * Creates a new GraphQLRequestClientFactory instance + * @param config jss config + * @returns GraphQLRequestClientFactory instance + */ +export const createGraphQLClientFactory = (config: JssConfig) => { + let clientConfig: GraphQLRequestClientFactoryConfig; + + if (config.sitecoreEdgeContextId) { + clientConfig = { + endpoint: getEdgeProxyContentUrl(config.sitecoreEdgeContextId, config.sitecoreEdgeUrl), + }; + } else if (config.graphQLEndpoint && config.sitecoreApiKey) { + clientConfig = { + endpoint: config.graphQLEndpoint, + apiKey: config.sitecoreApiKey, + }; + } else { + throw new Error( + 'Please configure either your sitecoreEdgeContextId, or your graphQLEndpoint and sitecoreApiKey.' + ); + } + + return GraphQLRequestClient.createClientFactory(clientConfig); +}; diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/graphql-client-factory/index.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/graphql-client-factory/index.ts new file mode 100644 index 0000000000..d500587ef8 --- /dev/null +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/graphql-client-factory/index.ts @@ -0,0 +1,7 @@ +import config from 'temp/config'; +import { createGraphQLClientFactory } from './create'; + +// The GraphQLRequestClientFactory serves as the central hub for executing GraphQL requests within the application + +// Create a new instance on each import call +export default createGraphQLClientFactory(config); diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts index 2c6e49b1d5..c175488bd7 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/layout-service-factory.ts @@ -5,6 +5,7 @@ import { constants, } from '@sitecore-jss/sitecore-jss-nextjs'; import config from 'temp/config'; +import clientFactory from 'lib/graphql-client-factory'; /** * Factory responsible for creating a LayoutService instance @@ -17,9 +18,8 @@ export class LayoutServiceFactory { create(siteName: string): LayoutService { return process.env.FETCH_WITH === constants.FETCH_WITH.GRAPHQL ? new GraphQLLayoutService({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, siteName, + clientFactory, /* GraphQL endpoint may reach its rate limit with the amount of Layout and Dictionary requests it receives and throw a rate limit error. GraphQL Dictionary and Layout Services can handle rate limit errors from server and attempt a retry on requests. diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/site.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/site.ts index 9cc0582c3a..5a0f13299a 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/site.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/page-props-factory/plugins/site.ts @@ -11,7 +11,7 @@ class SitePlugin implements Plugin { if (context.preview) return props; // Resolve site by name - props.site = siteResolver.getByName(config.jssAppName); + props.site = siteResolver.getByName(config.siteName); return props; } diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/site-resolver/plugins/default.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/site-resolver/plugins/default.ts index 45cd4e034c..7f6bb697e6 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/site-resolver/plugins/default.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/site-resolver/plugins/default.ts @@ -6,7 +6,7 @@ class DefaultPlugin implements SiteResolverPlugin { exec(sites: SiteInfo[]): SiteInfo[] { // Add default/configured site sites.unshift({ - name: config.jssAppName, + name: config.siteName, language: config.defaultLanguage, hostName: '*', }); diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index ea50d1c09e..9b41675535 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -6,15 +6,15 @@ import config from 'temp/config'; import { SitemapFetcherPlugin } from '..'; import { GetStaticPathsContext } from 'next'; +import clientFactory from 'lib/graphql-client-factory'; class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { _graphqlSitemapService: GraphQLSitemapService; constructor() { this._graphqlSitemapService = new GraphQLSitemapService({ - endpoint: config.graphQLEndpoint, - apiKey: config.sitecoreApiKey, - siteName: config.jssAppName, + siteName: config.siteName, + clientFactory, }); } diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index 44a5ac7ef1..e753bbee5d 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -8,6 +8,9 @@ export { AxiosDataFetcherConfig, NativeDataFetcher, NativeDataFetcherConfig, + GraphQLRequestClient, + GraphQLRequestClientFactory, + GraphQLRequestClientFactoryConfig, HTMLLink, enableDebug, debug, @@ -69,6 +72,7 @@ export { EDITING_COMPONENT_PLACEHOLDER, EDITING_COMPONENT_ID, } from '@sitecore-jss/sitecore-jss/layout'; +export { getEdgeProxyContentUrl } from '@sitecore-jss/sitecore-jss/graphql'; export { mediaApi } from '@sitecore-jss/sitecore-jss/media'; export { trackingApi, @@ -95,7 +99,6 @@ export { CdpHelper, PosResolver, } from '@sitecore-jss/sitecore-jss/personalize'; -export { GraphQLRequestClient } from '@sitecore-jss/sitecore-jss'; export { ComponentPropsCollection, diff --git a/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts index 089b901f7c..8d8c13ab98 100644 --- a/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/base-graphql-sitemap-service.ts @@ -1,4 +1,9 @@ -import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql'; +import { + GraphQLClient, + GraphQLRequestClient, + GraphQLRequestClientFactory, + PageInfo, +} from '@sitecore-jss/sitecore-jss/graphql'; import { debug } from '@sitecore-jss/sitecore-jss'; import { getPersonalizedRewrite } from '@sitecore-jss/sitecore-jss/personalize'; @@ -128,19 +133,26 @@ export interface BaseGraphQLSitemapServiceConfig extends Omit { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication. + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; /** * A flag for whether to include personalized routes in service output - only works on XM Cloud * turned off by default */ includePersonalizedRoutes?: boolean; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; } /** @@ -314,6 +326,16 @@ export abstract class BaseGraphQLSitemapService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.options.endpoint) { + if (!this.options.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.options.clientFactory({ + debugger: debug.sitemap, + }); + } + return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.sitemap, diff --git a/packages/sitecore-jss/src/graphql-request-client.test.ts b/packages/sitecore-jss/src/graphql-request-client.test.ts index 89d76fddb9..ccb785a0ab 100644 --- a/packages/sitecore-jss/src/graphql-request-client.test.ts +++ b/packages/sitecore-jss/src/graphql-request-client.test.ts @@ -230,4 +230,19 @@ describe('GraphQLRequestClient', () => { expect(error.name).to.equal('AbortError'); }); }); + + describe('createClientFactory', () => { + it('should create a graphql request factory', () => { + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint: 'https://foo.com', + apiKey: 'bar', + }); + + const client = clientFactory({ retries: 5, timeout: 300 }); + + expect(client instanceof GraphQLRequestClient).to.equal(true); + expect(client['retries']).to.equal(5); + expect(client['timeout']).to.equal(300); + }); + }); }); diff --git a/packages/sitecore-jss/src/graphql-request-client.ts b/packages/sitecore-jss/src/graphql-request-client.ts index 3e9dac24d7..80da5232cf 100644 --- a/packages/sitecore-jss/src/graphql-request-client.ts +++ b/packages/sitecore-jss/src/graphql-request-client.ts @@ -42,6 +42,21 @@ export type GraphQLRequestClientConfig = { retries?: number; }; +/** + * A GraphQL Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + * @param config - The configuration object that specifies how the GraphQL client should be set up. + * @returns An instance of a GraphQL Request Client ready to send GraphQL requests. + */ +export type GraphQLRequestClientFactory = ( + config: Omit +) => GraphQLRequestClient; + +/** + * Configuration type for @type GraphQLRequestClientFactory + */ +export type GraphQLRequestClientFactoryConfig = { endpoint: string; apiKey?: string }; + /** * A GraphQL client for Sitecore APIs that uses the 'graphql-request' library. * https://github.com/prisma-labs/graphql-request @@ -79,6 +94,20 @@ export class GraphQLRequestClient implements GraphQLClient { this.debug = clientConfig.debugger || debuggers.http; } + /** + * Factory method for creating a GraphQLRequestClientFactory. + * @param {Object} config - client configuration options. + * @param {string} config.endpoint - endpoint + * @param {string} [config.apiKey] - apikey + */ + static createClientFactory({ + endpoint, + apiKey, + }: GraphQLRequestClientFactoryConfig): GraphQLRequestClientFactory { + return (config: Omit = {}) => + new GraphQLRequestClient(endpoint, { ...config, apiKey }); + } + /** * Execute graphql request * @param {string | DocumentNode} query graphql query diff --git a/packages/sitecore-jss/src/graphql/graphql-edge-proxy.test.ts b/packages/sitecore-jss/src/graphql/graphql-edge-proxy.test.ts new file mode 100644 index 0000000000..3310843e76 --- /dev/null +++ b/packages/sitecore-jss/src/graphql/graphql-edge-proxy.test.ts @@ -0,0 +1,28 @@ +/* eslint-disable no-unused-expressions */ +import { expect } from 'chai'; +import { getEdgeProxyContentUrl } from './graphql-edge-proxy'; + +describe('graphql-edge-proxy', () => { + describe('getEdgeProxyContentUrl', () => { + it('should return url', () => { + const sitecoreEdgeContextId = '0730fc5a-3333-5555-5555-08db6d7ddb49'; + + const url = getEdgeProxyContentUrl(sitecoreEdgeContextId); + + expect(url).to.equal( + 'https://edge-platform.sitecorecloud.io/content/api/graphql/v1?sitecoreContextId=0730fc5a-3333-5555-5555-08db6d7ddb49' + ); + }); + + it('should return url when custom sitecoreEdgeUrl is provided', () => { + const sitecoreEdgeUrl = 'https://test.com'; + const sitecoreEdgeContextId = '0730fc5a-3333-5555-5555-08db6d7ddb49'; + + const url = getEdgeProxyContentUrl(sitecoreEdgeContextId, sitecoreEdgeUrl); + + expect(url).to.equal( + 'https://test.com/content/api/graphql/v1?sitecoreContextId=0730fc5a-3333-5555-5555-08db6d7ddb49' + ); + }); + }); +}); diff --git a/packages/sitecore-jss/src/graphql/graphql-edge-proxy.ts b/packages/sitecore-jss/src/graphql/graphql-edge-proxy.ts new file mode 100644 index 0000000000..4d5d1dbf65 --- /dev/null +++ b/packages/sitecore-jss/src/graphql/graphql-edge-proxy.ts @@ -0,0 +1,10 @@ +/** + * Generates a URL for accessing Sitecore Edge Platform Content using the provided endpoint and context ID. + * @param {string} sitecoreEdgeContextId - The unique context id. + * @param {string} [sitecoreEdgeUrl] - The base endpoint URL for the Edge Platform. Default is https://edge-platform.sitecorecloud.io + * @returns {string} The complete URL for accessing content through the Edge Platform. + */ +export const getEdgeProxyContentUrl = ( + sitecoreEdgeContextId: string, + sitecoreEdgeUrl = 'https://edge-platform.sitecorecloud.io' +) => `${sitecoreEdgeUrl}/content/api/graphql/v1?sitecoreContextId=${sitecoreEdgeContextId}`; diff --git a/packages/sitecore-jss/src/graphql/index.ts b/packages/sitecore-jss/src/graphql/index.ts index cf3f74b6da..9476d2f47d 100644 --- a/packages/sitecore-jss/src/graphql/index.ts +++ b/packages/sitecore-jss/src/graphql/index.ts @@ -3,6 +3,8 @@ export { GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, + GraphQLRequestClientFactory, + GraphQLRequestClientFactoryConfig, } from './../graphql-request-client'; export { SearchQueryResult, @@ -11,3 +13,4 @@ export { SearchQueryService, PageInfo, } from './search-service'; +export { getEdgeProxyContentUrl } from './graphql-edge-proxy'; diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.test.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.test.ts index c7a8b93669..7cd8260b4a 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.test.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.test.ts @@ -42,6 +42,27 @@ describe('GraphQLDictionaryService', () => { expect(result.bar).to.equal('bar'); }); + it('should fetch dictionary phrases using clientFactory', async () => { + nock(endpoint, { reqheaders: { sc_apikey: apiKey } }) + .post('/', /DictionarySearch/gi) + .reply(200, dictionaryQueryResponse); + + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint, + apiKey, + }); + + const service = new GraphQLDictionaryService({ + siteName, + rootItemId, + cacheEnabled: false, + clientFactory, + }); + const result = await service.fetchDictionaryData('en'); + expect(result.foo).to.equal('foo'); + expect(result.bar).to.equal('bar'); + }); + it('should attempt to fetch the rootItemId, if rootItemId not provided', async () => { nock(endpoint) .post('/', /AppRootQuery/) diff --git a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts index e378bdfcc5..88a865d26c 100644 --- a/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts +++ b/packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts @@ -2,6 +2,7 @@ import { GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, + GraphQLRequestClientFactory, } from '../graphql-request-client'; import { SitecoreTemplateId } from '../constants'; import { DictionaryPhrases, DictionaryServiceBase } from './dictionary-service'; @@ -58,13 +59,21 @@ export interface GraphQLDictionaryServiceConfig Pick { /** * The URL of the graphQL endpoint. + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication. + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; + + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; /** * Optional. The template ID to use when searching for dictionary entries. @@ -161,6 +170,17 @@ export class GraphQLDictionaryService extends DictionaryServiceBase { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.options.endpoint) { + if (!this.options.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.options.clientFactory({ + debugger: debug.dictionary, + retries: this.options.retries, + }); + } + return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.dictionary, diff --git a/packages/sitecore-jss/src/index.ts b/packages/sitecore-jss/src/index.ts index c5997bdb56..bf1a5a8791 100644 --- a/packages/sitecore-jss/src/index.ts +++ b/packages/sitecore-jss/src/index.ts @@ -8,6 +8,8 @@ export { GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig, + GraphQLRequestClientFactory, + GraphQLRequestClientFactoryConfig, } from './graphql-request-client'; export { AxiosDataFetcher, AxiosDataFetcherConfig } from './axios-fetcher'; export { AxiosResponse } from 'axios'; diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.test.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.test.ts index a6a0861fc1..a85623f0fb 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.test.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.test.ts @@ -2,6 +2,7 @@ import { expect, use } from 'chai'; import spies from 'chai-spies'; import nock from 'nock'; import { GraphQLLayoutService } from './graphql-layout-service'; +import { GraphQLRequestClient, GraphQLRequestClientFactory } from '../graphql-request-client'; use(spies); @@ -67,6 +68,65 @@ describe('GraphQLLayoutService', () => { }); }); + it('should fetch layout data using clientFactory', async () => { + nock('https://bar.com', { + reqheaders: { + sc_apikey: apiKey, + }, + }) + .post('/graphql', (body) => { + return ( + body.query.replace(/\n|\s/g, '') === + 'query{layout(site:"supersite",routePath:"/styleguide",language:"da-DK"){item{rendered}}}' + ); + }) + .reply(200, { + data: { + layout: { + item: { + rendered: { + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }, + }, + }, + }, + }); + + const clientFactory: GraphQLRequestClientFactory = GraphQLRequestClient.createClientFactory({ + apiKey, + endpoint: 'https://bar.com/graphql', + }); + + const service = new GraphQLLayoutService({ + siteName: 'supersite', + clientFactory, + }); + + const data = await service.fetchLayoutData('/styleguide', 'da-DK'); + + expect(data).to.deep.equal({ + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }); + }); + it('should fetch layout data if locale is not provided', async () => { nock('http://sctest', { reqheaders: { diff --git a/packages/sitecore-jss/src/layout/graphql-layout-service.ts b/packages/sitecore-jss/src/layout/graphql-layout-service.ts index 5f7c59b9a2..7ad50aa41a 100644 --- a/packages/sitecore-jss/src/layout/graphql-layout-service.ts +++ b/packages/sitecore-jss/src/layout/graphql-layout-service.ts @@ -2,6 +2,7 @@ import { LayoutServiceBase } from './layout-service'; import { LayoutServiceData } from './models'; import { GraphQLClient, + GraphQLRequestClientFactory, GraphQLRequestClient, GraphQLRequestClientConfig, } from '../graphql-request-client'; @@ -10,16 +11,23 @@ import debug from '../debug'; export interface GraphQLLayoutServiceConfig extends Pick { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The JSS application name */ siteName: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; /** * Override default layout query * @param {string} siteName @@ -85,6 +93,17 @@ export class GraphQLLayoutService extends LayoutServiceBase { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.serviceConfig.endpoint) { + if (!this.serviceConfig.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.serviceConfig.clientFactory({ + debugger: debug.layout, + retries: this.serviceConfig.retries, + }); + } + return new GraphQLRequestClient(this.serviceConfig.endpoint, { apiKey: this.serviceConfig.apiKey, debugger: debug.layout, diff --git a/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts b/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts index e16b6b95c8..c55b2fd8dc 100644 --- a/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts +++ b/packages/sitecore-jss/src/personalize/graphql-personalize-service.test.ts @@ -3,6 +3,7 @@ import { expect, use } from 'chai'; import spies from 'chai-spies'; import nock from 'nock'; import { GraphQLPersonalizeService } from './graphql-personalize-service'; +import { GraphQLRequestClient } from '../graphql-request-client'; use(spies); @@ -75,6 +76,24 @@ describe('GraphQLPersonalizeService', () => { }); }); + it('should return personalize info for a route using clientFactory', async () => { + mockNonEmptyResponse(); + + const clientFactory = GraphQLRequestClient.createClientFactory(config); + + const service = new GraphQLPersonalizeService({ clientFactory }); + const personalizeData = await service.getPersonalizeInfo( + '/sitecore/content/home', + 'en', + siteName + ); + + expect(personalizeData).to.deep.equal({ + contentId: `embedded_${id}_en`.toLowerCase(), + variantIds, + }); + }); + it('should return personalize info for a route when scope is provided', async () => { mockNonEmptyResponse(); diff --git a/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts b/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts index 9e10163e2b..cc4f99d485 100644 --- a/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts +++ b/packages/sitecore-jss/src/personalize/graphql-personalize-service.ts @@ -1,4 +1,8 @@ -import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client'; +import { + GraphQLClient, + GraphQLRequestClient, + GraphQLRequestClientFactory, +} from '../graphql-request-client'; import debug from '../debug'; import { isTimeoutError } from '../utils'; import { CdpHelper } from './utils'; @@ -7,12 +11,14 @@ import { CacheClient, CacheOptions, MemoryCacheClient } from '../cache-client'; export type GraphQLPersonalizeServiceConfig = CacheOptions & { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; /** * Timeout (ms) for the Personalize request. Default is 400. */ @@ -25,6 +31,11 @@ export type GraphQLPersonalizeServiceConfig = CacheOptions & { * Override fetch method. Uses 'GraphQLRequestClient' default otherwise. */ fetch?: typeof fetch; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; }; /** @@ -138,6 +149,18 @@ export class GraphQLPersonalizeService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.config.endpoint) { + if (!this.config.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.config.clientFactory({ + debugger: debug.personalize, + fetch: this.config.fetch, + timeout: this.config.timeout, + }); + } + return new GraphQLRequestClient(this.config.endpoint, { apiKey: this.config.apiKey, debugger: debug.personalize, diff --git a/packages/sitecore-jss/src/site/graphql-error-pages-service.test.ts b/packages/sitecore-jss/src/site/graphql-error-pages-service.test.ts index d92a7523fd..c8c79c1498 100644 --- a/packages/sitecore-jss/src/site/graphql-error-pages-service.test.ts +++ b/packages/sitecore-jss/src/site/graphql-error-pages-service.test.ts @@ -3,6 +3,7 @@ import nock from 'nock'; import { ErrorPages, GraphQLErrorPagesService } from './graphql-error-pages-service'; import { siteNameError } from '../constants'; import { LayoutServiceData } from '../layout'; +import { GraphQLRequestClient } from '../graphql-request-client'; const errorQueryResultNull = { site: { @@ -80,6 +81,27 @@ describe('GraphQLErrorPagesService', () => { return expect(nock.isDone()).to.be.true; }); + it('should fetch error pages using clientFactory', async () => { + mockErrorPagesRequest(mockErrorPages); + + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint, + apiKey, + }); + + const service = new GraphQLErrorPagesService({ + siteName, + language, + clientFactory, + }); + + const errorPages = await service.fetchErrorPages(); + + expect(errorPages).to.deep.equal(mockErrorPages); + + return expect(nock.isDone()).to.be.true; + }); + it('should get null if error not exists', async () => { mockErrorPagesRequest(); diff --git a/packages/sitecore-jss/src/site/graphql-error-pages-service.ts b/packages/sitecore-jss/src/site/graphql-error-pages-service.ts index f7a123cf47..32f704dd45 100644 --- a/packages/sitecore-jss/src/site/graphql-error-pages-service.ts +++ b/packages/sitecore-jss/src/site/graphql-error-pages-service.ts @@ -2,6 +2,7 @@ import { GraphQLClient, GraphQLRequestClient, GraphQLRequestClientConfig } from import { siteNameError } from '../constants'; import debug from '../debug'; import { LayoutServiceData } from '../layout'; +import { GraphQLRequestClientFactory } from '../graphql-request-client'; // The default query for request error handling const defaultQuery = /* GraphQL */ ` @@ -27,12 +28,14 @@ export interface GraphQLErrorPagesServiceConfig extends Pick { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; /** * The JSS application name */ @@ -41,6 +44,11 @@ export interface GraphQLErrorPagesServiceConfig * The language */ language: string; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; } /** @@ -108,6 +116,17 @@ export class GraphQLErrorPagesService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.options.endpoint) { + if (!this.options.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.options.clientFactory({ + debugger: debug.errorpages, + retries: this.options.retries, + }); + } + return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.errorpages, diff --git a/packages/sitecore-jss/src/site/graphql-redirects-service.test.ts b/packages/sitecore-jss/src/site/graphql-redirects-service.test.ts index 2ced61e857..942e485ab6 100644 --- a/packages/sitecore-jss/src/site/graphql-redirects-service.test.ts +++ b/packages/sitecore-jss/src/site/graphql-redirects-service.test.ts @@ -4,6 +4,7 @@ import spies from 'chai-spies'; import nock from 'nock'; import { GraphQLRedirectsService, RedirectsQueryResult } from './graphql-redirects-service'; import { siteNameError } from '../constants'; +import { GraphQLRequestClient } from '../graphql-request-client'; use(spies); @@ -82,6 +83,22 @@ describe('GraphQLRedirectsService', () => { return expect(nock.isDone()).to.be.true; }); + it('should get redirects using clientFactory', async () => { + mockRedirectsRequest(siteName); + + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint, + apiKey, + }); + + const service = new GraphQLRedirectsService({ clientFactory }); + const result = await service.fetchRedirects(siteName); + + expect(result).to.deep.equal(redirectsQueryResult.site?.siteInfo?.redirects); + + return expect(nock.isDone()).to.be.true; + }); + it('should get no redirects', async () => { mockRedirectsRequest(); diff --git a/packages/sitecore-jss/src/site/graphql-redirects-service.ts b/packages/sitecore-jss/src/site/graphql-redirects-service.ts index 11adc17757..87be5a21c4 100644 --- a/packages/sitecore-jss/src/site/graphql-redirects-service.ts +++ b/packages/sitecore-jss/src/site/graphql-redirects-service.ts @@ -2,6 +2,7 @@ import { GraphQLClient, GraphQLRequestClient } from '../graphql'; import { siteNameError } from '../constants'; import debug from '../debug'; import { MemoryCacheClient, CacheOptions, CacheClient } from '../cache-client'; +import { GraphQLRequestClientFactory } from '../graphql-request-client'; export const REDIRECT_TYPE_301 = 'REDIRECT_301'; export const REDIRECT_TYPE_302 = 'REDIRECT_302'; @@ -35,16 +36,23 @@ const defaultQuery = /* GraphQL */ ` export type GraphQLRedirectsServiceConfig = CacheOptions & { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; /** * Override fetch method. Uses 'GraphQLRequestClient' default otherwise. */ fetch?: typeof fetch; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; }; /** @@ -105,6 +113,17 @@ export class GraphQLRedirectsService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.options.endpoint) { + if (!this.options.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.options.clientFactory({ + debugger: debug.redirects, + fetch: this.options.fetch, + }); + } + return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.redirects, diff --git a/packages/sitecore-jss/src/site/graphql-robots-service.test.ts b/packages/sitecore-jss/src/site/graphql-robots-service.test.ts index 26c46bf10c..f9d7e3f266 100644 --- a/packages/sitecore-jss/src/site/graphql-robots-service.test.ts +++ b/packages/sitecore-jss/src/site/graphql-robots-service.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import nock from 'nock'; import { GraphQLRobotsService } from './graphql-robots-service'; import { siteNameError } from '../constants'; +import { GraphQLRequestClient } from '../graphql-request-client'; const robotsQueryResultNull = { site: { @@ -60,5 +61,21 @@ describe('GraphQLRobotsService', () => { return expect(nock.isDone()).to.be.true; }); + + it('should get robots.txt using clientFactory', async () => { + mockRobotsRequest(siteName); + + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint, + apiKey, + }); + + const service = new GraphQLRobotsService({ siteName, clientFactory }); + + const robots = await service.fetchRobots(); + expect(robots).to.equal(siteName); + + return expect(nock.isDone()).to.be.true; + }); }); }); diff --git a/packages/sitecore-jss/src/site/graphql-robots-service.ts b/packages/sitecore-jss/src/site/graphql-robots-service.ts index e1541b6c9f..f0dbafd456 100644 --- a/packages/sitecore-jss/src/site/graphql-robots-service.ts +++ b/packages/sitecore-jss/src/site/graphql-robots-service.ts @@ -1,6 +1,7 @@ import { GraphQLClient, GraphQLRequestClient } from '../graphql'; import { siteNameError } from '../constants'; import debug from '../debug'; +import { GraphQLRequestClientFactory } from '../graphql-request-client'; // The default query for request robots.txt const defaultQuery = /* GraphQL */ ` @@ -16,16 +17,23 @@ const defaultQuery = /* GraphQL */ ` export type GraphQLRobotsServiceConfig = { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; /** * The JSS application name */ siteName: string; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; }; /** @@ -82,6 +90,16 @@ export class GraphQLRobotsService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.options.endpoint) { + if (!this.options.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.options.clientFactory({ + debugger: debug.robots, + }); + } + return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.robots, diff --git a/packages/sitecore-jss/src/site/graphql-siteinfo-service.test.ts b/packages/sitecore-jss/src/site/graphql-siteinfo-service.test.ts index 9b986eddd4..d9c95db7d4 100644 --- a/packages/sitecore-jss/src/site/graphql-siteinfo-service.test.ts +++ b/packages/sitecore-jss/src/site/graphql-siteinfo-service.test.ts @@ -2,7 +2,7 @@ import { expect } from 'chai'; import nock from 'nock'; import { GraphQLSiteInfoService, GraphQLSiteInfoResult } from './graphql-siteinfo-service'; -import { PageInfo } from '../graphql'; +import { GraphQLRequestClient, PageInfo } from '../graphql'; describe('GraphQLSiteInfoService', () => { const endpoint = 'http://site'; @@ -106,6 +106,43 @@ describe('GraphQLSiteInfoService', () => { ]); }); + it('should return correct result using clientFactory', async () => { + mockSiteInfoRequest( + nonEmptyResponse({ + sites: [ + site({ + name: 'public 0', + hostName: 'pr.showercurtains.org', + language: '', + pointOfSale: '', + }), + ], + }) + ); + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint, + apiKey, + }); + const service = new GraphQLSiteInfoService({ clientFactory }); + const result = await service.fetchSiteInfo(); + expect(result).to.be.deep.equal([ + { + name: 'site 0', + hostName: 'restricted.gov', + language: 'en', + pointOfSale: { + en: 'en-pos', + }, + }, + { + name: 'public 0', + hostName: 'pr.showercurtains.org', + language: '', + pointOfSale: undefined, + }, + ]); + }); + it('should return correct result using custom pageSize', async () => { mockSiteInfoRequest(nonEmptyResponse({ count: 2, pageInfo: { hasNext: true, endCursor: '' } })); mockSiteInfoRequest( diff --git a/packages/sitecore-jss/src/site/graphql-siteinfo-service.ts b/packages/sitecore-jss/src/site/graphql-siteinfo-service.ts index 88625de9fc..8c57ca256c 100644 --- a/packages/sitecore-jss/src/site/graphql-siteinfo-service.ts +++ b/packages/sitecore-jss/src/site/graphql-siteinfo-service.ts @@ -2,6 +2,7 @@ import { URLSearchParams } from 'url'; import { GraphQLClient, GraphQLRequestClient, PageInfo } from '../graphql'; import debug from '../debug'; import { CacheClient, CacheOptions, MemoryCacheClient } from '../cache-client'; +import { GraphQLRequestClientFactory } from '../graphql-request-client'; const headlessSiteGroupingTemplate = 'E46F3AF2-39FA-4866-A157-7017C4B2A40C'; const sitecoreContentRootItem = '0DE95AE4-41AB-4D01-9EB0-67441B7C2450'; @@ -68,18 +69,25 @@ export type SiteInfo = { export type GraphQLSiteInfoServiceConfig = CacheOptions & { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: 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 10 */ pageSize?: number; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; }; type GraphQLSiteInfoResponse = { @@ -176,6 +184,16 @@ export class GraphQLSiteInfoService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.config.endpoint) { + if (!this.config.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.config.clientFactory({ + debugger: debug.multisite, + }); + } + return new GraphQLRequestClient(this.config.endpoint, { apiKey: this.config.apiKey, debugger: debug.multisite, diff --git a/packages/sitecore-jss/src/site/graphql-sitemap-service.test.ts b/packages/sitecore-jss/src/site/graphql-sitemap-service.test.ts index 953640a7b8..967ca4f508 100644 --- a/packages/sitecore-jss/src/site/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss/src/site/graphql-sitemap-service.test.ts @@ -2,6 +2,7 @@ import { expect } from 'chai'; import nock from 'nock'; import { GraphQLSitemapXmlService } from './graphql-sitemap-service'; import { siteNameError } from '../constants'; +import { GraphQLRequestClient } from '../graphql-request-client'; const sitemapQueryResultNull = { site: { @@ -65,6 +66,23 @@ describe('GraphQLSitemapXmlService', () => { return expect(nock.isDone()).to.be.true; }); + it('should fetch sitemap using clientFactory', async () => { + mockSitemapRequest(mockSitemap); + + const clientFactory = GraphQLRequestClient.createClientFactory({ + endpoint, + apiKey, + }); + + const service = new GraphQLSitemapXmlService({ siteName, clientFactory }); + const sitemaps = await service.fetchSitemaps(); + + expect(sitemaps.length).to.equal(1); + expect(sitemaps).to.deep.equal(mockSitemap); + + return expect(nock.isDone()).to.be.true; + }); + it('should fetch sitemaps', async () => { mockSitemapRequest(mockSitemaps); diff --git a/packages/sitecore-jss/src/site/graphql-sitemap-service.ts b/packages/sitecore-jss/src/site/graphql-sitemap-service.ts index 4309c381e1..3faf5bb5a7 100644 --- a/packages/sitecore-jss/src/site/graphql-sitemap-service.ts +++ b/packages/sitecore-jss/src/site/graphql-sitemap-service.ts @@ -1,6 +1,7 @@ import { GraphQLClient, GraphQLRequestClient } from '../graphql'; import { siteNameError } from '../constants'; import debug from '../debug'; +import { GraphQLRequestClientFactory } from '../graphql-request-client'; const PREFIX_NAME_SITEMAP = 'sitemap'; @@ -18,16 +19,23 @@ const defaultQuery = /* GraphQL */ ` export type GraphQLSitemapXmlServiceConfig = { /** * Your Graphql endpoint + * @deprecated use @param clientFactory property instead */ - endpoint: string; + endpoint?: string; /** * The API key to use for authentication + * @deprecated use @param clientFactory property instead */ - apiKey: string; + apiKey?: string; /** * The JSS application name */ siteName: string; + /** + * A GraphQL Request Client Factory is a function that accepts configuration and returns an instance of a GraphQLRequestClient. + * This factory function is used to create and configure GraphQL clients for making GraphQL API requests. + */ + clientFactory?: GraphQLRequestClientFactory; }; /** @@ -94,6 +102,16 @@ export class GraphQLSitemapXmlService { * @returns {GraphQLClient} implementation */ protected getGraphQLClient(): GraphQLClient { + if (!this.options.endpoint) { + if (!this.options.clientFactory) { + throw new Error('You should provide either an endpoint and apiKey, or a clientFactory.'); + } + + return this.options.clientFactory({ + debugger: debug.sitemap, + }); + } + return new GraphQLRequestClient(this.options.endpoint, { apiKey: this.options.apiKey, debugger: debug.sitemap,