Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(SDK): Provide a client.getPage method to fetch page, content, nav using graphQL #31411

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion core-web/libs/sdk/client/project.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"format": ["esm"],
"tsConfig": "libs/sdk/client/tsconfig.lib.json",
"project": "libs/sdk/client/package.json",
"entryFile": "libs/sdk/client/src/lib/editor/sdk-editor-vtl.ts",
"entryFile": "libs/sdk/client/src/lib/deprecated/editor/sdk-editor-vtl.ts",
"external": ["react/jsx-runtime"],
"rollupConfig": "@nrwl/react/plugins/bundle-rollup",
"compiler": "tsc",
Expand Down
12 changes: 6 additions & 6 deletions core-web/libs/sdk/client/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
import { ClientConfig, DotCmsClient } from './lib/client/sdk-js-client';
import { CLIENT_ACTIONS, postMessageToEditor } from './lib/editor/models/client.model';
import { CLIENT_ACTIONS, postMessageToEditor } from './lib/deprecated/editor/models/client.model';
import {
CustomClientParams,
DotCMSPageEditorConfig,
EditorConfig
} from './lib/editor/models/editor.model';
} from './lib/deprecated/editor/models/editor.model';
import {
InlineEditorData,
INLINE_EDITING_EVENT_KEY,
InlineEditEventData
} from './lib/editor/models/inline-event.model';
import { NOTIFY_CLIENT } from './lib/editor/models/listeners.model';
} from './lib/deprecated/editor/models/inline-event.model';
import { NOTIFY_CLIENT } from './lib/deprecated/editor/models/listeners.model';
import {
destroyEditor,
editContentlet,
Expand All @@ -19,7 +18,8 @@ import {
isInsideEditor,
updateNavigation,
initInlineEditing
} from './lib/editor/sdk-editor';
} from './lib/deprecated/editor/sdk-editor';
import { ClientConfig, DotCmsClient } from './lib/deprecated/sdk-js-client';
import { getPageRequestParams, graphqlToPageEntity } from './lib/utils';

export {
Expand Down
96 changes: 96 additions & 0 deletions core-web/libs/sdk/client/src/lib/client/client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
import { Content } from './content/content-api';
import { NavigationClient } from './navigation/navigation-api';
import { PageClient } from './page/page-client';

export type RequestOptions = Omit<RequestInit, 'body' | 'method'>;

export interface DotCMSClientConfig {
/**
* The URL of the dotCMS instance.
* Ensure to include the protocol (http or https).
* @example `https://demo.dotcms.com`
*/
dotcmsUrl: string;

/**
* The id of the site you want to interact with. Defaults to the default site if not provided.
*/
siteId?: string;

/**
* The authentication token for requests.
* Obtainable from the dotCMS UI.
*/
authToken: string;

/**
* Additional options for the fetch request.
*/
requestOptions?: RequestOptions;
}

function parseUrl(url: string): URL | undefined {
try {
return new URL(url);
} catch {
return undefined;
}
}

const defaultConfig: DotCMSClientConfig = {
dotcmsUrl: '',
authToken: '',
requestOptions: {}
};

/**
* `DotCMSClient` provides methods to interact with the DotCMS REST API.
*/
class DotCMSClient {
private config: DotCMSClientConfig;
private requestOptions!: RequestOptions;

dotcmsUrl?: string;
content: Content;
page: PageClient;
nav: NavigationClient;

constructor(config: DotCMSClientConfig = defaultConfig) {
this.config = this.initializeConfig(config);
this.requestOptions = this.initializeRequestOptions(this.config);

// Initialize clients
this.page = new PageClient(this.config, this.requestOptions);
this.content = new Content(this.requestOptions, this.config.dotcmsUrl);
this.nav = new NavigationClient(this.config, this.requestOptions);
}

private initializeConfig(config: DotCMSClientConfig): DotCMSClientConfig {
const dotcmsUrl = parseUrl(config.dotcmsUrl)?.origin ?? '';

if (!dotcmsUrl) {
throw new Error("Invalid configuration - 'dotcmsUrl' must be a valid URL");
}

if (!config.authToken) {
throw new Error("Invalid configuration - 'authToken' is required");
}

return {
...config,
dotcmsUrl
};
}

private initializeRequestOptions(config: DotCMSClientConfig): RequestOptions {
return {
...config.requestOptions,
headers: {
Authorization: `Bearer ${config.authToken}`,
...config.requestOptions?.headers
}
};
}
}

export const dotCMSCreateClient = (config: DotCMSClientConfig) => new DotCMSClient(config);
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
import { CollectionBuilder } from './collection';

import { Equals } from '../../../../query-builder/lucene-syntax';
import { ClientOptions } from '../../../sdk-js-client';
import { ClientOptions } from '../../../deprecated/sdk-js-client';
import { CONTENT_API_URL } from '../../shared/const';
import { SortBy } from '../../shared/types';

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { ClientOptions } from '../../../../deprecated/sdk-js-client';
import { Equals } from '../../../../query-builder/lucene-syntax';
import { QueryBuilder } from '../../../../query-builder/sdk-query-builder';
import { ClientOptions } from '../../../sdk-js-client';
import { CONTENT_API_URL } from '../../shared/const';
import {
GetCollectionResponse,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { CollectionBuilder } from './builders/collection/collection';

import { ClientOptions } from '../sdk-js-client';
import { ClientOptions } from '../../deprecated/sdk-js-client';

/**
* Creates a builder to filter and fetch a collection of content items.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import { DotCMSClientConfig, RequestOptions } from '../client';

interface NavRequestParams {
/**
* The depth of the folder tree to return.
* @example
* `1` returns only the element specified in the path.
* `2` returns the element specified in the path, and if that element is a folder, returns all direct children of that folder.
* `3` returns all children and grandchildren of the element specified in the path.
*/
depth?: number;

/**
* The language ID of content to return.
* @example
* `1` (or unspecified) returns content in the default language of the site.
*/
languageId?: number;
}

export class NavigationClient {
private requestOptions: RequestOptions;

private BASE_URL: string;

constructor(config: DotCMSClientConfig, requestOptions: RequestOptions) {
this.requestOptions = requestOptions;
this.BASE_URL = `${config?.dotcmsUrl}/api/v1/nav`;
}

/**
* Retrieves information about the dotCMS file and folder tree.
* @param {NavigationApiOptions} options - The options for the Navigation API call. Defaults to `{ depth: 0, path: '/', languageId: 1 }`.
* @returns {Promise<unknown>} - A Promise that resolves to the response from the DotCMS API.
* @throws {Error} - Throws an error if the options are not valid.
*/
async get(path: string, params: NavRequestParams): Promise<unknown> {
if (!path) {
throw new Error("The 'path' parameter is required for the Navigation API");
}

const navParams = this.mapToBackendParams(params);

const urlParams = new URLSearchParams(navParams).toString();
const url = `${this.BASE_URL}/${path}${urlParams ? `?${urlParams}` : ''}`;

const response = await fetch(url, this.requestOptions);

if (!response.ok) {
throw new Error(`Failed to fetch navigation data: ${response.statusText}`);
}

return response.json().then((data) => data.entity);
}

private mapToBackendParams(params: NavRequestParams): Record<string, string> {
return {
depth: params.depth ? String(params.depth) : '',
language_id: params.languageId ? String(params.languageId) : ''
};
}
}
142 changes: 142 additions & 0 deletions core-web/libs/sdk/client/src/lib/client/page/page-client.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
import { DotCMSClientConfig, RequestOptions } from '../client';
import { ErrorMessages } from '../models';

/**
* The parameters for the Page API.
* @public
*/
export interface PageRequestParams {
/**
* The id of the site you want to interact with. Defaults to the one from the config if not provided.
*/
siteId?: string;

/**
* The mode of the page you want to retrieve. Defaults to the site's default mode if not provided.
*/
mode?: 'EDIT_MODE' | 'PREVIEW_MODE' | 'LIVE';

/**
* The language id of the page you want to retrieve. Defaults to the site's default language if not provided.
*/
languageId?: number;

/**
* The id of the persona for which you want to retrieve the page.
*/
personaId?: string;

/**
* Whether to fire the rules set on the page.
*/
fireRules?: boolean;

/**
* Allows access to related content via the Relationship fields of contentlets on a Page; 0 (default).
*/
depth?: 0 | 1 | 2 | 3;

/**
* The publish date of the page you want to retrieve.
*/
publishDate?: string;
}

/**
* The private parameters for the Page API.
* @internal
*/
export interface BackendPageParams {
/**
* The id of the site you want to interact with.
*/
hostId?: string;

/**
* The mode of the page you want to retrieve.
*/
mode?: string;

/**
* The id of the persona for which you want to retrieve the page.
*/
'com.dotmarketing.persona.id'?: string;

/**
* The language id of the page you want to retrieve.
*/
language_id?: string;

/**
* Whether to fire the rules set on the page.
*/
fireRules?: string;

/**
* The depth of the page you want to retrieve.
*/
depth?: string;

/**
* The publish date of the page you want to retrieve.
*/
publishDate?: string;
}

export class PageClient {
private requestOptions: RequestOptions;

private BASE_URL: string;

constructor(config: DotCMSClientConfig, requestOptions: RequestOptions) {
this.requestOptions = requestOptions;
this.BASE_URL = `${config?.dotcmsUrl}/api/v1/page`;
}

/**
* Retrieves all the elements of a Page in your dotCMS system in JSON format.
* @param {PageRequestParams} options - The options for the Page API call.
* @returns {Promise<unknown>} - A Promise that resolves to the response from the DotCMS API.
* @throws {Error} - Throws an error if the options are not valid.
*/
async get(path: string, params: PageRequestParams): Promise<unknown> {
if (!path) {
throw new Error("The 'path' parameter is required for the Page API");
}

const pageParams = this.mapToBackendParams(params) as Record<string, string>;
const urlParams = new URLSearchParams(pageParams).toString();
const url = `${this.BASE_URL}/json/${path}${urlParams ? `?${urlParams}` : ''}`;

const response = await fetch(url, this.requestOptions);

if (!response.ok) {
const error = {
status: response.status,
message: ErrorMessages[response.status] || response.statusText
};

console.error(error);
throw error;
}

return response.json().then((data) => data.entity);
}

/**
* Maps public API parameters to private API parameters.
* @param {PageRequestParams} params - The public API parameters.
* @returns {BackendPageParams} - The private API parameters.
*/
private mapToBackendParams(params: PageRequestParams): BackendPageParams {
return {
hostId: params.siteId,
mode: params.mode,
language_id: params.languageId ? String(params.languageId) : '',
'com.dotmarketing.persona.id': params.personaId,
fireRules: params.fireRules ? String(params.fireRules) : '',
depth: params.depth ? String(params.depth) : '',
publishDate: params.publishDate
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { CLIENT_ACTIONS, INITIAL_DOT_UVE, postMessageToEditor } from './models/c
import { DotCMSPageEditorConfig, ReorderMenuConfig } from './models/editor.model';
import { INLINE_EDITING_EVENT_KEY, InlineEditEventData } from './models/inline-event.model';

import { Contentlet } from '../client/content/shared/types';
import { Contentlet } from '../../client/content/shared/types';

/**
* Updates the navigation in the editor.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
/// <reference types="jest" />
/* eslint-disable @typescript-eslint/no-explicit-any */
import { Content } from './content/content-api';
import { ClientConfig, DotCmsClient } from './sdk-js-client';

import { NOTIFY_CLIENT } from '../editor/models/listeners.model';
import * as dotcmsEditor from '../editor/sdk-editor';
import { NOTIFY_CLIENT } from '../../deprecated/editor/models/listeners.model';
import * as dotcmsEditor from '../../deprecated/editor/sdk-editor';
import { Content } from '../content/content-api';
import { ClientConfig, DotCmsClient } from '../sdk-js-client';

global.fetch = jest.fn();

Expand Down
Loading