Skip to main content

Extending the CMS integration

Most components usually need some kind of static content (like titles or labels) that do not come from any kind of backend APIs. There are actually two ways of fetching that content:

  1. Simply fetching it directly in the harmonizing component - which is simpler, but less generic and strongly coupled with the specific data source,
  2. Or extending the CMS integration with new models and services (specific to newly created component), which makes it possible to re-use it even after switching the CMS integration to another.

In case of the TicketSummary component let's use the second approach and see how we can extend an existing mocked integration.

Customization of existing integrations is done by creating a new, custom integration that will add new features to the one it's extending.

Generate the integration extension

We can start by using the integration generator and choosing the cms option when asked which modules should be included. Let's also call this integration custom-cms, which will create a new package in the ./packages/integrations/custom-cms folder.

Extend the model

Firstly, let's create the ./src/modules/cms/cms.model.ts file where we will include the new models for our component. While we could add the models there directly, it's usually not the best way as components usually require several classes or types, and if, in the future, we decide on adding more components, all models will be mixed together.

Instead, let's place our models in the ./src/modules/cms/models/components/tickets-summary.model.ts, which we can re-export inside the cms.model.ts:

export * as TicketsSummaryComponent from './models/components/tickets-summary.model';

Now, let's add the normalized component model in the tickets-summary.model.ts:

import { Models, Tickets } from '@o2s/framework/modules';

export class TicketsSummaryComponent extends Models.Component.Component {
title!: string;
labels!: {
open: string;
closed: string;
latest: string;
today: string;
yesterday: string;
};
// this field stores the mapping of enums values
// into more user-friendly labels
// e.g. `IN_PROGRESSS: 'New response'`
fieldMapping!: Models.Mapping.Mapping<Tickets.Model.Ticket>;
}

We also need to re-export the original models from the base module from the framework. Let's open the ./src/modules/cms/cms.model.ts and, in addition to the models related to the new component, we need to also add all the others:

import { CMS } from '@o2s/framework/modules';

export * as TicketsSummaryComponent from './models/components/tickets-summary.model';

export import Page = CMS.Model.Page;
export import LoginPage = CMS.Model.LoginPage;
export import Footer = CMS.Model.Footer;
export import Header = CMS.Model.Header;
export import FaqComponent = CMS.Model.FaqComponent;
export import TicketListComponent = CMS.Model.TicketListComponent;
export import TicketDetailsComponent = CMS.Model.TicketDetailsComponent;
export import NotificationListComponent = CMS.Model.NotificationListComponent;
export import NotificationDetailsComponent = CMS.Model.NotificationDetailsComponent;
export import InvoiceListComponent = CMS.Model.InvoiceListComponent;
export import PaymentsSummaryComponent = CMS.Model.PaymentsSummaryComponent;
export import PaymentsHistoryComponent = CMS.Model.PaymentsHistoryComponent;
export import ArticleListComponent = CMS.Model.ArticleListComponent;
export import ArticleDetailsComponent = CMS.Model.ArticleDetailsComponent;
info

We realize that having to explicitly re-export every model might be very cumbersome, especially for more complex modules like the CMS. We plan to improve this behaviour to make it more developer-friendly.

The last step is to edit the main entrypoint of this package ./src/modules/cms/index.ts and export the necessary service, model and request:

import { CMS } from '@o2s/framework/modules';

export { CmsService as Service } from './cms.service';

export import Request = CMS.Request;

export * as Model from './cms.model';

Add method to service

The auto-generated service assumes we want to implement it from scratch - but in our case, we want to extend another one. Let's firstly install that integrations as a dependency:

npm install @o2s/integrations.mocked --workspace=@o2s/integrations.custom-cms

Now we can edit the cms.service.ts file

import { TicketsSummaryComponent } from './cms.model'

// let's mock the response
// notice that it's format is not exactly like the normalized one
const MOCK = {
id: 'tickets-summary-1',
title: 'Summary of your tickets',
open: 'Open tickets',
closed: 'Closed tickets',
latest: 'Your latest ticket',
today: 'Today',
yesterday: 'Yesterday',
fieldMapping: [
{
name: 'topic',
values: [
{ key: 'TOPIC_1', value: 'Topic 1' },
{ key: 'TOPIC_2', value: 'Topic 2' }
]
},
{
name: 'type',
values: [
{ key: 'TYPE_1', value: 'Type 1' },
{ key: 'TYPE_2', value: 'Type 2' }
]
},
{
name: 'status',
values: [
{ key: 'OPEN', value: 'Under consideration' },
{ key: 'CLOSED', value: 'Resolved' },
{ key: 'IN_PROGRESS', value: 'New response' }
]
},
],
};

@Injectable()
export class CmsService extends Integration.CMS.Service {
getTicketsSummaryComponent(): Observable<TicketsSummaryComponent.TicketsSummaryComponent> {
// implement your own data fetching method
// it can be anything, http/graphql request, database connection, or read from disk
const response = Promise.resolve(MOCK);

return from(response).pipe(map((data) => mapTicketSummaryComponent(data)));
}
}

Normalize the response

We still need to normalize the response. To do that, let's create the ./mappers/mapTicketsSummary.ts file:

import { Models } from '@o2s/framework/modules';

import { TicketsSummaryComponent } from '../cms.model';
// this is temporary, ideally the typings
// should be taken from a "real" source
// like auto-generated types from e.g. OpenApi definition
import { MOCK } from '../cms.service';

export const mapTicketSummaryComponent = (data: typeof MOCK): TicketsSummaryComponent.TicketsSummaryComponent => {
// in this case the mapping is quite simple,
// but depending in the API it can also be more complex
return {
id: data.id,
title: data.title,
labels: {
open: data.open,
closed: data.closed,
latest: data.latest,
today: data.today,
yesterday: data.yesterday,
},
// we need to transform the data that was in array format
// into an object format
fieldMapping: mapFields(data.fieldMapping),
};
};

const mapFields = <T>(
data: (typeof MOCK)['fieldMapping'],
): TicketsSummaryComponent.TicketsSummaryComponent['fieldMapping'] => {
return data.reduce(
(acc, field) => ({
...acc,
[field.name]: field.values.reduce(
(acc, item) => ({
...acc,
[item.key]: item.value,
}),
{} as { [key: string]: string },
),
}),
{} as Models.Mapping.Mapping<T>,
);
};

Adding a new component to a page

One more thing that will be required to actually see the component on the frontend, is to add it to some page. Let's open the cms.service.ts file again, and override the getPage method:

getPage(options: CMS.Request.GetCmsPageParams) {
// we can use the original method to fetch page data
return super.getPage(options).pipe(
map((data) => {
// let's add the new component to the page at `/` slug (e.g. the dashboard page)
if (data?.slug === '/') {
// deep clone of the original object
const newData = JSON.parse(JSON.stringify(data)) as typeof data;
switch (newData.template.__typename) {
// we need to check the page template
// to decide which slot to use
case 'TwoColumnTemplate':
// we add the component to a slot, and
// component definition consists of just its ID and a name
newData.template.slots.top.push({
__typename: 'TicketsSummaryComponent',
id: 'tickets-summary-1',
});
return newData;
}
}
return data;
}),
);
}

Switch the integration

Finally, the last thing is to switch the CMS integration in the API Harmonization server. Firstly, let's install it:

npm install @o2s/integrations.custom-cms --workspace=@o2s/api-harmonization

Now we can replace the package within the apps/api-harmonization/src/models/cms.ts file:

import { Config, Integration } from '@o2s/integrations.mocked/integration';

with

import { Config, Integration } from '@o2s/integrations.custom-cms/integration';

Testing the API

Once the model, service and mapper are ready, we should make sure that it actually works. To do that let's query the API Harmonization server for this component:

  • the localhost port is defined by the PORT env variable (default 3001),
  • there is an optional prefix that is added, defined by the API_PREFIX env variable (default /api),
  • finally, the path of the component is declared in the ./index.ts file (default '/components/tickets-summary'),

which gives a final URL of http://localhost:3001/api/page?slug=/.

There are also some mandatory headers that we need to add:

  • Authorization in the format of bearer token (e.g. Bearer eyJhbG...),
  • X-Locale in the format of IETF language tag (e.g. en).

For the sake of convenience, let's use Postman for this:

postman-get-page.png