Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[sitecore-jss][nextjs] Use site query to retrieve dictionary data in XMCloud #1804

Merged
merged 12 commits into from
Jul 18, 2024
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ Our versioning strategy is as follows:
* `[sitecore-jss]` _GraphQLRequestClient_ now can accept custom 'headers' in the constructor or via _createClientFactory_ ([#1806](https://github.com/Sitecore/jss/pull/1806))
* `[templates/nextjs]` Removed cors header for API endpoints from _lib/next-config/plugins/cors-header_ plugin since cors is handled by API handlers / middlewares ([#1806](https://github.com/Sitecore/jss/pull/1806))
* `[sitecore-jss-nextjs]` Updates to Next.js editing integration to further support secure hosting scenarios (on XM Cloud & Vercel) ([#1832](https://github.com/Sitecore/jss/pull/1832))
* `[sitecore-jss]` `[nextjs-xmcloud]` DictionaryService can now use a `site` GraphQL query instead of `search` one to improve performance. This is currently only available for XMCloud deployments and is enabled with `nextjs-xmcloud` add-on by default ([#1804](https://github.com/Sitecore/jss/pull/1804))

### 🛠 Breaking Change

Expand Down
9 changes: 9 additions & 0 deletions docs/upgrades/unreleased.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,15 @@
);
```

* Add a `useSiteQuery` parameter when `GraphQLDictionaryService` is initialized in `/src/lib/dictionary-service-factory.ts` :
```
new GraphQLDictionaryService({
siteName,
clientFactory,
.....
useSiteQuery: true,
})

* We have introduced a new configuration option, `pagesEditMode`, in the `\src\pages\api\editing\config.ts` file to support the new editing metadata architecture for Pages (XMCloud). This option allows you to specify the editing mode used by Pages. It is set to `metadata` by default. However, if you are not ready to use a new integration and continue using the existing architecture, you can explicitly set the `pagesEditMode` to `chromes`.

```ts
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
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,
/*
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,
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
})
: new RestDictionaryService({
apiHost: config.sitecoreApiHost,
apiKey: config.sitecoreApiKey,
siteName,
});
}
}

/** DictionaryServiceFactory singleton */
export const dictionaryServiceFactory = new DictionaryServiceFactory();
34 changes: 2 additions & 32 deletions packages/sitecore-jss/src/editing/graphql-editing-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,10 +101,8 @@ describe('GraphQLEditingService', () => {
expect(result).to.deep.equal({
layoutData: layoutDataResponse,
dictionary: {
/* TODO: revert when dictionary schema updated
foo: 'foo-phrase',
bar: 'bar-phrase',
*/
},
});

Expand Down Expand Up @@ -150,18 +148,15 @@ describe('GraphQLEditingService', () => {
expect(result).to.deep.equal({
layoutData: layoutDataResponse,
dictionary: {
/* TODO: revert when dictionary schema updated
foo: 'foo-phrase',
bar: 'bar-phrase',
*/
},
});

spy.restore(clientFactorySpy);
});

// TODO: re-enable when dictionary schema updated
xit('should fetch editing data when dicionary has multiple pages', async () => {
it('should fetch editing data when dicionary has multiple pages', async () => {
nock(hostname, { reqheaders: { sc_editMode: 'true' } })
.post(endpointPath, /EditingQuery/gi)
.reply(200, mockEditingServiceResponse(true));
Expand Down Expand Up @@ -212,7 +207,7 @@ describe('GraphQLEditingService', () => {
.to.be.called.with(dictionaryQuery, {
language,
siteName,
after: '',
after: 'cursor',
});

expect(clientFactorySpy.returnValues[0].request)
Expand All @@ -226,10 +221,8 @@ describe('GraphQLEditingService', () => {
expect(result).to.deep.equal({
layoutData: layoutDataResponse,
dictionary: {
/* TODO: revert when dictionary schema updated
foo: 'foo-phrase',
bar: 'bar-phrase',
*/
'foo-one': 'foo-one-phrase',
'bar-one': 'bar-one-phrase',
'foo-two': 'foo-two-phrase',
Expand Down Expand Up @@ -277,27 +270,4 @@ describe('GraphQLEditingService', () => {
expect(error.response.error).to.equal('Internal server error');
}
});

// TODO: remove when dictionary site schema available
it('should return empty dictionary results', async () => {
nock(hostname, { reqheaders: { sc_editMode: 'true' } })
.post(endpointPath, /EditingQuery/gi)
.reply(200, editingData);

const service = new GraphQLEditingService({
clientFactory,
});

const result = await service.fetchEditingData({
language,
version,
itemId,
siteName,
});

expect(result).to.deep.equal({
layoutData: layoutDataResponse,
dictionary: {},
});
});
});
25 changes: 3 additions & 22 deletions packages/sitecore-jss/src/editing/graphql-editing-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,15 +12,8 @@ const PAGE_SIZE = 1000;
/**
* GraphQL query for fetching editing data.
*/
/*
TODO: re-add dictionary part when dictionary schema updated
query EditingQuery(
$siteName: String!
$itemId: String!
$version: String!
$language: String!
$after: String
) {
export const query = /* GraphQL */ `
query EditingQuery($itemId: String!, $language: String!, $version: String) {
item(path: $itemId, language: $language, version: $version) {
rendered
}
Expand All @@ -35,13 +28,6 @@ TODO: re-add dictionary part when dictionary schema updated
}
}
}
*/
export const query = /* GraphQL */ `
query EditingQuery($itemId: String!, $language: String!, $version: String) {
item(path: $itemId, language: $language, version: $version) {
rendered
}
}
`;

/**
Expand Down Expand Up @@ -113,7 +99,6 @@ export class GraphQLEditingService {
* @param {string} variables.itemId - The item id (path) to fetch layout data for.
* @param {string} variables.language - The language to fetch layout data for.
* @param {string} [variables.version] - The version of the item (optional).
* @param variables.version
* @returns {Promise} The layout data and dictionary phrases.
*/
async fetchEditingData({
Expand All @@ -131,8 +116,7 @@ export class GraphQLEditingService {

const dictionary: DictionaryPhrases = {};
let dictionaryResults: { key: string; value: string }[] = [];
// TODO: set to true when dictionary schema updated
let hasNext = false;
let hasNext = true;
let after = '';

const editingData = await this.graphQLClient.request<GraphQLEditingQueryResponse>(query, {
Expand All @@ -142,12 +126,9 @@ export class GraphQLEditingService {
language,
});

/*
TODO: re-enable when dictionary schema updated
dictionaryResults = editingData.site.siteInfo.dictionary.results;
hasNext = editingData.site.siteInfo.dictionary.pageInfo.hasNext;
after = editingData.site.siteInfo.dictionary.pageInfo.endCursor;
*/

while (hasNext) {
const data = await this.graphQLClient.request<GraphQLDictionaryQueryResponse>(
Expand Down
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
84 changes: 84 additions & 0 deletions packages/sitecore-jss/src/i18n/graphql-dictionary-service.test.ts
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 Down Expand Up @@ -270,4 +271,87 @@ describe('GraphQLDictionaryService', () => {
expect(calledWithArgs.retries).to.equal(mockServiceConfig.retries);
expect(calledWithArgs.retryStrategy).to.deep.equal(mockServiceConfig.retryStrategy);
});

describe('with site query', () => {
art-alexeyenko marked this conversation as resolved.
Show resolved Hide resolved
it('should fetch dictionary phrases using clientFactory', async () => {
nock(endpoint, { reqheaders: { sc_apikey: apiKey } })
.post('/')
.reply(200, dictionarySiteQueryResponse.singlepage);

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 of 500, if pageSize not provided in constructor', async () => {
nock(endpoint)
.persist()
.post(
'/',
(body) =>
body.query.indexOf('$pageSize: Int = 500') > 0 && body.variables.pageSize === undefined
)
.reply(200, dictionarySiteQueryResponse.singlepage);

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 = 1;

nock(endpoint)
.post('/', (body) => body.variables.pageSize === customPageSize)
.reply(200, dictionarySiteQueryResponse.multipage.page1)
.post(
'/',
(body) =>
body.variables.pageSize === customPageSize && body.variables.after === 'nextpage'
)
.reply(200, dictionarySiteQueryResponse.multipage.page2);

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', 'baz');
});

it('should throw when getting http errors', async () => {
nock(endpoint)
.post('/')
.reply(401, {
error: 'whoops',
});

const service = new GraphQLDictionaryService({
clientFactory,
siteName,
cacheEnabled: false,
useSiteQuery: true,
});

await service.fetchDictionaryData('en').catch((error) => {
expect(error.response.status).to.equal(401);
expect(error.response.error).to.equal('whoops');
});
});
});
});
Loading