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

GraphQL site info service #1227

Merged
merged 5 commits into from
Nov 4, 2022
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 packages/sitecore-jss/src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,4 +16,6 @@ export const JSS_MODE = {
DISCONNECTED: 'disconnected',
};

export const headlessSiteGroupingTemplate = 'E46F3AF2-39FA-4866-A157-7017C4B2A40C';
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can it be added to SitecoreTemplateId object?
As I pointed in the comment: https://github.com/Sitecore/jss/blob/dev/packages/sitecore-jss/src/constants.ts#L1


export const siteNameError = 'The siteName cannot be empty';
1 change: 1 addition & 0 deletions packages/sitecore-jss/src/debug.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ export default {
dictionary: debug(`${rootNamespace}:dictionary`),
editing: debug(`${rootNamespace}:editing`),
sitemap: debug(`${rootNamespace}:sitemap`),
multisite: debug(`${rootNamespace}:multisite`),
robots: debug(`${rootNamespace}:robots`),
redirects: debug(`${rootNamespace}:redirects`),
personalize: debug(`${rootNamespace}:personalize`),
Expand Down
128 changes: 128 additions & 0 deletions packages/sitecore-jss/src/site/graphql-siteinfo-service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import { expect } from 'chai';
import nock from 'nock';
import { GraphQLSiteInfoService } from './graphql-siteinfo-service';

describe('GraphQLSiteInfoService', () => {
const endpoint = 'http://site';
const apiKey = 'some-api-key';
const nonEmptyResponse = {
data: {
search: {
results: [
{
name: 'site 51',
hostName: {
value: 'restricted.gov',
},
virtualFolder: {
value: '/aliens',
},
language: {
value: 'en',
},
},
{
name: 'public',
hostName: {
value: 'pr.showercurtains.org',
},
virtualFolder: {
value: '/we-are-open',
},
language: {
value: '',
},
},
],
},
},
};
const emptyResponse = {
data: {
search: {
results: [],
},
},
};

afterEach(() => {
nock.cleanAll();
});

const mockSiteInfoRequest = () => {
nock(endpoint)
.post('/')
.reply(200, nonEmptyResponse);
};

it('should return correct result', async () => {
mockSiteInfoRequest();
const service = new GraphQLSiteInfoService({ apiKey: apiKey, endpoint: endpoint });
const result = await service.fetchSiteInfo();
expect(result).to.be.deep.equal([
{
name: 'site 51',
hostName: 'restricted.gov',
virtualFolder: '/aliens',
language: 'en',
},
{
name: 'public',
hostName: 'pr.showercurtains.org',
virtualFolder: '/we-are-open',
language: '',
},
]);
});

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

it('should use caching by default', async () => {
mockSiteInfoRequest();
const service = new GraphQLSiteInfoService({ apiKey: apiKey, endpoint: endpoint });
const result = await service.fetchSiteInfo();
nock.cleanAll();
nock(endpoint)
.post('/')
.reply(200, emptyResponse);
const resultCached = await service.fetchSiteInfo();
expect(resultCached).to.deep.equal(result);
});

it('should be possible to disable cache', async () => {
mockSiteInfoRequest();
const service = new GraphQLSiteInfoService({
apiKey: apiKey,
endpoint: endpoint,
cacheEnabled: false,
});
const result = await service.fetchSiteInfo();
expect(result).to.be.deep.equal([
{
name: 'site 51',
hostName: 'restricted.gov',
virtualFolder: '/aliens',
language: 'en',
},
{
name: 'public',
hostName: 'pr.showercurtains.org',
virtualFolder: '/we-are-open',
language: '',
},
]);
nock.cleanAll();
nock(endpoint)
.post('/')
.reply(200, emptyResponse);
const resultCached = await service.fetchSiteInfo();
expect(resultCached).to.deep.equal([]);
});
});
131 changes: 131 additions & 0 deletions packages/sitecore-jss/src/site/graphql-siteinfo-service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
import { GraphQLClient, GraphQLRequestClient } from '../graphql';
import debug from '../debug';
import { CacheClient, CacheOptions, MemoryCacheClient } from '../cache-client';
import { headlessSiteGroupingTemplate } from '../constants';

const defaultQuery = /* GraphQL */ `
{
search(
where: { name: "_templates", value: "${headlessSiteGroupingTemplate}", operator: CONTAINS }
) {
results {
... on Item {
name
hostName: field(name: "Hostname") {
value
}
virtualFolder: field(name: "VirtualFolder") {
value
}
language: field(name: "Language") {
value
}
}
}
}
}
`;

export type SiteInfo = {
name: string;
hostName: string;
virtualFolder: string;
language: string;
};

export type GraphQLSiteInfoServiceConfig = CacheOptions & {
/**
* Your Graphql endpoint
*/
endpoint: string;
/**
* The API key to use for authentication
*/
apiKey: string;
};

type GraphQLSiteInfoResponse = {
search: {
results: GraphQLSiteInfoResult[];
};
};

type GraphQLSiteInfoResult = {
name: string;
hostName: {
value: string;
};
virtualFolder: {
value: string;
};
language: {
value: string;
};
};

export class GraphQLSiteInfoService {
private graphQLClient: GraphQLClient;
private cache: CacheClient<SiteInfo[]>;

protected get query(): string {
return defaultQuery;
}

/**
* Creates an instance of graphQL service to retrieve site configuration list from Sitecore
* @param {GraphQLSiteInfoServiceConfig} config instance
*/
constructor(private config: GraphQLSiteInfoServiceConfig) {
this.graphQLClient = this.getGraphQLClient();
this.cache = this.getCacheClient();
}

async fetchSiteInfo(): Promise<SiteInfo[]> {
const cachedResult = this.cache.getCacheValue(this.getCacheKey());
if (cachedResult) {
return cachedResult;
}

const response = await this.graphQLClient.request<GraphQLSiteInfoResponse>(this.query);
const result = response?.search?.results?.reduce<SiteInfo[]>((result, current) => {
result.push({
name: current.name,
hostName: current.hostName.value,
virtualFolder: current.virtualFolder.value,
language: current.language.value,
});
return result;
}, []);
this.cache.setCacheValue(this.getCacheKey(), result);
return result;
}

/**
* Gets cache client implementation
* Override this method if custom cache needs to be used
* @returns CacheClient instance
*/
protected getCacheClient(): CacheClient<SiteInfo[]> {
return new MemoryCacheClient<SiteInfo[]>({
cacheEnabled: this.config.cacheEnabled ?? true,
cacheTimeout: this.config.cacheTimeout ?? 10,
});
}

/**
* Gets a GraphQL client that can make requests to the API. Uses graphql-request as the default
* library for fetching graphql data (@see GraphQLRequestClient). Override this method if you
* want to use something else.
* @returns {GraphQLClient} implementation
*/
protected getGraphQLClient(): GraphQLClient {
return new GraphQLRequestClient(this.config.endpoint, {
apiKey: this.config.apiKey,
debugger: debug.multisite,
});
}

private getCacheKey(): string {
return 'siteinfo-service-cache';
}
}
6 changes: 6 additions & 0 deletions packages/sitecore-jss/src/site/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,9 @@ export {
GraphQLErrorPagesService,
GraphQLErrorPagesServiceConfig,
} from './graphql-error-pages-service';

export {
SiteInfo,
GraphQLSiteInfoService,
GraphQLSiteInfoServiceConfig,
} from './graphql-siteinfo-service';