From dcf79b3213d6825d576ab4ce5b320ac7c1848536 Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Wed, 27 Apr 2022 20:58:31 -0400 Subject: [PATCH 1/9] Changing sitemap GraphQL query from 'search' to 'site' plus adding a base GQL service to use with site and search services ("DI-ing" graphql services in sitecore-jss) --- .../services/disconnected-sitemap-service.ts | 4 +- .../src/services/graphql-sitemap-service.ts | 86 ++++++++--------- .../src/graphql/base-query-service.ts | 29 ++++++ packages/sitecore-jss/src/graphql/index.ts | 1 + .../src/graphql/search-service.ts | 21 +--- .../src/graphql/site-query-service.ts | 96 +++++++++++++++++++ 6 files changed, 174 insertions(+), 63 deletions(-) create mode 100644 packages/sitecore-jss/src/graphql/base-query-service.ts create mode 100644 packages/sitecore-jss/src/graphql/site-query-service.ts diff --git a/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts index 8c3cccc97d..1146aa0678 100644 --- a/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts @@ -17,7 +17,7 @@ export class DisconnectedSitemapService { fetchExportSitemap(): StaticPath[] { const sitemap: { params: { - path: string[]; + routePath: string[]; }; }[] = []; @@ -30,7 +30,7 @@ export class DisconnectedSitemapService { if (renderings && renderings.length) { sitemap.push({ params: { - path: routePath, + routePath: routePath, }, }); } diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 5461f9af21..0a612a3819 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -3,7 +3,7 @@ import { GraphQLRequestClient, getAppRootId, SearchServiceConfig, - SearchQueryService, + SiteQueryService, } from '@sitecore-jss/sitecore-jss/graphql'; import { debug } from '@sitecore-jss/sitecore-jss'; @@ -16,49 +16,46 @@ export const languageError = 'The list of languages cannot be empty'; // Even though _hasLayout should always be "true" in this query, using a variable is necessary for compatibility with Edge const defaultQuery = /* GraphQL */ ` - query SitemapQuery( - $rootItemId: String! - $language: String! - $pageSize: Int = 10 - $hasLayout: String = "true" - $after: String - ) { - search( - where: { - AND: [ - { name: "_path", value: $rootItemId, operator: CONTAINS } - { name: "_language", value: $language } - { name: "_hasLayout", value: $hasLayout } - ] - } - first: $pageSize - after: $after - ) { - total - pageInfo { - endCursor - hasNext - } - results { - url { - path +query Sitemap( + $siteName: String!, + $language: String!, + $pageSize: Int = 10, + $after: String +) { + site { + siteInfo(site: $siteName) { + routes( + language: $language + first: $pageSize + after: $after + ){ + total + pageInfo { + endCursor + hasNext + } + routesResult{ + routePath } } } } +} `; /** - * The schema of data returned in response to a page list query request + * The schema of data returned in response to a pages list query request */ -export type PageListQueryResult = { url: { path: string } }; +export type PageListQueryResult = { + routePath: string +}; /** * Object model of a site page item. */ export type StaticPath = { params: { - path: string[]; + routePath: string[]; }; locale?: string; }; @@ -91,7 +88,7 @@ export interface GraphQLSitemapServiceConfig extends SearchServiceConfig { */ export class GraphQLSitemapService { private graphQLClient: GraphQLClient; - private searchService: SearchQueryService; + private siteService: SiteQueryService; /** * Gets the default query used for fetching the list of site pages @@ -106,7 +103,7 @@ export class GraphQLSitemapService { */ constructor(public options: GraphQLSitemapServiceConfig) { this.graphQLClient = this.getGraphQLClient(); - this.searchService = new SearchQueryService(this.graphQLClient); + this.siteService = new SiteQueryService(this.graphQLClient); } /** @@ -117,9 +114,9 @@ export class GraphQLSitemapService { * @returns an array of @see StaticPath objects */ async fetchExportSitemap(locale: string): Promise { - const formatPath = (path: string[]) => ({ + const formatPath = (routePath: string[]) => ({ params: { - path, + routePath, }, }); @@ -132,9 +129,9 @@ export class GraphQLSitemapService { * @returns an array of @see StaticPath objects */ async fetchSSGSitemap(locales: string[]): Promise { - const formatPath = (path: string[], locale: string) => ({ + const formatPath = (routePath: string[], locale: string) => ({ params: { - path, + routePath, }, locale, }); @@ -160,7 +157,7 @@ export class GraphQLSitemapService { } // If the caller does not specify a root item ID, then we try to figure it out - const rootItemId = + const rootItemId = this.options.rootItemId || (await getAppRootId( this.graphQLClient, @@ -168,6 +165,7 @@ export class GraphQLSitemapService { languages[0], this.options.jssAppTemplateId )); + const siteName = this.options.siteName; if (!rootItemId) { throw new Error(queryError); @@ -177,15 +175,15 @@ export class GraphQLSitemapService { const paths = await Promise.all( languages.map((language) => { debug.sitemap('fetching sitemap data for %s', language); - return this.searchService + return this.siteService .fetch(this.query, { - rootItemId, + siteName, language, pageSize: this.options.pageSize, }) - .then((results) => { - return results.map((item) => - formatStaticPath(item.url.path.replace(/^\/|\/$/g, '').split('/'), language) + .then((routesResult) => { + return routesResult.map((item) => + formatStaticPath(item.routePath.replace(/^\/|\/$/g, '').split('/'), language) ); }); }) @@ -212,7 +210,7 @@ export class GraphQLSitemapService { * Gets a service that can perform GraphQL "search" queries to fetch @see PageListQueryResult * @returns {SearchQueryService} the search query service */ - protected getSearchService(): SearchQueryService { - return new SearchQueryService(this.graphQLClient); + protected getSearchService(): SiteQueryService { + return new SiteQueryService(this.graphQLClient); } } diff --git a/packages/sitecore-jss/src/graphql/base-query-service.ts b/packages/sitecore-jss/src/graphql/base-query-service.ts new file mode 100644 index 0000000000..4eaf64345e --- /dev/null +++ b/packages/sitecore-jss/src/graphql/base-query-service.ts @@ -0,0 +1,29 @@ +import { DocumentNode } from 'graphql'; +import { GraphQLClient } from '../graphql-request-client'; + +export interface BaseQueryVariables { + /** common variable for all GraphQL queries + * it will be used for every type of query to regulate result batch size + * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. + * @default 10 + */ + pageSize?: number; +} + +export abstract class BaseQueryService { + /** + * Creates an instance of search query service. + * @param {GraphQLClient} client that fetches data from a GraphQL endpoint. + */ + constructor(protected client: GraphQLClient) {} + + /** + * 1. Validates mandatory search query arguments + * 2. Executes search query with pagination + * 3. Aggregates pagination results into a single result-set. + * @template T The type of objects being requested. + * @param {string | DocumentNode} query the graph query. + * @param {BaseQueryVariables} args graph query arguments - should have derived types for different types of GraphQL queries. + * */ + abstract fetch(query: string | DocumentNode, args: BaseQueryVariables): Promise; +} diff --git a/packages/sitecore-jss/src/graphql/index.ts b/packages/sitecore-jss/src/graphql/index.ts index 46772b1e84..6cfe84bc54 100644 --- a/packages/sitecore-jss/src/graphql/index.ts +++ b/packages/sitecore-jss/src/graphql/index.ts @@ -10,3 +10,4 @@ export { SearchServiceConfig, SearchQueryService, } from './search-service'; +export { SiteQueryResult, SiteQueryService, SiteQueryVariables } from './site-query-service'; diff --git a/packages/sitecore-jss/src/graphql/search-service.ts b/packages/sitecore-jss/src/graphql/search-service.ts index db6c631f63..883db70902 100644 --- a/packages/sitecore-jss/src/graphql/search-service.ts +++ b/packages/sitecore-jss/src/graphql/search-service.ts @@ -1,6 +1,5 @@ -import { GraphQLClient } from './../graphql-request-client'; import { DocumentNode } from 'graphql'; - +import { BaseQueryService, BaseQueryVariables } from './base-query-service'; /** * Schema of data returned in response to a "search" query request * @template T The type of objects being requested. @@ -31,7 +30,7 @@ export type SearchQueryResult = { * Describes the variables used by the 'search' query. Language should always be specified. * The other predicates are optional. */ -export type SearchQueryVariables = { +export interface SearchQueryVariables extends BaseQueryVariables { /** * Required. The language versions to search for. Fetch pages that have versions in this language. */ @@ -42,17 +41,11 @@ export type SearchQueryVariables = { */ rootItemId?: string; - /** - * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. - * @default 10 - */ - pageSize?: number; - /** * Optional. Sitecore template ID(s). Fetch items that inherit from this template(s). */ templates?: string; -}; +} /** * Configuration options for service classes that extend @see SearchQueryService. @@ -75,13 +68,7 @@ export interface SearchServiceConfig extends Omit { - /** - * Creates an instance of search query service. - * @param {GraphQLClient} client that fetches data from a GraphQL endpoint. - */ - constructor(protected client: GraphQLClient) {} - +export class SearchQueryService extends BaseQueryService { /** * 1. Validates mandatory search query arguments * 2. Executes search query with pagination diff --git a/packages/sitecore-jss/src/graphql/site-query-service.ts b/packages/sitecore-jss/src/graphql/site-query-service.ts new file mode 100644 index 0000000000..18b5b91035 --- /dev/null +++ b/packages/sitecore-jss/src/graphql/site-query-service.ts @@ -0,0 +1,96 @@ +import { DocumentNode } from 'graphql'; +import { BaseQueryService, BaseQueryVariables } from './base-query-service'; +// TODO: rewrite site query results +// TODO: review-rewrite comments + +/** + * Schema of data returned in response to a "site" query request + * @template T The type of objects being requested. + */ +export type SiteQueryResult = { + site: { + siteInfo: { + /** + * Data needed to paginate the site results + */ + pageInfo: { + /** + * string token that can be used to fetch the next page of results + */ + endCursor: string; + /** + * a value that indicates whether more pages of results are available + */ + hasNext: boolean; + }; + /* + * the type of data querying about items matching the search criteria + */ + routesResult: T[]; + }; + }; +}; + +/** + * Describes the variables used by the 'search' query. Language should always be specified. + * The other predicates are optional. + */ +export declare interface SiteQueryVariables extends BaseQueryVariables { + /** + * Required. The name of the site being queried. + */ + siteName: string; + + language: string; + + includedPaths?: string[]; + + excludedPaths?: string[]; +} + +/** + * 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. + * @mixin + */ +export class SiteQueryService extends BaseQueryService { + /** + * Creates an instance of search query service. + * @param {GraphQLClient} client that fetches data from a GraphQL endpoint. + */ + + /** + * 1. Validates mandatory search query arguments + * 2. Executes search query with pagination + * 3. Aggregates pagination results into a single result-set. + * @template T The type of objects being requested. + * @param {string | DocumentNode} query the search query. + * @param {SiteQueryVariables} args search query arguments. + * @returns {T[]} array of result objects. + * @throws {RangeError} if a valid root item ID is not provided. + * @throws {RangeError} if the provided language(s) is(are) not valid. + */ + async fetch(query: string | DocumentNode, args: SiteQueryVariables): Promise { + if (!args.siteName) { + throw new RangeError('"siteName" must a be non-empty string'); + } + + let results: T[] = []; + let hasNext = true; + let after = ''; + + while (hasNext) { + const fetchResponse = await this.client.request>(query, { + ...args, + after, + }); + + results = results.concat(fetchResponse?.site?.siteInfo?.routesResult); + hasNext = fetchResponse.site.siteInfo.pageInfo.hasNext; + after = fetchResponse.site.siteInfo.pageInfo.endCursor; + } + + return results; + } +} From 9123e38ca4da3e8c5e9153a9be8364032bfa68be Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Wed, 27 Apr 2022 22:18:49 -0400 Subject: [PATCH 2/9] Addressing existing tests --- .../services/disconnected-sitemap-service.ts | 4 +- .../services/graphql-sitemap-service.test.ts | 108 ++++++++++-------- .../src/services/graphql-sitemap-service.ts | 24 ++-- .../src/testData/sitemapQueryResult.json | 50 ++++---- .../src/graphql/site-query-service.ts | 34 +++--- 5 files changed, 117 insertions(+), 103 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts index 1146aa0678..8c3cccc97d 100644 --- a/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/disconnected-sitemap-service.ts @@ -17,7 +17,7 @@ export class DisconnectedSitemapService { fetchExportSitemap(): StaticPath[] { const sitemap: { params: { - routePath: string[]; + path: string[]; }; }[] = []; @@ -30,7 +30,7 @@ export class DisconnectedSitemapService { if (renderings && renderings.length) { sitemap.push({ params: { - routePath: routePath, + path: routePath, }, }); } diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 3abccb0142..694cf293f9 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -67,7 +67,7 @@ describe('GraphQLSitemapService', () => { : { data: { search: { - results, + results }, }, } @@ -82,16 +82,20 @@ describe('GraphQLSitemapService', () => { results === undefined ? sitemapQueryResult : { - data: { - search: { - total: results.length, - pageInfo: { - hasNext: false, - }, - results, + data: { + site: { + siteInfo:{ + routes:{ + total: results.length, + pageInfo: { + hasNext: false, + }, + results, + } }, }, } + } ); }; @@ -116,25 +120,29 @@ describe('GraphQLSitemapService', () => { }) .reply(200, { data: { - search: { - total: 4, - pageInfo: { - hasNext: false, - }, - results: [ - { - url: { path: '/' }, - }, - { - url: { path: '/x1' }, - }, - { - url: { path: '/y1/y2/y3/y4' }, - }, - { - url: { path: '/y1/y2' }, - }, - ], + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + } + } }, }, }); @@ -145,26 +153,30 @@ describe('GraphQLSitemapService', () => { }) .reply(200, { data: { - search: { - total: 4, - pageInfo: { - hasNext: false, + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1-da-DK', + }, + { + path: '/y1/y2/y3/y4-da-DK', + }, + { + path: '/y1/y2-da-DK', + }, + ], + } + } }, - results: [ - { - url: { path: '/' }, - }, - { - url: { path: '/x1-da-DK' }, - }, - { - url: { path: '/y1/y2/y3/y4-da-DK' }, - }, - { - url: { path: '/y1/y2-da-DK' }, - }, - ], - }, }, }); @@ -283,6 +295,7 @@ describe('GraphQLSitemapService', () => { return expect(nock.isDone()).to.be.true; }); + /* TODO: cleanup these tests or replace it('should attempt to fetch the rootItemId, if rootItemId not provided', async () => { mockRootItemIdRequest(); @@ -318,6 +331,8 @@ describe('GraphQLSitemapService', () => { expect(sitemap).to.deep.equal(sitemapServiceResult); }); + + it('should use a jssTemplateId, if provided', async () => { const jssAppTemplateId = '{de397294-cfcc-4795-847e-442416d0617b}'; const randomId = '{5a4e6edc-4518-4afb-afdc-9fa22ec4eb91}'; @@ -352,6 +367,7 @@ describe('GraphQLSitemapService', () => { const sitemap = await service.fetchSSGSitemap(['ua']); expect(sitemap).to.deep.equal(sitemapServiceResult); }); + */ it('should throw error if SitemapQuery fails', async () => { mockRootItemIdRequest(); diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 0a612a3819..97f10dfc19 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -16,7 +16,7 @@ export const languageError = 'The list of languages cannot be empty'; // Even though _hasLayout should always be "true" in this query, using a variable is necessary for compatibility with Edge const defaultQuery = /* GraphQL */ ` -query Sitemap( +query SitemapQuery( $siteName: String!, $language: String!, $pageSize: Int = 10, @@ -34,8 +34,8 @@ query Sitemap( endCursor hasNext } - routesResult{ - routePath + results: routesResult{ + path: routePath } } } @@ -47,7 +47,7 @@ query Sitemap( * The schema of data returned in response to a pages list query request */ export type PageListQueryResult = { - routePath: string + path: string }; /** @@ -55,7 +55,7 @@ export type PageListQueryResult = { */ export type StaticPath = { params: { - routePath: string[]; + path: string[]; }; locale?: string; }; @@ -114,9 +114,9 @@ export class GraphQLSitemapService { * @returns an array of @see StaticPath objects */ async fetchExportSitemap(locale: string): Promise { - const formatPath = (routePath: string[]) => ({ + const formatPath = (path: string[]) => ({ params: { - routePath, + path, }, }); @@ -129,9 +129,9 @@ export class GraphQLSitemapService { * @returns an array of @see StaticPath objects */ async fetchSSGSitemap(locales: string[]): Promise { - const formatPath = (routePath: string[], locale: string) => ({ + const formatPath = (path: string[], locale: string) => ({ params: { - routePath, + path, }, locale, }); @@ -181,9 +181,9 @@ export class GraphQLSitemapService { language, pageSize: this.options.pageSize, }) - .then((routesResult) => { - return routesResult.map((item) => - formatStaticPath(item.routePath.replace(/^\/|\/$/g, '').split('/'), language) + .then((results) => { + return results.map((item) => + formatStaticPath(item.path.replace(/^\/|\/$/g, '').split('/'), language) ); }); }) diff --git a/packages/sitecore-jss-nextjs/src/testData/sitemapQueryResult.json b/packages/sitecore-jss-nextjs/src/testData/sitemapQueryResult.json index a0aeb0c458..bc586b2067 100644 --- a/packages/sitecore-jss-nextjs/src/testData/sitemapQueryResult.json +++ b/packages/sitecore-jss-nextjs/src/testData/sitemapQueryResult.json @@ -1,33 +1,29 @@ { "data": { - "search": { - "total": 6, - "pageInfo": { - "endCursor": "Ng==", - "hasNext": false - }, - "results": [ - { - "url": { - "path": "/" - } - }, - { - "url": { - "path": "/x1" - } - }, - { - "url": { - "path": "/y1/y2/y3/y4" - } - }, - { - "url": { - "path": "/y1/y2" - } + "site": { + "siteInfo": { + "routes": { + "total": 6, + "pageInfo": { + "endCursor": "Ng==", + "hasNext": false + }, + "results": [ + { + "path": "/" + }, + { + "path": "/x1" + }, + { + "path": "/y1/y2/y3/y4" + }, + { + "path": "/y1/y2" + } + ] } - ] + } } } } \ No newline at end of file diff --git a/packages/sitecore-jss/src/graphql/site-query-service.ts b/packages/sitecore-jss/src/graphql/site-query-service.ts index 18b5b91035..e8bae539d4 100644 --- a/packages/sitecore-jss/src/graphql/site-query-service.ts +++ b/packages/sitecore-jss/src/graphql/site-query-service.ts @@ -10,23 +10,25 @@ import { BaseQueryService, BaseQueryVariables } from './base-query-service'; export type SiteQueryResult = { site: { siteInfo: { - /** - * Data needed to paginate the site results - */ - pageInfo: { + routes: { /** - * string token that can be used to fetch the next page of results + * Data needed to paginate the site results */ - endCursor: string; - /** - * a value that indicates whether more pages of results are available + pageInfo: { + /** + * string token that can be used to fetch the next page of results + */ + endCursor: string; + /** + * a value that indicates whether more pages of results are available + */ + hasNext: boolean; + }; + /* + * the type of data querying about items matching the search criteria */ - hasNext: boolean; + results: T[]; }; - /* - * the type of data querying about items matching the search criteria - */ - routesResult: T[]; }; }; }; @@ -86,9 +88,9 @@ export class SiteQueryService extends BaseQueryService { after, }); - results = results.concat(fetchResponse?.site?.siteInfo?.routesResult); - hasNext = fetchResponse.site.siteInfo.pageInfo.hasNext; - after = fetchResponse.site.siteInfo.pageInfo.endCursor; + results = results.concat(fetchResponse?.site?.siteInfo?.routes?.results); + hasNext = fetchResponse.site.siteInfo.routes.pageInfo.hasNext; + after = fetchResponse.site.siteInfo.routes.pageInfo.endCursor; } return results; From 01a752c92b671cecb21e9e776946217e36cbd3a1 Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Thu, 28 Apr 2022 17:51:47 -0400 Subject: [PATCH 3/9] Cleanup, comments and adding tests --- .../src/edge/redirects-middleware.ts | 2 +- .../services/graphql-sitemap-service.test.ts | 183 ++++++++++++------ .../src/services/graphql-sitemap-service.ts | 56 ++---- .../src/graphql/base-query-service.ts | 4 + .../src/graphql/site-query-service.ts | 27 ++- 5 files changed, 163 insertions(+), 109 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/edge/redirects-middleware.ts b/packages/sitecore-jss-nextjs/src/edge/redirects-middleware.ts index e3a65b4ce3..3f843a2039 100644 --- a/packages/sitecore-jss-nextjs/src/edge/redirects-middleware.ts +++ b/packages/sitecore-jss-nextjs/src/edge/redirects-middleware.ts @@ -67,7 +67,7 @@ export class RedirectsMiddleware { /** * Method returns RedirectInfo when matches * @param url - * @return Promise + * @returns Promise * @private */ private async getExistsRedirect(url: URL): Promise { diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 694cf293f9..36e61d1e83 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -57,23 +57,6 @@ describe('GraphQLSitemapService', () => { nock.cleanAll(); }); - const mockRootItemIdRequest = (results?: { data: { item: { id: string } | undefined } }) => { - nock(endpoint) - .post('/', /AppRootQuery/gi) - .reply( - 200, - results === undefined - ? appRootQueryResponse - : { - data: { - search: { - results - }, - }, - } - ); - }; - const mockPathsRequest = (results?: { url: { path: string } }[]) => { nock(endpoint) .post('/', /SitemapQuery/gi) @@ -82,20 +65,20 @@ describe('GraphQLSitemapService', () => { results === undefined ? sitemapQueryResult : { - data: { - site: { - siteInfo:{ - routes:{ - total: results.length, - pageInfo: { - hasNext: false, + data: { + site: { + siteInfo: { + routes: { + total: results.length, + pageInfo: { + hasNext: false, + }, + results, }, - results, - } + }, }, }, } - } ); }; @@ -110,6 +93,84 @@ describe('GraphQLSitemapService', () => { return expect(nock.isDone()).to.be.true; }); + it('should work when includePaths and excludePaths are provided', async () => { + const includedPaths = ['/y1/']; + const excludedPaths = ['/y1/y2/y3/y4']; + + nock(endpoint) + .post('/', (body) => { + return body.variables.includedPaths && body.variables.excludedPaths; + }) + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 1, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/y1/y2/', + }, + ], + }, + }, + }, + }, + }); + + nock(endpoint) + .post('/') + .reply(200, { + data: { + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', + }, + { + path: '/x1', + }, + { + path: '/y1/y2/y3/y4', + }, + { + path: '/y1/y2', + }, + ], + }, + }, + }, + }, + }); + + const service = new GraphQLSitemapService({ + endpoint, + apiKey, + siteName, + includedPaths, + excludedPaths, + }); + const sitemap = await service.fetchSSGSitemap(['en']); + + return expect(sitemap).to.deep.equal([ + { + params: { + path: ['y1', 'y2'], + }, + locale: 'en', + }, + ]); + }); + it('should work when multiple languages are requested', async () => { const lang1 = 'ua'; const lang2 = 'da-DK'; @@ -141,8 +202,8 @@ describe('GraphQLSitemapService', () => { path: '/y1/y2', }, ], - } - } + }, + }, }, }, }); @@ -153,34 +214,34 @@ describe('GraphQLSitemapService', () => { }) .reply(200, { data: { - site: { - siteInfo: { - routes: { - total: 4, - pageInfo: { - hasNext: false, + site: { + siteInfo: { + routes: { + total: 4, + pageInfo: { + hasNext: false, + }, + results: [ + { + path: '/', }, - results: [ - { - path: '/', - }, - { - path: '/x1-da-DK', - }, - { - path: '/y1/y2/y3/y4-da-DK', - }, - { - path: '/y1/y2-da-DK', - }, - ], - } - } + { + path: '/x1-da-DK', + }, + { + path: '/y1/y2/y3/y4-da-DK', + }, + { + path: '/y1/y2-da-DK', + }, + ], + }, }, + }, }, }); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName, rootItemId }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchSSGSitemap([lang1, lang2]); expect(sitemap).to.deep.equal([ @@ -238,7 +299,7 @@ describe('GraphQLSitemapService', () => { }); it('should throw error if valid language is not provided', async () => { - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName, rootItemId }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); await service.fetchSSGSitemap([]).catch((error: RangeError) => { expect(error.message).to.equal(languageError); }); @@ -255,7 +316,6 @@ describe('GraphQLSitemapService', () => { endpoint, apiKey, siteName, - rootItemId, pageSize: customPageSize, }); const sitemap = await service.fetchSSGSitemap(['ua']); @@ -277,7 +337,6 @@ describe('GraphQLSitemapService', () => { endpoint, apiKey, siteName, - rootItemId, pageSize: undefined, }); const sitemap = await service.fetchSSGSitemap(['ua']); @@ -289,7 +348,7 @@ describe('GraphQLSitemapService', () => { it('should work if sitemap has 0 pages', async () => { mockPathsRequest([]); - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName, rootItemId }); + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); const sitemap = await service.fetchSSGSitemap(['ua']); expect(sitemap).to.deep.equal([]); return expect(nock.isDone()).to.be.true; @@ -332,7 +391,6 @@ describe('GraphQLSitemapService', () => { }); - it('should use a jssTemplateId, if provided', async () => { const jssAppTemplateId = '{de397294-cfcc-4795-847e-442416d0617b}'; const randomId = '{5a4e6edc-4518-4afb-afdc-9fa22ec4eb91}'; @@ -370,7 +428,6 @@ describe('GraphQLSitemapService', () => { */ it('should throw error if SitemapQuery fails', async () => { - mockRootItemIdRequest(); nock(endpoint) .post('/', /SitemapQuery/gi) .reply(500, 'Error 😥'); @@ -382,7 +439,7 @@ describe('GraphQLSitemapService', () => { }); return expect(nock.isDone()).to.be.true; }); - + /* TODO: consider cleanup it('should throw error if AppRootQuery fails', async () => { nock(endpoint) .post('/', /AppRootQuery/gi) @@ -395,6 +452,7 @@ describe('GraphQLSitemapService', () => { }); return expect(nock.isDone()).to.be.true; }); + */ }); const expectedExportSitemap = [ @@ -422,7 +480,6 @@ describe('GraphQLSitemapService', () => { describe('Fetch sitemap in export mode', () => { it('should fetch sitemap', async () => { - mockRootItemIdRequest(); mockPathsRequest(); const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); @@ -433,7 +490,6 @@ describe('GraphQLSitemapService', () => { }); it('should work if endpoint returns 0 pages', async () => { - mockRootItemIdRequest(); mockPathsRequest([]); const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); @@ -444,7 +500,6 @@ describe('GraphQLSitemapService', () => { }); it('should throw error if SitemapQuery fails', async () => { - mockRootItemIdRequest(); nock(endpoint) .post('/', /SitemapQuery/gi) .reply(500, 'Error 😥'); @@ -456,7 +511,7 @@ describe('GraphQLSitemapService', () => { }); return expect(nock.isDone()).to.be.true; }); - + /* TODO: consider cleanup it('should throw error if AppRootQuery fails', async () => { nock(endpoint) .post('/', /AppRootQuery/gi) @@ -469,9 +524,9 @@ describe('GraphQLSitemapService', () => { }); return expect(nock.isDone()).to.be.true; }); + */ it('should throw error if language is not provided', async () => { - mockRootItemIdRequest(); mockPathsRequest(); const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 97f10dfc19..240c5f0682 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -1,24 +1,22 @@ import { GraphQLClient, GraphQLRequestClient, - getAppRootId, - SearchServiceConfig, + SiteQueryVariables, SiteQueryService, } from '@sitecore-jss/sitecore-jss/graphql'; import { debug } from '@sitecore-jss/sitecore-jss'; -/** @private */ -export const queryError = - 'Valid value for rootItemId not provided and failed to auto-resolve app root item.'; - /** @private */ export const languageError = 'The list of languages cannot be empty'; -// Even though _hasLayout should always be "true" in this query, using a variable is necessary for compatibility with Edge +const languageEmptyError = 'The language must be a non-empty string'; + const defaultQuery = /* GraphQL */ ` query SitemapQuery( $siteName: String!, $language: String!, + $includedPaths: String[], + $excludedPaths: String[], $pageSize: Int = 10, $after: String ) { @@ -26,6 +24,8 @@ query SitemapQuery( siteInfo(site: $siteName) { routes( language: $language + includedPaths: $includedPaths + excludedPaths: $excludedPaths first: $pageSize after: $after ){ @@ -44,10 +44,10 @@ query SitemapQuery( `; /** - * The schema of data returned in response to a pages list query request + * The schema of data returned in response to a routes list query request */ -export type PageListQueryResult = { - path: string +export type RouteListQueryResult = { + path: string; }; /** @@ -63,7 +63,7 @@ export type StaticPath = { /** * Configuration options for @see GraphQLSitemapService instances */ -export interface GraphQLSitemapServiceConfig extends SearchServiceConfig { +export interface GraphQLSitemapServiceConfig extends Omit { /** * Your Graphql endpoint */ @@ -73,12 +73,6 @@ export interface GraphQLSitemapServiceConfig extends SearchServiceConfig { * The API key to use for authentication. */ apiKey: string; - - /** - * Optional. The template ID of a JSS App to use when searching for the appRootId. - * @default '061cba1554744b918a0617903b102b82' (/sitecore/templates/Foundation/JavaScript Services/App) - */ - jssAppTemplateId?: string; } /** @@ -88,7 +82,7 @@ export interface GraphQLSitemapServiceConfig extends SearchServiceConfig { */ export class GraphQLSitemapService { private graphQLClient: GraphQLClient; - private siteService: SiteQueryService; + private siteService: SiteQueryService; /** * Gets the default query used for fetching the list of site pages @@ -103,7 +97,7 @@ export class GraphQLSitemapService { */ constructor(public options: GraphQLSitemapServiceConfig) { this.graphQLClient = this.getGraphQLClient(); - this.siteService = new SiteQueryService(this.graphQLClient); + this.siteService = new SiteQueryService(this.graphQLClient); } /** @@ -146,6 +140,7 @@ export class GraphQLSitemapService { * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. * @returns list of pages * @throws {RangeError} if the list of languages is empty. + * @throws {RangeError} if the any of the languages is an empty string. * @throws {Error} if the app root was not found for the specified site and language. */ protected async fetchSitemap( @@ -155,31 +150,22 @@ export class GraphQLSitemapService { if (!languages.length) { throw new RangeError(languageError); } - - // If the caller does not specify a root item ID, then we try to figure it out - const rootItemId = - this.options.rootItemId || - (await getAppRootId( - this.graphQLClient, - this.options.siteName, - languages[0], - this.options.jssAppTemplateId - )); const siteName = this.options.siteName; - if (!rootItemId) { - throw new Error(queryError); - } - // Fetch paths using all locales const paths = await Promise.all( languages.map((language) => { + if (language === '') { + throw new RangeError(languageEmptyError); + } debug.sitemap('fetching sitemap data for %s', language); return this.siteService .fetch(this.query, { siteName, language, pageSize: this.options.pageSize, + includedPaths: this.options.includedPaths, + excludedPaths: this.options.excludedPaths, }) .then((results) => { return results.map((item) => @@ -210,7 +196,7 @@ export class GraphQLSitemapService { * Gets a service that can perform GraphQL "search" queries to fetch @see PageListQueryResult * @returns {SearchQueryService} the search query service */ - protected getSearchService(): SiteQueryService { - return new SiteQueryService(this.graphQLClient); + protected getSearchService(): SiteQueryService { + return new SiteQueryService(this.graphQLClient); } } diff --git a/packages/sitecore-jss/src/graphql/base-query-service.ts b/packages/sitecore-jss/src/graphql/base-query-service.ts index 4eaf64345e..b18ce9a384 100644 --- a/packages/sitecore-jss/src/graphql/base-query-service.ts +++ b/packages/sitecore-jss/src/graphql/base-query-service.ts @@ -1,6 +1,10 @@ import { DocumentNode } from 'graphql'; import { GraphQLClient } from '../graphql-request-client'; +/** + * This file contains base types for GraphQL services within sitecore-jss + */ + export interface BaseQueryVariables { /** common variable for all GraphQL queries * it will be used for every type of query to regulate result batch size diff --git a/packages/sitecore-jss/src/graphql/site-query-service.ts b/packages/sitecore-jss/src/graphql/site-query-service.ts index e8bae539d4..f1ed62368e 100644 --- a/packages/sitecore-jss/src/graphql/site-query-service.ts +++ b/packages/sitecore-jss/src/graphql/site-query-service.ts @@ -7,7 +7,7 @@ import { BaseQueryService, BaseQueryVariables } from './base-query-service'; * Schema of data returned in response to a "site" query request * @template T The type of objects being requested. */ -export type SiteQueryResult = { +export interface SiteQueryResult { site: { siteInfo: { routes: { @@ -31,10 +31,10 @@ export type SiteQueryResult = { }; }; }; -}; +} /** - * Describes the variables used by the 'search' query. Language should always be specified. + * Describes the variables used by the 'site' query. Language and siteName should always be specified. * The other predicates are optional. */ export declare interface SiteQueryVariables extends BaseQueryVariables { @@ -42,17 +42,22 @@ export declare interface SiteQueryVariables extends BaseQueryVariables { * Required. The name of the site being queried. */ siteName: string; - + /** + * Required. The language to return routes/pages for. + */ language: string; - + /** + * Optional. Only paths starting with these provided prefixes will be returned. + */ includedPaths?: string[]; - + /** + * Optional. Paths starting with these provided prefixes will be excluded from returned results. + */ excludedPaths?: string[]; } /** - * 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. + * Provides functionality for performing GraphQL 'site' operations, including handling pagination. * @template T The type of objects being requested. * @mixin */ @@ -64,7 +69,7 @@ export class SiteQueryService extends BaseQueryService { /** * 1. Validates mandatory search query arguments - * 2. Executes search query with pagination + * 2. Executes site query with pagination * 3. Aggregates pagination results into a single result-set. * @template T The type of objects being requested. * @param {string | DocumentNode} query the search query. @@ -78,6 +83,10 @@ export class SiteQueryService extends BaseQueryService { throw new RangeError('"siteName" must a be non-empty string'); } + if (!args.language) { + throw new RangeError('"rootItemId" and "language" must be non-empty strings'); + } + let results: T[] = []; let hasNext = true; let after = ''; From 099a9306acdaf40c3efdfced4a5cb1fad07d7aaf Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Fri, 29 Apr 2022 14:25:10 -0400 Subject: [PATCH 4/9] Test and comment cleanup --- .../services/graphql-sitemap-service.test.ts | 101 ------------------ .../src/services/graphql-sitemap-service.ts | 3 +- 2 files changed, 1 insertion(+), 103 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 36e61d1e83..7ff3f3e8f8 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -354,79 +354,6 @@ describe('GraphQLSitemapService', () => { return expect(nock.isDone()).to.be.true; }); - /* TODO: cleanup these tests or replace - it('should attempt to fetch the rootItemId, if rootItemId not provided', async () => { - mockRootItemIdRequest(); - - nock(endpoint) - .post('/', (body) => body.variables.rootItemId === 'GUIDGUIDGUID') - .reply(200, sitemapQueryResult); - - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - siteName, - }); - - const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceResult); - }); - - it('should use a custom rootItemId, if provided', async () => { - const customRootId = 'cats'; - - nock(endpoint) - .post('/', (body) => body.variables.rootItemId === customRootId) - .reply(200, sitemapQueryResult); - - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - siteName, - rootItemId: customRootId, - }); - - const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceResult); - }); - - - it('should use a jssTemplateId, if provided', async () => { - const jssAppTemplateId = '{de397294-cfcc-4795-847e-442416d0617b}'; - const randomId = '{5a4e6edc-4518-4afb-afdc-9fa22ec4eb91}'; - - nock(endpoint) - .post('/', (body) => body.variables.jssAppTemplateId === jssAppTemplateId) - .reply(200, { - data: { - layout: { - homePage: { - rootItem: [ - { - id: randomId, - }, - ], - }, - }, - }, - }); - - nock(endpoint) - .post('/', (body) => body.variables.rootItemId === randomId) - .reply(200, sitemapQueryResult); - - const service = new GraphQLSitemapService({ - endpoint, - apiKey, - siteName, - jssAppTemplateId, - }); - - const sitemap = await service.fetchSSGSitemap(['ua']); - expect(sitemap).to.deep.equal(sitemapServiceResult); - }); - */ - it('should throw error if SitemapQuery fails', async () => { nock(endpoint) .post('/', /SitemapQuery/gi) @@ -439,20 +366,6 @@ describe('GraphQLSitemapService', () => { }); return expect(nock.isDone()).to.be.true; }); - /* TODO: consider cleanup - it('should throw error if AppRootQuery fails', async () => { - nock(endpoint) - .post('/', /AppRootQuery/gi) - .reply(500, 'Error 😥'); - - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); - await service.fetchSSGSitemap(['ua']).catch((error: RangeError) => { - expect(error.message).to.contain('AppRootQuery'); - expect(error.message).to.contain('Error 😥'); - }); - return expect(nock.isDone()).to.be.true; - }); - */ }); const expectedExportSitemap = [ @@ -511,20 +424,6 @@ describe('GraphQLSitemapService', () => { }); return expect(nock.isDone()).to.be.true; }); - /* TODO: consider cleanup - it('should throw error if AppRootQuery fails', async () => { - nock(endpoint) - .post('/', /AppRootQuery/gi) - .reply(500, 'Error 😥'); - - const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); - await service.fetchExportSitemap('ua').catch((error: RangeError) => { - expect(error.message).to.contain('AppRootQuery'); - expect(error.message).to.contain('Error 😥'); - }); - return expect(nock.isDone()).to.be.true; - }); - */ it('should throw error if language is not provided', async () => { mockPathsRequest(); diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 240c5f0682..0bc36d61ad 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -134,14 +134,13 @@ export class GraphQLSitemapService { } /** - * Fetch a flat list of all pages that are descendants of the specified root item and have a + * Fetch a flat list of all pages that belong to the specificed site and have a * version in the specified language(s). * @param {string[]} languages Fetch pages that have versions in this language(s). * @param {Function} formatStaticPath Function for transforming the raw search results into (@see StaticPath) types. * @returns list of pages * @throws {RangeError} if the list of languages is empty. * @throws {RangeError} if the any of the languages is an empty string. - * @throws {Error} if the app root was not found for the specified site and language. */ protected async fetchSitemap( languages: string[], From 84ff91754eadec9075f961bf1dea3c26eaca30e1 Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Tue, 3 May 2022 10:52:49 -0400 Subject: [PATCH 5/9] Addressing Adam's comments - moving site query logic into nextjs project --- .../services/graphql-sitemap-service.test.ts | 12 +- .../src/services/graphql-sitemap-service.ts | 134 +++++++++++++----- packages/sitecore-jss/src/graphql/index.ts | 2 +- .../src/graphql/search-service.ts | 45 ++++-- 4 files changed, 139 insertions(+), 54 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts index 7ff3f3e8f8..da5b5e7a82 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.test.ts @@ -5,7 +5,6 @@ import { GraphQLSitemapServiceConfig, languageError, } from './graphql-sitemap-service'; -import appRootQueryResponse from '../testData/mockAppRootQueryResponse.json'; import sitemapQueryResult from '../testData/sitemapQueryResult.json'; import sitemapServiceResult from '../testData/sitemapServiceResult'; import { GraphQLClient, GraphQLRequestClient } from '@sitecore-jss/sitecore-jss/graphql'; @@ -305,6 +304,17 @@ describe('GraphQLSitemapService', () => { }); }); + it('should throw error if empty language is provided', async () => { + mockPathsRequest(); + + const service = new GraphQLSitemapService({ endpoint, apiKey, siteName }); + await service.fetchExportSitemap('').catch((error: RangeError) => { + expect(error.message).to.equal('The language must be a non-empty string'); + }); + + return expect(nock.isDone()).to.be.false; + }); + it('should use a custom pageSize, if provided', async () => { const customPageSize = 20; diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 0bc36d61ad..40aa96bbb7 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -1,9 +1,4 @@ -import { - GraphQLClient, - GraphQLRequestClient, - SiteQueryVariables, - SiteQueryService, -} from '@sitecore-jss/sitecore-jss/graphql'; +import { GraphQLClient, GraphQLRequestClient, PageInfo } from '@sitecore-jss/sitecore-jss/graphql'; import { debug } from '@sitecore-jss/sitecore-jss'; /** @private */ @@ -42,22 +37,58 @@ query SitemapQuery( } } `; - /** - * The schema of data returned in response to a routes list query request + * type for input variables for the site routes query */ -export type RouteListQueryResult = { - path: string; -}; +interface SiteQueryVariables { + /** + * Required. The name of the site being queried. + */ + siteName: string; + /** + * Required. The language to return routes/pages for. + */ + language: string; + /** + * Optional. Only paths starting with these provided prefixes will be returned. + */ + includedPaths?: string[]; + /** + * Optional. Paths starting with these provided prefixes will be excluded from returned results. + */ + excludedPaths?: string[]; + + /** common variable for all GraphQL queries + * it will be used for every type of query to regulate result batch size + * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. + * @default 10 + */ + pageSize?: number; +} /** - * Object model of a site page item. + * Schema of data returned in response to a "site" query request + * @template T The type of objects being requested. */ -export type StaticPath = { - params: { - path: string[]; +export interface SiteRouteQueryResult { + site: { + siteInfo: { + routes: { + /** + * Data needed to paginate the site results + */ + pageInfo: PageInfo; + results: T[]; + }; + }; }; - locale?: string; +} + +/** + * The schema of data returned in response to a routes list query request + */ +export type RouteListQueryResult = { + path: string; }; /** @@ -75,6 +106,16 @@ export interface GraphQLSitemapServiceConfig extends Omit; /** * Gets the default query used for fetching the list of site pages @@ -97,7 +137,6 @@ export class GraphQLSitemapService { */ constructor(public options: GraphQLSitemapServiceConfig) { this.graphQLClient = this.getGraphQLClient(); - this.siteService = new SiteQueryService(this.graphQLClient); } /** @@ -151,26 +190,28 @@ export class GraphQLSitemapService { } const siteName = this.options.siteName; + const args: SiteQueryVariables = { + siteName, + language: '', + pageSize: this.options.pageSize, + includedPaths: this.options.includedPaths, + excludedPaths: this.options.excludedPaths, + }; + // Fetch paths using all locales const paths = await Promise.all( languages.map((language) => { if (language === '') { throw new RangeError(languageEmptyError); } + args.language = language; debug.sitemap('fetching sitemap data for %s', language); - return this.siteService - .fetch(this.query, { - siteName, - language, - pageSize: this.options.pageSize, - includedPaths: this.options.includedPaths, - excludedPaths: this.options.excludedPaths, - }) - .then((results) => { - return results.map((item) => - formatStaticPath(item.path.replace(/^\/|\/$/g, '').split('/'), language) - ); - }); + + return this.fetchLanguageSitePaths(this.query, args).then((results) => { + return results.map((item) => + formatStaticPath(item.path.replace(/^\/|\/$/g, '').split('/'), language) + ); + }); }) ); @@ -178,6 +219,29 @@ export class GraphQLSitemapService { return ([] as StaticPath[]).concat(...paths); } + protected async fetchLanguageSitePaths( + query: string, + args: SiteQueryVariables + ): Promise { + let results: RouteListQueryResult[] = []; + let hasNext = true; + let after = ''; + + while (hasNext) { + const fetchResponse = await this.graphQLClient.request< + SiteRouteQueryResult + >(query, { + ...args, + after, + }); + + results = results.concat(fetchResponse?.site?.siteInfo?.routes?.results); + hasNext = fetchResponse.site.siteInfo.routes.pageInfo.hasNext; + after = fetchResponse.site.siteInfo.routes.pageInfo.endCursor; + } + return results; + } + /** * 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 @@ -190,12 +254,4 @@ export class GraphQLSitemapService { debugger: debug.sitemap, }); } - - /** - * Gets a service that can perform GraphQL "search" queries to fetch @see PageListQueryResult - * @returns {SearchQueryService} the search query service - */ - protected getSearchService(): SiteQueryService { - return new SiteQueryService(this.graphQLClient); - } } diff --git a/packages/sitecore-jss/src/graphql/index.ts b/packages/sitecore-jss/src/graphql/index.ts index 6cfe84bc54..cf3f74b6da 100644 --- a/packages/sitecore-jss/src/graphql/index.ts +++ b/packages/sitecore-jss/src/graphql/index.ts @@ -9,5 +9,5 @@ export { SearchQueryVariables, SearchServiceConfig, SearchQueryService, + PageInfo, } from './search-service'; -export { SiteQueryResult, SiteQueryService, SiteQueryVariables } from './site-query-service'; diff --git a/packages/sitecore-jss/src/graphql/search-service.ts b/packages/sitecore-jss/src/graphql/search-service.ts index 883db70902..9550141a8f 100644 --- a/packages/sitecore-jss/src/graphql/search-service.ts +++ b/packages/sitecore-jss/src/graphql/search-service.ts @@ -1,5 +1,20 @@ import { DocumentNode } from 'graphql'; -import { BaseQueryService, BaseQueryVariables } from './base-query-service'; +import { GraphQLClient } from '../graphql-request-client'; + +/** + * Data needed to paginate the site results + */ +export interface PageInfo { + /** + * string token that can be used to fetch the next page of results + */ + endCursor: string; + /** + * a value that indicates whether more pages of results are available + */ + hasNext: boolean; +} + /** * Schema of data returned in response to a "search" query request * @template T The type of objects being requested. @@ -9,16 +24,7 @@ export type SearchQueryResult = { /** * Data needed to paginate the search results */ - pageInfo: { - /** - * string token that can be used to fetch the next page of results - */ - endCursor: string; - /** - * a value that indicates whether more pages of results are available - */ - hasNext: boolean; - }; + pageInfo: PageInfo; /* * the type of data querying about items matching the search criteria */ @@ -30,7 +36,7 @@ export type SearchQueryResult = { * Describes the variables used by the 'search' query. Language should always be specified. * The other predicates are optional. */ -export interface SearchQueryVariables extends BaseQueryVariables { +export interface SearchQueryVariables { /** * Required. The language versions to search for. Fetch pages that have versions in this language. */ @@ -45,6 +51,13 @@ export interface SearchQueryVariables extends BaseQueryVariables { * Optional. Sitecore template ID(s). Fetch items that inherit from this template(s). */ templates?: string; + + /** common variable for all GraphQL queries + * it will be used for every type of query to regulate result batch size + * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. + * @default 10 + */ + pageSize?: number; } /** @@ -68,7 +81,13 @@ export interface SearchServiceConfig extends Omit extends BaseQueryService { +export class SearchQueryService { + /** + * Creates an instance of search query service. + * @param {GraphQLClient} client that fetches data from a GraphQL endpoint. + */ + constructor(protected client: GraphQLClient) {} + /** * 1. Validates mandatory search query arguments * 2. Executes search query with pagination From ef20465302454bdb6df543eca12e07634988473c Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Tue, 3 May 2022 15:05:06 -0400 Subject: [PATCH 6/9] remove extra files --- .../src/graphql/base-query-service.ts | 33 ------ .../src/graphql/site-query-service.ts | 107 ------------------ 2 files changed, 140 deletions(-) delete mode 100644 packages/sitecore-jss/src/graphql/base-query-service.ts delete mode 100644 packages/sitecore-jss/src/graphql/site-query-service.ts diff --git a/packages/sitecore-jss/src/graphql/base-query-service.ts b/packages/sitecore-jss/src/graphql/base-query-service.ts deleted file mode 100644 index b18ce9a384..0000000000 --- a/packages/sitecore-jss/src/graphql/base-query-service.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { DocumentNode } from 'graphql'; -import { GraphQLClient } from '../graphql-request-client'; - -/** - * This file contains base types for GraphQL services within sitecore-jss - */ - -export interface BaseQueryVariables { - /** common variable for all GraphQL queries - * it will be used for every type of query to regulate result batch size - * Optional. How many result items to fetch in each GraphQL call. This is needed for pagination. - * @default 10 - */ - pageSize?: number; -} - -export abstract class BaseQueryService { - /** - * Creates an instance of search query service. - * @param {GraphQLClient} client that fetches data from a GraphQL endpoint. - */ - constructor(protected client: GraphQLClient) {} - - /** - * 1. Validates mandatory search query arguments - * 2. Executes search query with pagination - * 3. Aggregates pagination results into a single result-set. - * @template T The type of objects being requested. - * @param {string | DocumentNode} query the graph query. - * @param {BaseQueryVariables} args graph query arguments - should have derived types for different types of GraphQL queries. - * */ - abstract fetch(query: string | DocumentNode, args: BaseQueryVariables): Promise; -} diff --git a/packages/sitecore-jss/src/graphql/site-query-service.ts b/packages/sitecore-jss/src/graphql/site-query-service.ts deleted file mode 100644 index f1ed62368e..0000000000 --- a/packages/sitecore-jss/src/graphql/site-query-service.ts +++ /dev/null @@ -1,107 +0,0 @@ -import { DocumentNode } from 'graphql'; -import { BaseQueryService, BaseQueryVariables } from './base-query-service'; -// TODO: rewrite site query results -// TODO: review-rewrite comments - -/** - * Schema of data returned in response to a "site" query request - * @template T The type of objects being requested. - */ -export interface SiteQueryResult { - site: { - siteInfo: { - routes: { - /** - * Data needed to paginate the site results - */ - pageInfo: { - /** - * string token that can be used to fetch the next page of results - */ - endCursor: string; - /** - * a value that indicates whether more pages of results are available - */ - hasNext: boolean; - }; - /* - * the type of data querying about items matching the search criteria - */ - results: T[]; - }; - }; - }; -} - -/** - * Describes the variables used by the 'site' query. Language and siteName should always be specified. - * The other predicates are optional. - */ -export declare interface SiteQueryVariables extends BaseQueryVariables { - /** - * Required. The name of the site being queried. - */ - siteName: string; - /** - * Required. The language to return routes/pages for. - */ - language: string; - /** - * Optional. Only paths starting with these provided prefixes will be returned. - */ - includedPaths?: string[]; - /** - * Optional. Paths starting with these provided prefixes will be excluded from returned results. - */ - excludedPaths?: string[]; -} - -/** - * Provides functionality for performing GraphQL 'site' operations, including handling pagination. - * @template T The type of objects being requested. - * @mixin - */ -export class SiteQueryService extends BaseQueryService { - /** - * Creates an instance of search query service. - * @param {GraphQLClient} client that fetches data from a GraphQL endpoint. - */ - - /** - * 1. Validates mandatory search query arguments - * 2. Executes site query with pagination - * 3. Aggregates pagination results into a single result-set. - * @template T The type of objects being requested. - * @param {string | DocumentNode} query the search query. - * @param {SiteQueryVariables} args search query arguments. - * @returns {T[]} array of result objects. - * @throws {RangeError} if a valid root item ID is not provided. - * @throws {RangeError} if the provided language(s) is(are) not valid. - */ - async fetch(query: string | DocumentNode, args: SiteQueryVariables): Promise { - if (!args.siteName) { - throw new RangeError('"siteName" must a be non-empty string'); - } - - if (!args.language) { - throw new RangeError('"rootItemId" and "language" must be non-empty strings'); - } - - let results: T[] = []; - let hasNext = true; - let after = ''; - - while (hasNext) { - const fetchResponse = await this.client.request>(query, { - ...args, - after, - }); - - results = results.concat(fetchResponse?.site?.siteInfo?.routes?.results); - hasNext = fetchResponse.site.siteInfo.routes.pageInfo.hasNext; - after = fetchResponse.site.siteInfo.routes.pageInfo.endCursor; - } - - return results; - } -} From cdcb62bba24d4c3744bd4aa63fd1da80258a0058 Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Tue, 3 May 2022 16:55:39 -0400 Subject: [PATCH 7/9] more cleanup --- .../sitemap-fetcher/plugins/graphql-sitemap-service.ts | 6 ------ .../src/services/graphql-sitemap-service.ts | 8 ++++---- packages/sitecore-jss/src/graphql/search-service.ts | 2 +- 3 files changed, 5 insertions(+), 11 deletions(-) diff --git a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts index 74b165dfbb..b81ee35ef0 100644 --- a/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts +++ b/packages/create-sitecore-jss/src/templates/nextjs/src/lib/sitemap-fetcher/plugins/graphql-sitemap-service.ts @@ -13,12 +13,6 @@ class GraphqlSitemapServicePlugin implements SitemapFetcherPlugin { endpoint: config.graphQLEndpoint, apiKey: config.sitecoreApiKey, siteName: config.jssAppName, - /* - The Sitemap Service needs a root item ID in order to fetch the list of pages for the current - app. If your Sitecore instance only has 1 JSS App, you can specify the root item ID here; - otherwise, the service will attempt to figure out the root item for the current JSS App using GraphQL and app name. - rootItemId: '{GUID}' - */ }); } diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 40aa96bbb7..aad7d537c6 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -40,7 +40,7 @@ query SitemapQuery( /** * type for input variables for the site routes query */ -interface SiteQueryVariables { +interface SiteRouteQueryVariables { /** * Required. The name of the site being queried. */ @@ -94,7 +94,7 @@ export type RouteListQueryResult = { /** * Configuration options for @see GraphQLSitemapService instances */ -export interface GraphQLSitemapServiceConfig extends Omit { +export interface GraphQLSitemapServiceConfig extends Omit { /** * Your Graphql endpoint */ @@ -190,7 +190,7 @@ export class GraphQLSitemapService { } const siteName = this.options.siteName; - const args: SiteQueryVariables = { + const args: SiteRouteQueryVariables = { siteName, language: '', pageSize: this.options.pageSize, @@ -221,7 +221,7 @@ export class GraphQLSitemapService { protected async fetchLanguageSitePaths( query: string, - args: SiteQueryVariables + args: SiteRouteQueryVariables ): Promise { let results: RouteListQueryResult[] = []; let hasNext = true; diff --git a/packages/sitecore-jss/src/graphql/search-service.ts b/packages/sitecore-jss/src/graphql/search-service.ts index 9550141a8f..4db6e44d71 100644 --- a/packages/sitecore-jss/src/graphql/search-service.ts +++ b/packages/sitecore-jss/src/graphql/search-service.ts @@ -2,7 +2,7 @@ import { DocumentNode } from 'graphql'; import { GraphQLClient } from '../graphql-request-client'; /** - * Data needed to paginate the site results + * Data needed to paginate results */ export interface PageInfo { /** From 9bb7d39f1cd87bf76751789043b2b252f8dc61c2 Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Thu, 5 May 2022 12:14:52 -0400 Subject: [PATCH 8/9] Fixed variables in GQL query --- .../src/services/graphql-sitemap-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index aad7d537c6..1b53d61294 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -10,8 +10,8 @@ const defaultQuery = /* GraphQL */ ` query SitemapQuery( $siteName: String!, $language: String!, - $includedPaths: String[], - $excludedPaths: String[], + $includedPaths: [String], + $excludedPaths: [String], $pageSize: Int = 10, $after: String ) { From 7b2001f8a3005df8b22de39a42218592b3f5d9e9 Mon Sep 17 00:00:00 2001 From: "DK\\ala" Date: Thu, 5 May 2022 12:23:24 -0400 Subject: [PATCH 9/9] small query change --- .../src/services/graphql-sitemap-service.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts index 1b53d61294..38764a019d 100644 --- a/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts +++ b/packages/sitecore-jss-nextjs/src/services/graphql-sitemap-service.ts @@ -29,8 +29,8 @@ query SitemapQuery( endCursor hasNext } - results: routesResult{ - path: routePath + results{ + path: routePath } } }