diff --git a/__test__/test-website/src/app/[...slug]/page.tsx b/__test__/test-website/src/app/[...slug]/page.tsx
index ddcccf2..0a58928 100644
--- a/__test__/test-website/src/app/[...slug]/page.tsx
+++ b/__test__/test-website/src/app/[...slug]/page.tsx
@@ -1,4 +1,4 @@
-import { GraphClient } from '@episerver/cms-sdk';
+import { GraphClient, GraphErrors } from '@episerver/cms-sdk';
import { OptimizelyComponent } from '@episerver/cms-sdk/react/server';
import React from 'react';
@@ -6,15 +6,34 @@ type Props = {
params: Promise<{
slug: string[];
}>;
+ // Assume that search params are correct:
+ searchParams: Promise<{ variation?: string }>;
};
-export default async function Page({ params }: Props) {
+function handleGraphErrors(err: unknown): never {
+ if (err instanceof GraphErrors.GraphResponseError) {
+ console.log('Error message:', err.message);
+ console.log('Query:', err.request.query);
+ console.log('Variables:', err.request.variables);
+ }
+ if (err instanceof GraphErrors.GraphContentResponseError) {
+ console.log('Detailed errors: ', err.errors);
+ }
+
+ throw err;
+}
+
+export default async function Page({ params, searchParams }: Props) {
const { slug } = await params;
+ const { variation } = await searchParams;
const client = new GraphClient(process.env.OPTIMIZELY_GRAPH_SINGLE_KEY!, {
graphUrl: process.env.OPTIMIZELY_GRAPH_URL,
});
- const c = await client.fetchContent(`/${slug.join('/')}/`);
- return ;
+ const content = await client
+ .fetchContent({ path: `/${slug.join('/')}/`, variation })
+ .catch(handleGraphErrors);
+
+ return ;
}
diff --git a/packages/optimizely-cms-sdk/src/graph/createQuery.ts b/packages/optimizely-cms-sdk/src/graph/createQuery.ts
index 0178d14..3e83c18 100644
--- a/packages/optimizely-cms-sdk/src/graph/createQuery.ts
+++ b/packages/optimizely-cms-sdk/src/graph/createQuery.ts
@@ -252,8 +252,8 @@ export function createQuery(contentType: string) {
return `
${fragment.join('\n')}
-query FetchContent($filter: _ContentWhereInput) {
- _Content(where: $filter) {
+query FetchContent($where: _ContentWhereInput, $variation: VariationInput) {
+ _Content(where: $where, variation: $variation) {
item {
__typename
...${contentType}
diff --git a/packages/optimizely-cms-sdk/src/graph/error.ts b/packages/optimizely-cms-sdk/src/graph/error.ts
index 2d059a0..7fdda4e 100644
--- a/packages/optimizely-cms-sdk/src/graph/error.ts
+++ b/packages/optimizely-cms-sdk/src/graph/error.ts
@@ -1,4 +1,4 @@
-import type { GraphVariables } from './index.js';
+import type { ContentInput } from './filters.js';
/** Represents the request sent to graph */
type GraphRequest = {
@@ -6,7 +6,7 @@ type GraphRequest = {
query: string;
/** Variables sent to Graph */
- variables: GraphVariables;
+ variables: ContentInput;
};
/** Super-class for all errors related to Optimizely Graph */
diff --git a/packages/optimizely-cms-sdk/src/graph/filters.ts b/packages/optimizely-cms-sdk/src/graph/filters.ts
new file mode 100644
index 0000000..cfc4b94
--- /dev/null
+++ b/packages/optimizely-cms-sdk/src/graph/filters.ts
@@ -0,0 +1,156 @@
+/**
+ * This module contains the TypeScript definitions of a Graph Query
+ * and functions to build those filters based on path,
+ * preview parameters, etc.
+ *
+ * This is used internally in the SDK
+ */
+
+/**
+ * Creates a {@linkcode ContentInput} object that filters results by a specific URL path.
+ *
+ * @param path - The URL path to filter by.
+ * @returns A `GraphQueryArguments` object with a `where` clause that matches the given path.
+ */
+export function pathFilter(path: string): ContentInput {
+ return {
+ where: {
+ _metadata: {
+ url: {
+ default: {
+ eq: path,
+ },
+ },
+ },
+ },
+ };
+}
+
+/**
+ * Creates a {@linkcode ContentInput} object for previewing content based on key, version, and locale.
+ *
+ * @param params - An object containing the following properties:
+ * @param params.key - The unique key identifying the content.
+ * @param params.ver - The version of the content to preview.
+ * @param params.loc - The locale of the content to preview.
+ *
+ * @returns A `GraphQueryArguments` object with a `where` clause filtering by key, version, and locale.
+ */
+export function previewFilter(params: {
+ key: string;
+ ver: string;
+ loc: string;
+}): ContentInput {
+ return {
+ where: {
+ _metadata: {
+ key: { eq: params.key },
+ version: { eq: params.ver },
+ locale: { eq: params.loc },
+ },
+ },
+ };
+}
+
+export function variationFilter(value: string): ContentInput {
+ return {
+ variation: {
+ include: 'SOME',
+ value: [value],
+ },
+ };
+}
+
+/**
+ * Arguments for querying content via the Graph API.
+ */
+export type ContentInput = {
+ variation?: VariationInput;
+ where?: ContentWhereInput;
+};
+
+type VariationInput = {
+ include?: VariationIncludeMode;
+ value?: string[];
+ includeOriginal?: boolean;
+};
+
+type VariationIncludeMode = 'ALL' | 'SOME' | 'NONE';
+
+type ContentWhereInput = {
+ _and?: ContentWhereInput[];
+ _or?: ContentWhereInput[];
+ _fulltext?: StringFilterInput;
+ _modified?: DateFilterInput;
+ _metadata?: IContentMetadataWhereInput;
+};
+
+type StringFilterInput = ScalarFilterInput & {
+ like?: string;
+ startsWith?: string;
+ endsWith?: string;
+ in?: string[];
+ notIn?: string[];
+ match?: string;
+ contains?: string;
+ synonyms?: ('ONE' | 'TWO')[];
+ fuzzly?: boolean;
+};
+
+type DateFilterInput = ScalarFilterInput & {
+ gt?: string;
+ gte?: string;
+ lt?: string;
+ lte?: string;
+ decay?: {
+ origin?: string;
+ scale?: number;
+ rate?: number;
+ };
+};
+
+type IContentMetadataWhereInput = {
+ key?: StringFilterInput;
+ locale?: StringFilterInput;
+ fallbackForLocale?: StringFilterInput;
+ version?: StringFilterInput;
+ displayName?: StringFilterInput;
+ url?: ContentUrlInput;
+ types?: StringFilterInput;
+ published?: DateFilterInput;
+ status?: StringFilterInput;
+ changeset?: StringFilterInput;
+ created?: DateFilterInput;
+ lastModified?: DateFilterInput;
+ sortOrder?: IntFilterInput;
+ variation?: StringFilterInput;
+};
+
+type IntFilterInput = ScalarFilterInput & {
+ gt?: number;
+ gte?: number;
+ lt?: number;
+ lte?: number;
+ in?: number[];
+ notIn?: number[];
+ factor?: {
+ value?: number;
+ modifier?: 'NONE' | 'SQUARE' | 'SQRT' | 'LOG' | 'RECIPROCAL';
+ };
+};
+
+type ContentUrlInput = {
+ type?: T;
+ default?: T;
+ hierarchical?: T;
+ internal?: T;
+ graph?: T;
+ base?: T;
+};
+
+type ScalarFilterInput = {
+ eq?: T;
+ notEq?: T;
+ exist?: boolean;
+ boost?: number;
+};
diff --git a/packages/optimizely-cms-sdk/src/graph/index.ts b/packages/optimizely-cms-sdk/src/graph/index.ts
index e7c9cce..4e95741 100644
--- a/packages/optimizely-cms-sdk/src/graph/index.ts
+++ b/packages/optimizely-cms-sdk/src/graph/index.ts
@@ -4,6 +4,12 @@ import {
GraphHttpResponseError,
GraphResponseError,
} from './error.js';
+import {
+ ContentInput as GraphVariables,
+ pathFilter,
+ previewFilter,
+ variationFilter,
+} from './filters.js';
/** Options for Graph */
type GraphOptions = {
@@ -19,20 +25,38 @@ export type PreviewParams = {
loc: string;
};
-// TODO: this type definition is provisional
-export type GraphFilter = {
- _metadata: {
- [key: string]: any;
- };
+/** Arguments for the public methods `fetchContent`, `fetchContentType` */
+type FetchContentOptions = {
+ path?: string;
+ variation?: string;
};
-export type GraphVariables = {
- filter: GraphFilter;
-};
+/**
+ * Builds the variables object for a GraphQL query based on the provided options.
+ *
+ * If a string is provided, it is treated as a path and passed to `pathFilter`.
+ * If an object is provided, it may contain `path` and/or `variation` properties,
+ * which are processed by `pathFilter` and `variationsFilter` respectively.
+ *
+ * @param options - Either a string representing the content path, or an object containing fetch options.
+ * @returns A `GraphVariables` object containing the appropriate filters for the query.
+ */
+function buildGraphVariables(
+ options: string | FetchContentOptions
+): GraphVariables {
+ if (typeof options === 'string') {
+ return pathFilter(options);
+ }
+
+ return {
+ ...(options.path && pathFilter(options.path)),
+ ...(options.variation && variationFilter(options.variation)),
+ };
+}
const FETCH_CONTENT_TYPE_QUERY = `
-query FetchContentType($filter: _ContentWhereInput) {
- _Content(where: $filter) {
+query FetchContentType($where: _ContentWhereInput, $variation: VariationInput) {
+ _Content(where: $where, variation: $variation) {
item {
_metadata {
types
@@ -42,26 +66,6 @@ query FetchContentType($filter: _ContentWhereInput) {
}
`;
-export function getFilterFromPreviewParams(params: PreviewParams): GraphFilter {
- return {
- _metadata: {
- key: { eq: params.key },
- version: { eq: params.ver },
- locale: { eq: params.loc },
- },
- };
-}
-
-export function getFilterFromPath(path: string): GraphFilter {
- return {
- _metadata: {
- url: {
- default: { eq: path },
- },
- },
- };
-}
-
/** Adds an extra `__context` property next to each `__typename` property */
function decorateWithContext(obj: any, params: PreviewParams): any {
if (Array.isArray(obj)) {
@@ -149,57 +153,79 @@ export class GraphClient {
return json.data;
}
- /** Fetches the content type of a content. Returns `undefined` if the content doesn't exist */
- async fetchContentType(filter: GraphFilter, previewToken?: string) {
+ /**
+ * Fetches the content type metadata for a given content input.
+ *
+ * @param input - The content input used to query the content type.
+ * @param previewToken - Optional preview token for fetching preview content.
+ * @returns A promise that resolves to the first content type metadata object, or `undefined` if not found.
+ */
+ private async getContentType(input: GraphVariables, previewToken?: string) {
const data = await this.request(
FETCH_CONTENT_TYPE_QUERY,
- { filter },
+ input,
previewToken
);
return data._Content?.item?._metadata?.types?.[0];
}
- /** Fetches a content given its path */
- async fetchContent(path: string) {
- const filter = getFilterFromPath(path);
- const contentTypeName = await this.fetchContentType(filter);
+ /**
+ * Fetches a content type from the CMS using the provided options.
+ *
+ * @param options - A string representing the content path,
+ * or an {@linkcode FetchContentOptions} containing path and variation filters.
+ * @returns A promise that resolves to the requested content type.
+ */
+ async fetchContentType(options: string | FetchContentOptions) {
+ let input: GraphVariables = buildGraphVariables(options);
+
+ return this.getContentType(input);
+ }
+
+ /**
+ * Fetches content from the CMS based on the provided path or options.
+ *
+ * If a string is provided, it is treated as a content path. If an object is provided,
+ * it may include both a path and a variation to filter the content.
+ *
+ * @param options - A string representing the content path,
+ * or an {@linkcode FetchContentOptions} containing path and variation filters.
+ *
+ * @returns A promise that resolves to the fetched content item.
+ */
+ async fetchContent(options: string | FetchContentOptions) {
+ const input: GraphVariables = buildGraphVariables(options);
+ const contentTypeName = await this.getContentType(input);
if (!contentTypeName) {
- throw new GraphResponseError(
- `No content found for path [${path}]. Check that your CMS contains something in the given path`,
- { request: { variables: { filter }, query: FETCH_CONTENT_TYPE_QUERY } }
- );
+ throw new GraphResponseError(`No content found.`, {
+ request: { variables: input, query: FETCH_CONTENT_TYPE_QUERY },
+ });
}
const query = createQuery(contentTypeName);
-
- const response = await this.request(query, { filter });
+ const response = await this.request(query, input);
return response?._Content?.item;
}
/** Fetches a content given the preview parameters (preview_token, ctx, ver, loc, key) */
async fetchPreviewContent(params: PreviewParams) {
- const filter = getFilterFromPreviewParams(params);
- const contentTypeName = await this.fetchContentType(
- filter,
+ const input = previewFilter(params);
+ const contentTypeName = await this.getContentType(
+ input,
params.preview_token
);
if (!contentTypeName) {
throw new GraphResponseError(
`No content found for key [${params.key}]. Check that your CMS contains something there`,
- { request: { variables: { filter }, query: FETCH_CONTENT_TYPE_QUERY } }
+ { request: { variables: input, query: FETCH_CONTENT_TYPE_QUERY } }
);
}
const query = createQuery(contentTypeName);
-
- const response = await this.request(
- query,
- { filter },
- params.preview_token
- );
+ const response = await this.request(query, input, params.preview_token);
return decorateWithContext(response?._Content?.item, params);
}
diff --git a/packages/optimizely-cms-sdk/src/index.ts b/packages/optimizely-cms-sdk/src/index.ts
index 226de4b..c806d5c 100644
--- a/packages/optimizely-cms-sdk/src/index.ts
+++ b/packages/optimizely-cms-sdk/src/index.ts
@@ -8,7 +8,7 @@ export {
initContentTypeRegistry,
initDisplayTemplateRegistry,
} from './model/index.js';
-export { GraphClient, getFilterFromPath } from './graph/index.js';
+export { GraphClient } from './graph/index.js';
export { createQuery } from './graph/createQuery.js';
export type { PreviewParams } from './graph/index.js';
export {
diff --git a/samples/nextjs-template/src/app/json/[...slug]/page.tsx b/samples/nextjs-template/src/app/json/[...slug]/page.tsx
index 57caa28..5b8698d 100644
--- a/samples/nextjs-template/src/app/json/[...slug]/page.tsx
+++ b/samples/nextjs-template/src/app/json/[...slug]/page.tsx
@@ -1,8 +1,4 @@
-import {
- getFilterFromPath,
- GraphClient,
- GraphErrors,
-} from '@episerver/cms-sdk';
+import { GraphClient, GraphErrors } from '@episerver/cms-sdk';
import { createQuery } from '@episerver/cms-sdk';
type Props = {
@@ -36,7 +32,7 @@ export default async function Page({ params }: Props) {
// Note: this is shown for demo purposes.
// `fetchContentType` and `createQuery` are not needed
const contentType = await client
- .fetchContentType(getFilterFromPath(path))
+ .fetchContentType(path)
.catch(handleGraphErrors);
const query = createQuery(contentType);
const response = await client.fetchContent(path).catch(handleGraphErrors);