From 63295a6863623589b94167a3cca77a96ca7511dd Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 3 Mar 2021 09:30:35 +0200 Subject: [PATCH 1/8] [sitecore-jss] Add GraphQLLayoutService --- packages/sitecore-jss-nextjs/src/index.ts | 2 + packages/sitecore-jss/src/index.ts | 2 + .../sitecore-jss/src/layout-service.test.ts | 571 +++++++++++------- packages/sitecore-jss/src/layout-service.ts | 63 +- 4 files changed, 412 insertions(+), 226 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/index.ts b/packages/sitecore-jss-nextjs/src/index.ts index 45510cd12d..634afad24b 100644 --- a/packages/sitecore-jss-nextjs/src/index.ts +++ b/packages/sitecore-jss-nextjs/src/index.ts @@ -6,6 +6,8 @@ export { AxiosDataFetcher, AxiosDataFetcherConfig, LayoutService, + GraphQLLayoutService, + GraphQLLayoutServiceConfig, RestLayoutService, RestLayoutServiceConfig, DictionaryPhrases, diff --git a/packages/sitecore-jss/src/index.ts b/packages/sitecore-jss/src/index.ts index e4c06a5a14..5176131342 100644 --- a/packages/sitecore-jss/src/index.ts +++ b/packages/sitecore-jss/src/index.ts @@ -11,6 +11,8 @@ export { AxiosDataFetcher, AxiosDataFetcherConfig } from './data-fetcher'; export { LayoutService, + GraphQLLayoutService, + GraphQLLayoutServiceConfig, RestLayoutService, RestLayoutServiceConfig, DataFetcherResolver, diff --git a/packages/sitecore-jss/src/layout-service.test.ts b/packages/sitecore-jss/src/layout-service.test.ts index ae223dd5bc..8945dcf7f1 100644 --- a/packages/sitecore-jss/src/layout-service.test.ts +++ b/packages/sitecore-jss/src/layout-service.test.ts @@ -3,7 +3,8 @@ import { expect, spy, use } from 'chai'; import spies from 'chai-spies'; -import { RestLayoutService } from './layout-service'; +import nock from 'nock'; +import { GraphQLLayoutService, RestLayoutService } from './layout-service'; import axios from 'axios'; import MockAdapter from 'axios-mock-adapter'; import { IncomingMessage, ServerResponse } from 'http'; @@ -12,279 +13,401 @@ import { LayoutServiceData, PlaceholderData } from './dataModels'; use(spies); -describe('RestLayoutService', () => { - let mock: MockAdapter; +describe('LayoutService', () => { + describe('RestLayoutService', () => { + let mock: MockAdapter; - before(() => { - mock = new MockAdapter(axios); - }); - - afterEach(() => { - mock.reset(); - }); - - after(() => { - mock.restore(); - }); + before(() => { + mock = new MockAdapter(axios); + }); - it('should fetch route data', () => { - mock.onGet().reply((config) => { - return [200, { ...config, data: { sitecore: { context: {}, route: { name: 'xxx' } } } }]; + afterEach(() => { + mock.reset(); }); - const service = new RestLayoutService({ - apiHost: 'http://sctest', - apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', - siteName: 'supersite', + after(() => { + mock.restore(); }); - return service.fetchLayoutData('/styleguide', 'en').then((layoutServiceData: any) => { - expect(layoutServiceData.url).to.equal( - 'http://sctest/sitecore/api/layout/render/jss?item=%2Fstyleguide&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=en&tracking=true' - ); - expect(layoutServiceData.data).to.deep.equal({ - sitecore: { - context: {}, - route: { name: 'xxx' }, - }, + it('should fetch route data', () => { + mock.onGet().reply((config) => { + return [200, { ...config, data: { sitecore: { context: {}, route: { name: 'xxx' } } } }]; + }); + + const service = new RestLayoutService({ + apiHost: 'http://sctest', + apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', + siteName: 'supersite', + }); + + return service.fetchLayoutData('/styleguide', 'en').then((layoutServiceData: any) => { + expect(layoutServiceData.url).to.equal( + 'http://sctest/sitecore/api/layout/render/jss?item=%2Fstyleguide&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=en&tracking=true' + ); + expect(layoutServiceData.data).to.deep.equal({ + sitecore: { + context: {}, + route: { name: 'xxx' }, + }, + }); }); }); - }); - it('should fetch layout data and invoke callbacks', () => { - mock.onGet().reply((config) => { - return [ - 200, - { - ...config, - data: { sitecore: { context: {}, route: { name: 'xxx' } } }, + it('should fetch layout data and invoke callbacks', () => { + mock.onGet().reply((config) => { + return [ + 200, + { + ...config, + data: { sitecore: { context: {}, route: { name: 'xxx' } } }, + }, + { + 'set-cookie': 'test-set-cookie-value', + }, + ]; + }); + + const req = { + connection: { + remoteAddress: '192.168.1.10', }, - { - 'set-cookie': 'test-set-cookie-value', + headers: { + cookie: 'test-cookie-value', + referer: 'http://sctest', + 'user-agent': 'test-user-agent-value', }, - ]; - }); + } as IncomingMessage; - const req = { - connection: { - remoteAddress: '192.168.1.10', - }, - headers: { - cookie: 'test-cookie-value', - referer: 'http://sctest', - 'user-agent': 'test-user-agent-value', - }, - } as IncomingMessage; - - const setHeaderSpy: any = spy(); - - const res = { - setHeader: setHeaderSpy, - } as ServerResponse; - - const service = new RestLayoutService({ - apiHost: 'http://sctest', - apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', - siteName: 'supersite', - tracking: false, - }); + const setHeaderSpy: any = spy(); - return service.fetchLayoutData('/home', 'da-DK', req, res).then((layoutServiceData: any) => { - expect(layoutServiceData.headers.cookie).to.equal('test-cookie-value'); - expect(layoutServiceData.headers.referer).to.equal('http://sctest'); - expect(layoutServiceData.headers['user-agent']).to.equal('test-user-agent-value'); - expect(layoutServiceData.headers['X-Forwarded-For']).to.equal('192.168.1.10'); + const res = { + setHeader: setHeaderSpy, + } as ServerResponse; - expect(layoutServiceData.url).to.equal( - 'http://sctest/sitecore/api/layout/render/jss?item=%2Fhome&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=false' - ); - expect(layoutServiceData.data).to.deep.equal({ - sitecore: { - context: {}, - route: { name: 'xxx' }, - }, + const service = new RestLayoutService({ + apiHost: 'http://sctest', + apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', + siteName: 'supersite', + tracking: false, }); - expect(setHeaderSpy).to.be.called.with('set-cookie', 'test-set-cookie-value'); - }); - }); - it('should fetch layout data', () => { - mock.onGet().reply((config) => { - return [ - 200, - { - ...config, - data: { sitecore: { context: {}, route: { name: 'xxx' } } }, - }, - { - 'set-cookie': 'test-set-cookie-value', - }, - ]; - }); + return service.fetchLayoutData('/home', 'da-DK', req, res).then((layoutServiceData: any) => { + expect(layoutServiceData.headers.cookie).to.equal('test-cookie-value'); + expect(layoutServiceData.headers.referer).to.equal('http://sctest'); + expect(layoutServiceData.headers['user-agent']).to.equal('test-user-agent-value'); + expect(layoutServiceData.headers['X-Forwarded-For']).to.equal('192.168.1.10'); - const req = { - connection: { - remoteAddress: '192.168.1.10', - }, - headers: { - cookie: 'test-cookie-value', - referer: 'http://sctest', - 'user-agent': 'test-user-agent-value', - }, - } as IncomingMessage; - - const setHeaderSpy: any = spy(); - - const res = { - setHeader: setHeaderSpy, - } as ServerResponse; - - const service = new RestLayoutService({ - apiHost: 'http://sctest', - apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', - siteName: 'supersite', - tracking: false, + expect(layoutServiceData.url).to.equal( + 'http://sctest/sitecore/api/layout/render/jss?item=%2Fhome&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=false' + ); + expect(layoutServiceData.data).to.deep.equal({ + sitecore: { + context: {}, + route: { name: 'xxx' }, + }, + }); + expect(setHeaderSpy).to.be.called.with('set-cookie', 'test-set-cookie-value'); + }); }); - return service.fetchLayoutData('/home', 'da-DK', req, res).then((layoutServiceData: any) => { - expect(layoutServiceData.headers.cookie).to.equal('test-cookie-value'); - expect(layoutServiceData.headers.referer).to.equal('http://sctest'); - expect(layoutServiceData.headers['user-agent']).to.equal('test-user-agent-value'); - expect(layoutServiceData.headers['X-Forwarded-For']).to.equal('192.168.1.10'); + it('should fetch layout data', () => { + mock.onGet().reply((config) => { + return [ + 200, + { + ...config, + data: { sitecore: { context: {}, route: { name: 'xxx' } } }, + }, + { + 'set-cookie': 'test-set-cookie-value', + }, + ]; + }); - expect(layoutServiceData.url).to.equal( - 'http://sctest/sitecore/api/layout/render/jss?item=%2Fhome&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=false' - ); - expect(layoutServiceData.data).to.deep.equal({ - sitecore: { - context: {}, - route: { name: 'xxx' }, + const req = { + connection: { + remoteAddress: '192.168.1.10', }, - }); - expect(setHeaderSpy).to.be.called.with('set-cookie', 'test-set-cookie-value'); - }); - }); + headers: { + cookie: 'test-cookie-value', + referer: 'http://sctest', + 'user-agent': 'test-user-agent-value', + }, + } as IncomingMessage; - it('should fetch layout data using custom fetcher resolver', () => { - const fetcherSpy = spy((url: string) => { - return new AxiosDataFetcher().fetch(url); - }); + const setHeaderSpy: any = spy(); - mock.onGet().reply(() => { - return [200, { sitecore: { context: {}, route: { name: 'xxx' } } }]; - }); + const res = { + setHeader: setHeaderSpy, + } as ServerResponse; - const service = new RestLayoutService({ - apiHost: 'http://sctest', - apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', - siteName: 'supersite', - dataFetcherResolver: () => fetcherSpy, - }); + const service = new RestLayoutService({ + apiHost: 'http://sctest', + apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', + siteName: 'supersite', + tracking: false, + }); + + return service.fetchLayoutData('/home', 'da-DK', req, res).then((layoutServiceData: any) => { + expect(layoutServiceData.headers.cookie).to.equal('test-cookie-value'); + expect(layoutServiceData.headers.referer).to.equal('http://sctest'); + expect(layoutServiceData.headers['user-agent']).to.equal('test-user-agent-value'); + expect(layoutServiceData.headers['X-Forwarded-For']).to.equal('192.168.1.10'); - return service - .fetchLayoutData('/home', 'da-DK') - .then((layoutServiceData: LayoutServiceData) => { - expect(layoutServiceData).to.deep.equal({ + expect(layoutServiceData.url).to.equal( + 'http://sctest/sitecore/api/layout/render/jss?item=%2Fhome&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=false' + ); + expect(layoutServiceData.data).to.deep.equal({ sitecore: { context: {}, route: { name: 'xxx' }, }, }); + expect(setHeaderSpy).to.be.called.with('set-cookie', 'test-set-cookie-value'); + }); + }); - expect(fetcherSpy).to.be.called.once; - expect(fetcherSpy).to.be.called.with( - 'http://sctest/sitecore/api/layout/render/jss?item=%2Fhome&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=true' - ); + it('should fetch layout data using custom fetcher resolver', () => { + const fetcherSpy = spy((url: string) => { + return new AxiosDataFetcher().fetch(url); + }); + + mock.onGet().reply(() => { + return [200, { sitecore: { context: {}, route: { name: 'xxx' } } }]; + }); + + const service = new RestLayoutService({ + apiHost: 'http://sctest', + apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', + siteName: 'supersite', + dataFetcherResolver: () => fetcherSpy, + }); + + return service + .fetchLayoutData('/home', 'da-DK') + .then((layoutServiceData: LayoutServiceData) => { + expect(layoutServiceData).to.deep.equal({ + sitecore: { + context: {}, + route: { name: 'xxx' }, + }, + }); + + expect(fetcherSpy).to.be.called.once; + expect(fetcherSpy).to.be.called.with( + 'http://sctest/sitecore/api/layout/render/jss?item=%2Fhome&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=true' + ); + }); + }); + + it('should fetch placeholder data', () => { + mock.onGet().reply(() => { + return [ + 200, + { + name: 'x1', + path: 'x1/x2', + elements: [], + }, + { + 'set-cookie': 'test-set-cookie-value', + }, + ]; }); - }); - it('should fetch placeholder data', () => { - mock.onGet().reply(() => { - return [ - 200, - { - name: 'x1', - path: 'x1/x2', - elements: [], + const req = { + connection: { + remoteAddress: '192.168.1.10', }, - { - 'set-cookie': 'test-set-cookie-value', + headers: { + cookie: 'test-cookie-value', + referer: 'http://sctest', + 'user-agent': 'test-user-agent-value', }, - ]; - }); + } as IncomingMessage; - const req = { - connection: { - remoteAddress: '192.168.1.10', - }, - headers: { - cookie: 'test-cookie-value', - referer: 'http://sctest', - 'user-agent': 'test-user-agent-value', - }, - } as IncomingMessage; - - const setHeaderSpy: any = spy(); - - const res = { - setHeader: setHeaderSpy, - } as ServerResponse; - - const service = new RestLayoutService({ - apiHost: 'http://sctest', - apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', - siteName: 'supersite', - tracking: false, - }); + const setHeaderSpy: any = spy(); - return service - .fetchPlaceholderData('superPh', '/xxx', 'da-DK', req, res) - .then((placeholderData: PlaceholderData) => { - expect(placeholderData).to.deep.equal({ - name: 'x1', - path: 'x1/x2', - elements: [], + const res = { + setHeader: setHeaderSpy, + } as ServerResponse; + + const service = new RestLayoutService({ + apiHost: 'http://sctest', + apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', + siteName: 'supersite', + tracking: false, + }); + + return service + .fetchPlaceholderData('superPh', '/xxx', 'da-DK', req, res) + .then((placeholderData: PlaceholderData) => { + expect(placeholderData).to.deep.equal({ + name: 'x1', + path: 'x1/x2', + elements: [], + }); + expect(setHeaderSpy).to.be.called.with('set-cookie', 'test-set-cookie-value'); }); - expect(setHeaderSpy).to.be.called.with('set-cookie', 'test-set-cookie-value'); + }); + + it('should fetch placeholder data using custom fetcher resolver', () => { + const fetcherSpy = spy((url: string) => { + return new AxiosDataFetcher().fetch(url); + }); + + mock.onGet().reply(() => { + return [ + 200, + { + name: 'x1', + path: 'x1/x2', + elements: [], + }, + ]; }); + + const service = new RestLayoutService({ + apiHost: 'http://sctest', + apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', + siteName: 'supersite', + dataFetcherResolver: () => fetcherSpy, + }); + + return service + .fetchPlaceholderData('superPh', '/xxx', 'da-DK') + .then((placeholderData: PlaceholderData) => { + expect(placeholderData).to.deep.equal({ + name: 'x1', + path: 'x1/x2', + elements: [], + }); + + expect(fetcherSpy).to.be.called.once; + expect(fetcherSpy).to.be.called.with( + 'http://sctest/sitecore/api/layout/placeholder/jss?placeholderName=superPh&item=%2Fxxx&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=true' + ); + }); + }); }); - it('should fetch placeholder data using custom fetcher resolver', () => { - const fetcherSpy = spy((url: string) => { - return new AxiosDataFetcher().fetch(url); + describe('GraphQLLayoutService', () => { + beforeEach(() => { + nock.cleanAll(); }); - mock.onGet().reply(() => { - return [ - 200, - { - name: 'x1', - path: 'x1/x2', - elements: [], + it('should fetch layout data', async () => { + nock('http://sctest') + .post('/graphql', (body) => { + return ( + body.query.replace(/\n|\s/g, '') === + 'query{layout(site:"supersite",routePath:"/styleguide",language:"da-DK"){rendered}}' + ); + }) + .reply(200, { + data: { + layout: { + rendered: { + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }, + }, + }, + }); + + const service = new GraphQLLayoutService({ + apiHost: 'http://sctest/graphql', + siteName: 'supersite', + }); + + 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', + }, }, - ]; + }); }); - const service = new RestLayoutService({ - apiHost: 'http://sctest', - apiKey: '0FBFF61E-267A-43E3-9252-B77E71CEE4BA', - siteName: 'supersite', - dataFetcherResolver: () => fetcherSpy, + it('should fetch layout data if locale is not provided', async () => { + nock('http://sctest') + .post('/graphql', (body) => { + return ( + body.query.replace(/\n|\s/g, '') === + 'query{layout(site:"supersite",routePath:"/styleguide"){rendered}}' + ); + }) + .reply(200, { + data: { + layout: { + rendered: { + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }, + }, + }, + }); + + const service = new GraphQLLayoutService({ + apiHost: 'http://sctest/graphql', + siteName: 'supersite', + }); + + const data = await service.fetchLayoutData('/styleguide'); + + expect(data).to.deep.equal({ + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }); }); - return service - .fetchPlaceholderData('superPh', '/xxx', 'da-DK') - .then((placeholderData: PlaceholderData) => { - expect(placeholderData).to.deep.equal({ - name: 'x1', - path: 'x1/x2', - elements: [], + it('should return error', async () => { + nock('http://sctest') + .post('/graphql') + .reply(401, { + error: 'whoops', }); - expect(fetcherSpy).to.be.called.once; - expect(fetcherSpy).to.be.called.with( - 'http://sctest/sitecore/api/layout/placeholder/jss?placeholderName=superPh&item=%2Fxxx&sc_apikey=0FBFF61E-267A-43E3-9252-B77E71CEE4BA&sc_site=supersite&sc_lang=da-DK&tracking=true' - ); + const service = new GraphQLLayoutService({ + apiHost: 'http://sctest/graphql', + siteName: 'supersite', }); + + await service.fetchLayoutData('/styleguide', 'da-DK').catch((error) => { + expect(error.response.status).to.equal(401); + expect(error.response.error).to.equal('whoops'); + }); + }); }); }); diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index a98f883aea..40cfc045db 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -1,9 +1,10 @@ +import { AxiosRequestConfig, AxiosResponse } from 'axios'; +import { IncomingMessage, ServerResponse } from 'http'; import { AxiosDataFetcher, AxiosDataFetcherConfig } from './data-fetcher'; import { LayoutServiceData, PlaceholderData } from './dataModels'; import { fetchPlaceholderData, fetchRouteData, LayoutServiceConfig } from './dataApi'; import { HttpJsonFetcher } from './httpClientInterface'; -import { AxiosRequestConfig, AxiosResponse } from 'axios'; -import { IncomingMessage, ServerResponse } from 'http'; +import { GraphQLRequestClient } from './graphql-request-client'; export interface LayoutService { /** @@ -58,6 +59,17 @@ export type RestLayoutServiceConfig = { dataFetcherResolver?: DataFetcherResolver; }; +export type GraphQLLayoutServiceConfig = { + /** + * Your Graphql endpoint + */ + apiHost: string; + /** + * The JSS application name + */ + siteName: string; +}; + interface FetchParams { [param: string]: string | number | boolean; sc_apikey: string; @@ -201,3 +213,50 @@ export class RestLayoutService implements LayoutService { }; } } + +/** + * Fetch layout data using the Sitecore Layout Service GraphQL endpoint. + */ +export class GraphQLLayoutService implements LayoutService { + constructor(private serviceConfig: GraphQLLayoutServiceConfig) {} + + /** + * Fetch layout data for an item. + * @param {string} itemPath + * @param {string} [language] + * @returns {Promise} layout service data + */ + async fetchLayoutData(itemPath: string, language?: string): Promise { + const query = this.getLayoutQuery(itemPath, language); + + const data = await this.createClient().request<{ layout: { rendered: LayoutServiceData } }>( + query + ); + + return data.layout.rendered; + } + + /** + * Returns new graphql client instance + */ + private createClient(): GraphQLRequestClient { + const { apiHost } = this.serviceConfig; + + return new GraphQLRequestClient(apiHost); + } + + /** + * Returns GraphQL Layout query + * @param {string} itemPath page route + * @param {string} [language] language + */ + private getLayoutQuery(itemPath: string, language?: string) { + const languageVariable = language ? `, language:"${language}"` : ''; + + return `query { + layout(site:"${this.serviceConfig.siteName}", routePath:"${itemPath}"${languageVariable}) { + rendered + } + }`; + } +} From cbe011fc56b73764c9e37860dcbb5b3fcf5fe277 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 3 Mar 2021 09:33:01 +0200 Subject: [PATCH 2/8] Change jsdoc --- packages/sitecore-jss/src/layout-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index 40cfc045db..dd72012075 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -215,7 +215,7 @@ export class RestLayoutService implements LayoutService { } /** - * Fetch layout data using the Sitecore Layout Service GraphQL endpoint. + * Fetch layout data using the Sitecore GraphQL endpoint. */ export class GraphQLLayoutService implements LayoutService { constructor(private serviceConfig: GraphQLLayoutServiceConfig) {} From 3313bbb1298627e0d40f4ace920646347d3254cb Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 4 Mar 2021 11:08:32 +0200 Subject: [PATCH 3/8] add changes --- packages/sitecore-jss/src/layout-service.test.ts | 6 +++--- packages/sitecore-jss/src/layout-service.ts | 13 +++++++------ 2 files changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/sitecore-jss/src/layout-service.test.ts b/packages/sitecore-jss/src/layout-service.test.ts index 8945dcf7f1..060dbc2ded 100644 --- a/packages/sitecore-jss/src/layout-service.test.ts +++ b/packages/sitecore-jss/src/layout-service.test.ts @@ -324,7 +324,7 @@ describe('LayoutService', () => { }); const service = new GraphQLLayoutService({ - apiHost: 'http://sctest/graphql', + endpoint: 'http://sctest/graphql', siteName: 'supersite', }); @@ -372,7 +372,7 @@ describe('LayoutService', () => { }); const service = new GraphQLLayoutService({ - apiHost: 'http://sctest/graphql', + endpoint: 'http://sctest/graphql', siteName: 'supersite', }); @@ -400,7 +400,7 @@ describe('LayoutService', () => { }); const service = new GraphQLLayoutService({ - apiHost: 'http://sctest/graphql', + endpoint: 'http://sctest/graphql', siteName: 'supersite', }); diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index dd72012075..97203fdf80 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -63,7 +63,7 @@ export type GraphQLLayoutServiceConfig = { /** * Your Graphql endpoint */ - apiHost: string; + endpoint: string; /** * The JSS application name */ @@ -214,10 +214,11 @@ export class RestLayoutService implements LayoutService { } } -/** - * Fetch layout data using the Sitecore GraphQL endpoint. - */ export class GraphQLLayoutService implements LayoutService { + /** + * Fetch layout data using the Sitecore GraphQL endpoint. + * @param {GraphQLLayoutServiceConfig} serviceConfig + */ constructor(private serviceConfig: GraphQLLayoutServiceConfig) {} /** @@ -240,9 +241,9 @@ export class GraphQLLayoutService implements LayoutService { * Returns new graphql client instance */ private createClient(): GraphQLRequestClient { - const { apiHost } = this.serviceConfig; + const { endpoint } = this.serviceConfig; - return new GraphQLRequestClient(apiHost); + return new GraphQLRequestClient(endpoint); } /** From 2dcafcd2929ae757734aac472a7f52f00fbbd455 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Thu, 4 Mar 2021 12:47:53 +0200 Subject: [PATCH 4/8] Use latest schema --- .../sitecore-jss/src/layout-service.test.ts | 44 ++++++++++--------- packages/sitecore-jss/src/layout-service.ts | 12 ++--- 2 files changed, 31 insertions(+), 25 deletions(-) diff --git a/packages/sitecore-jss/src/layout-service.test.ts b/packages/sitecore-jss/src/layout-service.test.ts index 060dbc2ded..25fde0784a 100644 --- a/packages/sitecore-jss/src/layout-service.test.ts +++ b/packages/sitecore-jss/src/layout-service.test.ts @@ -301,21 +301,23 @@ describe('LayoutService', () => { .post('/graphql', (body) => { return ( body.query.replace(/\n|\s/g, '') === - 'query{layout(site:"supersite",routePath:"/styleguide",language:"da-DK"){rendered}}' + 'query{layout(site:"supersite",routePath:"/styleguide",language:"da-DK"){item{rendered}}}' ); }) .reply(200, { data: { layout: { - rendered: { - sitecore: { - context: { - pageEditing: false, - site: { name: 'JssNextWeb' }, - }, - route: { - name: 'styleguide', - layoutId: 'xxx', + item: { + rendered: { + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, }, }, }, @@ -349,21 +351,23 @@ describe('LayoutService', () => { .post('/graphql', (body) => { return ( body.query.replace(/\n|\s/g, '') === - 'query{layout(site:"supersite",routePath:"/styleguide"){rendered}}' + 'query{layout(site:"supersite",routePath:"/styleguide"){item{rendered}}}' ); }) .reply(200, { data: { layout: { - rendered: { - sitecore: { - context: { - pageEditing: false, - site: { name: 'JssNextWeb' }, - }, - route: { - name: 'styleguide', - layoutId: 'xxx', + item: { + rendered: { + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, }, }, }, diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index 97203fdf80..e00ff43907 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -230,11 +230,11 @@ export class GraphQLLayoutService implements LayoutService { async fetchLayoutData(itemPath: string, language?: string): Promise { const query = this.getLayoutQuery(itemPath, language); - const data = await this.createClient().request<{ layout: { rendered: LayoutServiceData } }>( - query - ); + const data = await this.createClient().request<{ + layout: { item: { rendered: LayoutServiceData } }; + }>(query); - return data.layout.rendered; + return data.layout.item.rendered; } /** @@ -256,7 +256,9 @@ export class GraphQLLayoutService implements LayoutService { return `query { layout(site:"${this.serviceConfig.siteName}", routePath:"${itemPath}"${languageVariable}) { - rendered + item { + rendered + } } }`; } From fc07076fabbdfa6f11a34c3db0fb36471e1e47f9 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Tue, 9 Mar 2021 11:08:23 +0200 Subject: [PATCH 5/8] Optional cahining for `data` --- packages/sitecore-jss/src/layout-service.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index e00ff43907..710b1e38c1 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -234,7 +234,7 @@ export class GraphQLLayoutService implements LayoutService { layout: { item: { rendered: LayoutServiceData } }; }>(query); - return data.layout.item.rendered; + return data?.layout.item.rendered; } /** From d9bb79c2911426852efe9157a73540ebac6e85dc Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Tue, 9 Mar 2021 15:44:24 +0200 Subject: [PATCH 6/8] Add ability to use custom search query --- .../sitecore-jss/src/layout-service.test.ts | 53 +++++++++++++++++++ packages/sitecore-jss/src/layout-service.ts | 13 ++++- 2 files changed, 65 insertions(+), 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout-service.test.ts b/packages/sitecore-jss/src/layout-service.test.ts index 25fde0784a..eb7d6a1ba7 100644 --- a/packages/sitecore-jss/src/layout-service.test.ts +++ b/packages/sitecore-jss/src/layout-service.test.ts @@ -396,6 +396,59 @@ describe('LayoutService', () => { }); }); + it('should fetch layout data using custom search query', async () => { + nock('http://sctest') + .post('/graphql', (body) => { + return ( + body.query.replace(/\n|\s/g, '') === + 'query{layout111(site:"supersite",routePath:"/styleguide",language:"en"){item{rendered}}}' + ); + }) + .reply(200, { + data: { + layout: { + item: { + rendered: { + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }, + }, + }, + }, + }); + + const service = new GraphQLLayoutService({ + endpoint: 'http://sctest/graphql', + siteName: 'supersite', + formatSearchQuery: (siteName, itemPath, locale) => + `query{layout111(site:"${siteName}",routePath:"${itemPath}",language:"${locale || + 'en'}"){item{rendered}}}`, + }); + + const data = await service.fetchLayoutData('/styleguide'); + + expect(data).to.deep.equal({ + sitecore: { + context: { + pageEditing: false, + site: { name: 'JssNextWeb' }, + }, + route: { + name: 'styleguide', + layoutId: 'xxx', + }, + }, + }); + }); + it('should return error', async () => { nock('http://sctest') .post('/graphql') diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index 710b1e38c1..fd9c950aa7 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -68,6 +68,14 @@ export type GraphQLLayoutServiceConfig = { * The JSS application name */ siteName: string; + /** + * Override default search query + * @param {string} siteName + * @param {string} itemPath + * @param {string} [locale] + * @returns {string} custom search query + */ + formatSearchQuery?: (siteName: string, itemPath: string, locale?: string) => string; }; interface FetchParams { @@ -225,10 +233,13 @@ export class GraphQLLayoutService implements LayoutService { * Fetch layout data for an item. * @param {string} itemPath * @param {string} [language] + * @param {string} [searchQuery] custom search query * @returns {Promise} layout service data */ async fetchLayoutData(itemPath: string, language?: string): Promise { - const query = this.getLayoutQuery(itemPath, language); + const query = this.serviceConfig.formatSearchQuery + ? this.serviceConfig.formatSearchQuery(this.serviceConfig.siteName, itemPath, language) + : this.getLayoutQuery(itemPath, language); const data = await this.createClient().request<{ layout: { item: { rendered: LayoutServiceData } }; From 5c0926127ccfaef301bc0f455e1235f6cb08de8b Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Tue, 9 Mar 2021 15:45:05 +0200 Subject: [PATCH 7/8] remove param --- packages/sitecore-jss/src/layout-service.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index fd9c950aa7..072f810708 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -233,7 +233,6 @@ export class GraphQLLayoutService implements LayoutService { * Fetch layout data for an item. * @param {string} itemPath * @param {string} [language] - * @param {string} [searchQuery] custom search query * @returns {Promise} layout service data */ async fetchLayoutData(itemPath: string, language?: string): Promise { From 587220a5c8093666311449508ec00d90a4208851 Mon Sep 17 00:00:00 2001 From: illiakovalenko Date: Wed, 10 Mar 2021 11:44:21 +0200 Subject: [PATCH 8/8] Provide ability to format only query arguments instead of modifying response data --- .../sitecore-jss/src/layout-service.test.ts | 9 ++++----- packages/sitecore-jss/src/layout-service.ts | 20 ++++++++++++------- 2 files changed, 17 insertions(+), 12 deletions(-) diff --git a/packages/sitecore-jss/src/layout-service.test.ts b/packages/sitecore-jss/src/layout-service.test.ts index eb7d6a1ba7..0cf54a5c50 100644 --- a/packages/sitecore-jss/src/layout-service.test.ts +++ b/packages/sitecore-jss/src/layout-service.test.ts @@ -396,12 +396,12 @@ describe('LayoutService', () => { }); }); - it('should fetch layout data using custom search query', async () => { + it('should fetch layout data using custom layout query', async () => { nock('http://sctest') .post('/graphql', (body) => { return ( body.query.replace(/\n|\s/g, '') === - 'query{layout111(site:"supersite",routePath:"/styleguide",language:"en"){item{rendered}}}' + 'query{layout111(site:"supersite",route:"/styleguide",language:"en"){item{rendered}}}' ); }) .reply(200, { @@ -428,9 +428,8 @@ describe('LayoutService', () => { const service = new GraphQLLayoutService({ endpoint: 'http://sctest/graphql', siteName: 'supersite', - formatSearchQuery: (siteName, itemPath, locale) => - `query{layout111(site:"${siteName}",routePath:"${itemPath}",language:"${locale || - 'en'}"){item{rendered}}}`, + formatLayoutQuery: (siteName, itemPath, locale) => + `layout111(site:"${siteName}",route:"${itemPath}",language:"${locale || 'en'}")`, }); const data = await service.fetchLayoutData('/styleguide'); diff --git a/packages/sitecore-jss/src/layout-service.ts b/packages/sitecore-jss/src/layout-service.ts index 072f810708..c76d0af7ba 100644 --- a/packages/sitecore-jss/src/layout-service.ts +++ b/packages/sitecore-jss/src/layout-service.ts @@ -69,13 +69,17 @@ export type GraphQLLayoutServiceConfig = { */ siteName: string; /** - * Override default search query + * Override default layout query * @param {string} siteName * @param {string} itemPath * @param {string} [locale] - * @returns {string} custom search query + * @returns {string} custom layout query + * + * @default + * Layout query + * layout(site:"${siteName}", routePath:"${itemPath}", language:"${language}") */ - formatSearchQuery?: (siteName: string, itemPath: string, locale?: string) => string; + formatLayoutQuery?: (siteName: string, itemPath: string, locale?: string) => string; }; interface FetchParams { @@ -236,9 +240,7 @@ export class GraphQLLayoutService implements LayoutService { * @returns {Promise} layout service data */ async fetchLayoutData(itemPath: string, language?: string): Promise { - const query = this.serviceConfig.formatSearchQuery - ? this.serviceConfig.formatSearchQuery(this.serviceConfig.siteName, itemPath, language) - : this.getLayoutQuery(itemPath, language); + const query = this.getLayoutQuery(itemPath, language); const data = await this.createClient().request<{ layout: { item: { rendered: LayoutServiceData } }; @@ -264,8 +266,12 @@ export class GraphQLLayoutService implements LayoutService { private getLayoutQuery(itemPath: string, language?: string) { const languageVariable = language ? `, language:"${language}"` : ''; + const layoutQuery = this.serviceConfig.formatLayoutQuery + ? this.serviceConfig.formatLayoutQuery(this.serviceConfig.siteName, itemPath, language) + : `layout(site:"${this.serviceConfig.siteName}", routePath:"${itemPath}"${languageVariable})`; + return `query { - layout(site:"${this.serviceConfig.siteName}", routePath:"${itemPath}"${languageVariable}) { + ${layoutQuery}{ item { rendered }