Skip to content

Commit

Permalink
[sitecore-jss][nextjs] Use site query to retrieve dictionary data in …
Browse files Browse the repository at this point in the history
…XMCloud
  • Loading branch information
art-alexeyenko committed May 23, 2024
1 parent 9346470 commit 17574e7
Show file tree
Hide file tree
Showing 5 changed files with 209 additions and 5 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import {
DictionaryService,
RestDictionaryService,
GraphQLDictionaryService,
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
*/
export class DictionaryServiceFactory {
/**
* @param {string} siteName site name
* @returns {DictionaryService} service instance
*/
create(siteName: string): DictionaryService {
return process.env.FETCH_WITH === constants.FETCH_WITH.GRAPHQL
? new GraphQLDictionaryService({
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.
For SXA site(s) and multisite setup there's no need to specify it - it will be autoresolved.
Otherwise, if your Sitecore instance only has 1 JSS App (i.e. in a Sitecore XP setup), you can specify the root item ID here.
rootItemId: '{GUID}'
*/
/*
GraphQL endpoint may reach its rate limit with the amount of 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.
For this, specify the number of 'retries' the GraphQL client will attempt.
By default it is set to 3. You can disable it by configuring it to 0 for this service.
Additionally, you have the flexibility to customize the retry strategy by passing a 'retryStrategy'.
By default it uses the `DefaultRetryStrategy` with exponential back-off factor of 2 and handles error codes 429,
502, 503, 504, 520, 521, 522, 523, 524, 'ECONNRESET', 'ETIMEDOUT' and 'EPROTO' . You can use this class or your own implementation of `RetryStrategy`.
*/
retries: (process.env.GRAPH_QL_SERVICE_RETRIES &&
parseInt(process.env.GRAPH_QL_SERVICE_RETRIES, 10)) as number,
useSiteQuery: true,
})
: new RestDictionaryService({
apiHost: config.sitecoreApiHost,
apiKey: config.sitecoreApiKey,
siteName,
});
}
}

/** DictionaryServiceFactory singleton */
export const dictionaryServiceFactory = new DictionaryServiceFactory();
2 changes: 2 additions & 0 deletions packages/sitecore-jss/src/graphql/search-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ export interface SearchQueryVariables {
}

/**
* @deprecated will be removed with SearchQueryService. Use GraphQLClient and supporting types
* Configuration options for service classes that extend @see SearchQueryService.
* This extends @see SearchQueryVariables because properties that can be passed to the search query
* as predicates should be configurable. 'language' is excluded because, normally, all properties
Expand All @@ -76,6 +77,7 @@ export interface SearchServiceConfig extends Omit<SearchQueryVariables, 'languag
}

/**
* @deprecated use GraphQLClient instead
* Provides functionality for performing GraphQL 'search' operations, including handling pagination.
* This class is meant to be extended or used as a mixin; it's not meant to be used directly.
* @template T The type of objects being requested.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { GraphQLClient, GraphQLRequestClient } from '../graphql-request-client';
import { queryError, GraphQLDictionaryServiceConfig } from './graphql-dictionary-service';
import { GraphQLDictionaryService } from '.';
import dictionaryQueryResponse from '../test-data/mockDictionaryQueryResponse.json';
import dictionarySiteQueryResponse from '../test-data/mockDictionarySiteQueryResponse.json';
import appRootQueryResponse from '../test-data/mockAppRootQueryResponse.json';

class TestService extends GraphQLDictionaryService {
Expand All @@ -17,7 +18,7 @@ class TestService extends GraphQLDictionaryService {
}
}

describe('GraphQLDictionaryService', () => {
describe.only('GraphQLDictionaryService', () => {
const endpoint = 'http://site';
const siteName = 'site-name';
const apiKey = 'api-key';
Expand Down Expand Up @@ -270,4 +271,60 @@ describe('GraphQLDictionaryService', () => {
expect(calledWithArgs.retries).to.equal(mockServiceConfig.retries);
expect(calledWithArgs.retryStrategy).to.deep.equal(mockServiceConfig.retryStrategy);
});

describe('with site query', () => {
it('should fetch dictionary phrases using clientFactory', async () => {
nock(endpoint, { reqheaders: { sc_apikey: apiKey } })
.post('/', /DictionarySiteQuery/gi)
.reply(200, dictionarySiteQueryResponse);

const service = new GraphQLDictionaryService({
siteName,
cacheEnabled: false,
clientFactory,
useSiteQuery: true,
});
const result = await service.fetchDictionaryData('en');
expect(result.foo).to.equal('foo');
expect(result.bar).to.equal('bar');
});

it('should use default pageSize, if pageSize not provided', async () => {
nock(endpoint)
.post(
'/',
(body) =>
body.query.indexOf('$pageSize: Int = 10') > 0 && body.variables.pageSize === undefined
)
.reply(200, dictionarySiteQueryResponse);

const service = new GraphQLDictionaryService({
clientFactory,
siteName,
cacheEnabled: false,
pageSize: undefined,
useSiteQuery: true,
});
const result = await service.fetchDictionaryData('en');
expect(result).to.have.all.keys('foo', 'bar');
});

it('should use a custom pageSize, if provided', async () => {
const customPageSize = 2;

nock(endpoint)
.post('/', (body) => body.variables.pageSize === customPageSize)
.reply(200, dictionarySiteQueryResponse);

const service = new GraphQLDictionaryService({
clientFactory,
siteName,
cacheEnabled: false,
pageSize: customPageSize,
useSiteQuery: true,
});
const result = await service.fetchDictionaryData('en');
expect(result).to.have.all.keys('foo', 'bar');
});
});
});
82 changes: 78 additions & 4 deletions packages/sitecore-jss/src/i18n/graphql-dictionary-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,39 @@ const query = /* GraphQL */ `
}
`;

const siteQuery = /* GraphQL */ `
query DictionarySiteQuery(
$siteName: String!
$language: String!
$pageSize: Int = 10
$after: String
) {
site {
siteInfo(site: $siteName) {
dictionary(language: $language, first: $pageSize, after: $after) [
{
key
value
}
]
}
}
}
`;

/**
* Configuration options for @see GraphQLDictionaryService instances
*/
export interface GraphQLDictionaryServiceConfig
extends SearchServiceConfig,
extends Omit<SearchServiceConfig, 'siteName'>,
CacheOptions,
Pick<GraphQLRequestClientConfig, 'retries' | 'retryStrategy'> {
/**
* The name of the current Sitecore site. This is used to to determine the search query root
* in cases where one is not specified by the caller.
*/
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.
Expand All @@ -73,6 +99,11 @@ export interface GraphQLDictionaryServiceConfig
* @default '061cba1554744b918a0617903b102b82' (/sitecore/templates/Foundation/JavaScript Services/App)
*/
jssAppTemplateId?: string;

/**
* Optional. Use site query for dictionary fetch instead of search query (XM Cloud only)
*/
useSiteQuery?: boolean;
}

/**
Expand All @@ -83,6 +114,14 @@ export type DictionaryQueryResult = {
phrase: { value: string };
};

export type DistionarySiteQueryResponse = {
site: {
siteInfo: {
dictionary: { key: string; value: string }[];
};
};
};

/**
* Service that fetch dictionary data using Sitecore's GraphQL API.
* @augments DictionaryServiceBase
Expand All @@ -103,9 +142,8 @@ export class GraphQLDictionaryService extends DictionaryServiceBase {
}

/**
* Fetches dictionary data for internalization.
* Fetches dictionary data for internalization. Uses search query by default
* @param {string} language the language to fetch
* @default query (@see query)
* @returns {Promise<DictionaryPhrases>} dictionary phrases
* @throws {Error} if the app root was not found for the specified site and language.
*/
Expand All @@ -117,6 +155,24 @@ export class GraphQLDictionaryService extends DictionaryServiceBase {
return cachedValue;
}

debug.dictionary('fetching site root for %s %s', language, this.options.siteName);
const phrases = this.options.useSiteQuery
? await this.fetchWithSiteQuery(language)
: await this.fetchWithSearchQuery(language);

this.setCacheValue(cacheKey, phrases);
return phrases;
}

/**
* Fetches dictionary data with search query
* This is the default behavior for non-XMCloud deployments
* @param {string} language the language to fetch
* @default query (@see query)
* @returns {Promise<DictionaryPhrases>} dictionary phrases
* @throws {Error} if the app root was not found for the specified site and language.
*/
async fetchWithSearchQuery(language: string): Promise<DictionaryPhrases> {
debug.dictionary('fetching site root for %s %s', language, this.options.siteName);

// If the caller does not specify a root item ID, then we try to figure it out
Expand Down Expand Up @@ -146,7 +202,25 @@ export class GraphQLDictionaryService extends DictionaryServiceBase {
results.forEach((item) => (phrases[item.key.value] = item.phrase.value));
});

this.setCacheValue(cacheKey, phrases);
return phrases;
}

/**
* Fetches dictionary data with site query
* This is the default behavior for XMCloud deployments
* @param {string} language the language to fetch
* @default siteQuery (@see siteQuery)
* @returns {Promise<DictionaryPhrases>} dictionary phrases
*/
async fetchWithSiteQuery(language: string): Promise<DictionaryPhrases> {
const phrases: DictionaryPhrases = {};
debug.dictionary('fetching dictionary data for %s %s', language, this.options.siteName);
const results = await this.graphQLClient.request<DistionarySiteQueryResponse>(siteQuery, {
siteName: this.options.siteName,
language,
pageSize: this.options.pageSize,
});
results.site.siteInfo.dictionary.forEach((item) => (phrases[item.key] = item.value));
return phrases;
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"data": {
"site": {
"siteInfo": {
"dictionary": [
{
"key": "foo",
"value": "foo"
},
{
"key": "bar",
"value": "bar"
}
]
}
}
}
}

0 comments on commit 17574e7

Please sign in to comment.