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][templates/nextjs-xmcloud] Enable site query in XMCloud #1739

Merged
merged 7 commits into from
Feb 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ Our versioning strategy is as follows:
* `[templates/nextjs-xmcloud]` `[sitecore-jss]` `[sitecore-jss-nextjs]` `[sitecore-jss-react]` Add support for loading appropriate stylesheets whenever a theme is applied to BYOC and SXA components by introducing new function getComponentLibraryStylesheetLinks, which replaces getFEAASLibraryStylesheetLinks (which has been marked as deprecated) ([#1722](https://github.com/Sitecore/jss/pull/1722))
* `[templates/nextjs-xmcloud]` `[sitecore-jss-nextjs]` Add protected endpoint which provides configuration information (the sitecore packages used by the app and all registered components) to be used to determine feature compatibility on Pages side. ([#1724](https://github.com/Sitecore/jss/pull/1724) [#1734](https://github.com/Sitecore/jss/pull/1734))
* `[sitecore-jss-nextjs]` `[templates/nextjs]` [BYOC] Component Builder integration endpoint ([#1729](https://github.com/Sitecore/jss/pull/1729))
* `[sitecore-jss]` `[templates/nextjs-xmcloud]` Enable site GraphQL query for mutlisite in XMCloud instead of default search one. Site query should take "Valid for environment" SXA site setting into account when returning site list. ([#1739](https://github.com/Sitecore/jss/pull/1739))
* To enable this on your existing app, modify the \scripts\config\plugins\multisite.ts file and add "useSiteQuery: true" to GraphQLSiteInfoService constructor call

### 🐛 Bug Fixes

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,10 @@ class MultisitePlugin implements ConfigPlugin {
try {
const siteInfoService = new GraphQLSiteInfoService({
clientFactory: createGraphQLClientFactory(config),
<% if (templates.includes("nextjs-xmcloud")) { -%>
// enable site query for the service. Only works on XMCloud currently
useSiteQuery: true,
<% } -%>
});
sites = await siteInfoService.fetchSiteInfo();
} catch (error) {
Expand Down
5 changes: 4 additions & 1 deletion packages/sitecore-jss/src/graphql-request-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,10 @@ export type GraphQLRequestClientFactory = (
/**
* Configuration type for @type GraphQLRequestClientFactory
*/
export type GraphQLRequestClientFactoryConfig = { endpoint: string; apiKey?: string };
export type GraphQLRequestClientFactoryConfig = {
endpoint: string;
apiKey?: string;
};

/**
* Represents a default retry strategy for handling retry attempts in case of specific HTTP status codes.
Expand Down
149 changes: 148 additions & 1 deletion packages/sitecore-jss/src/site/graphql-siteinfo-service.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import { expect, spy, use } from 'chai';
import spies from 'chai-spies';
import nock from 'nock';
import { GraphQLSiteInfoService, GraphQLSiteInfoResult } from './graphql-siteinfo-service';
import {
GraphQLSiteInfoService,
GraphQLSiteInfoResult,
GraphQLXmCloudSiteInfoResult,
} from './graphql-siteinfo-service';
import { GraphQLRequestClient, PageInfo } from '../graphql';
import debugApi from 'debug';
import debug from '../debug';
Expand Down Expand Up @@ -266,4 +270,147 @@ describe('GraphQLSiteInfoService', () => {
expect(debug.multisite.log, 'log debug message').to.be.called.once;
expect(nock.isDone(), 'skip request').to.be.false;
});

describe('Fetch with site query in XM Cloud', () => {
const site = ({
name,
hostName,
language,
}: {
name: string;
hostName: string;
language: string;
}): GraphQLXmCloudSiteInfoResult => ({
name,
hostName,
language,
});

const nonEmptyResponse = ({
count = 1,
sites = [],
}: {
count?: number;
sites?: GraphQLXmCloudSiteInfoResult[];
} = {}) => ({
data: {
site: {
siteInfoCollection: [
...[...Array(count).keys()].map((n) =>
site({
name: `site ${n}`,
hostName: 'restricted.gov',
language: 'en',
})
),
...sites,
],
},
},
});

const emptyResponse = {
data: {
site: {
siteInfoCollection: [],
},
},
};

const getSiteQuerySiteInfoService = (initProps: { [key: string]: unknown }) => {
return new GraphQLSiteInfoService({ useSiteQuery: true, ...initProps });
};

it('should return correct result', async () => {
mockSiteInfoRequest(
nonEmptyResponse({
sites: [
site({
name: 'public 0',
hostName: 'pr.showercurtains.org',
language: '',
}),
],
})
);
const service = getSiteQuerySiteInfoService({ apiKey: apiKey, endpoint: endpoint });
const result = await service.fetchSiteInfo();
expect(result).to.be.deep.equal([
{
name: 'site 0',
hostName: 'restricted.gov',
language: 'en',
},
{
name: 'public 0',
hostName: 'pr.showercurtains.org',
language: '',
},
]);
});

it('should return correct result using clientFactory', async () => {
mockSiteInfoRequest(
nonEmptyResponse({
sites: [
site({
name: 'public 0',
hostName: 'pr.showercurtains.org',
language: '',
}),
],
})
);
const clientFactory = GraphQLRequestClient.createClientFactory({
endpoint,
apiKey,
});
const service = getSiteQuerySiteInfoService({ clientFactory });
const result = await service.fetchSiteInfo();
expect(result).to.be.deep.equal([
{
name: 'site 0',
hostName: 'restricted.gov',
language: 'en',
},
{
name: 'public 0',
hostName: 'pr.showercurtains.org',
language: '',
},
]);
});

it('should return empty array when empty result received', async () => {
nock(endpoint)
.post('/')
.reply(200, emptyResponse);
const service = getSiteQuerySiteInfoService({ apiKey: apiKey, endpoint: endpoint });
const result = await service.fetchSiteInfo();
expect(result).to.deep.equal([]);
});

it('should filter out default website', async () => {
mockSiteInfoRequest(
nonEmptyResponse({
sites: [
site({
name: 'website',
hostName: 'notheadless.org',
language: '',
}),
],
})
);
const service = getSiteQuerySiteInfoService({ apiKey: apiKey, endpoint: endpoint });
const result = await service.fetchSiteInfo();
expect(result).to.be.deep.equal([
{
name: 'site 0',
hostName: 'restricted.gov',
language: 'en',
},
]);
});
});
});
64 changes: 62 additions & 2 deletions packages/sitecore-jss/src/site/graphql-siteinfo-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,18 @@ const defaultQuery = /* GraphQL */ `
}
`;

const siteQuery = /* GraphQL */ `
query {
site {
siteInfoCollection {
name
hostName: hostname
language
}
}
}
`;

export type SiteInfo = {
/**
* Additional user-defined properties
Expand Down Expand Up @@ -80,6 +92,10 @@ export type GraphQLSiteInfoServiceConfig = CacheOptions & {
* This factory function is used to create and configure GraphQL clients for making GraphQL API requests.
*/
clientFactory?: GraphQLRequestClientFactory;
/**
* Boolean indicating if service will use site GQL query instead of search
*/
useSiteQuery?: boolean;
};

type GraphQLSiteInfoResponse = {
Expand All @@ -101,6 +117,18 @@ export type GraphQLSiteInfoResult = {
};
};

type GraphQLXmCloudSiteInfoResponse = {
site: {
siteInfoCollection: GraphQLXmCloudSiteInfoResult[];
};
};

export type GraphQLXmCloudSiteInfoResult = {
name: string;
hostName: string;
language: string;
};

export class GraphQLSiteInfoService {
private graphQLClient: GraphQLClient;
private cache: CacheClient<SiteInfo[]>;
Expand All @@ -109,6 +137,13 @@ export class GraphQLSiteInfoService {
return defaultQuery;
}

/**
* site query is available on XM Cloud and XP 10.4+
*/
protected get siteQuery(): string {
return siteQuery;
}

/**
* Creates an instance of graphQL service to retrieve site configuration list from Sitecore
* @param {GraphQLSiteInfoServiceConfig} config instance
Expand All @@ -128,6 +163,15 @@ export class GraphQLSiteInfoService {
return [];
}

const results: SiteInfo[] = this.config.useSiteQuery
? await this.fetchWithSiteQuery()
: await this.fetchWithDefaultQuery();

this.cache.setCacheValue(this.getCacheKey(), results);
return results;
}

protected async fetchWithDefaultQuery(): Promise<SiteInfo[]> {
const results: SiteInfo[] = [];
let hasNext = true;
let after = '';
Expand All @@ -150,11 +194,27 @@ export class GraphQLSiteInfoService {
hasNext = response.search.pageInfo.hasNext;
after = response.search.pageInfo.endCursor;
}

this.cache.setCacheValue(this.getCacheKey(), results);
return results;
}

protected async fetchWithSiteQuery(): Promise<SiteInfo[]> {
const response = await this.graphQLClient.request<GraphQLXmCloudSiteInfoResponse>(
this.siteQuery
);
const result = response?.site?.siteInfoCollection?.reduce<SiteInfo[]>((result, current) => {
// filter out built in website
current.name !== 'website' &&
result.push({
name: current.name,
hostName: current.hostName,
language: current.language,
});
return result;
}, []);

return result;
}

/**
* Gets cache client implementation
* Override this method if custom cache needs to be used
Expand Down