Skip to main content

Creating the harmonizing component

We can start by using a generator to generate a scaffolded component, to which you will be adding your own implementation. After you provide the component name (using whatever case you want, e.g. tickets summary), it will create a new folder with the files: ./apps/api-harmonization/src/components/tickets-summary/.

Declaring dependencies

The very first thing we need to do is to add the required modules to the component dependencies. This component relies on:

  • CMS module to fetch static content, like labels,
  • Tickets module to fetch the tickets.

Generated code already includes the CMS dependency, but we still need to add the Tickets. Let's open ./tickets-summary.module.ts and define it:

// importing the `Tickets` module
import { CMS, Tickets } from '../../models';

@Module({})
export class TicketsSummaryComponentModule {
static register(_config: ApiConfig): DynamicModule {
return {
module: TicketsSummaryComponentModule,
// adding `Tickets.Service` as a provider to this component
providers: [TicketsSummaryService, CMS.Service, Tickets.Service],
controllers: [TicketsSummaryController],
exports: [TicketsSummaryService],
};
}
}

Defining data models

The next thing we need to do, is to define the aggregated data model for the component. Let's open the ./tickets-summary.model.ts and a few properties:

export class TicketsSummaryComponent extends Component.Component {
// the `__typename` property uniquely identifies this component
__typename!: 'TicketsSummaryComponent';
title!: string;
tickets!: {
open: TicketData;
closed: TicketData;
latest: TicketDetailsData;
};
}

export type TicketData = {
label: string;
value: number;
};

export type TicketDetailsData = {
title: string;
topic: {
// for value we re-use the type from the Ticket
value: Tickets.Model.Ticket['topic'];
// and label is a simple string
label: string;
};
type: {
value: Tickets.Model.Ticket['type'];
label: string;
};
editDate: {
label: string;
};
};

Since this component will not require any parameters, we don't need to modify the model for the incoming request, and the one autogenerated in /tickets-summary.request.ts will suffice.

Fetching data

With that ready, we can now fetch the data in ./tickets-summary.service.ts. Start by adding the Tickets.Service:

import { CMS, Tickets } from '../../models';

...

constructor(
private readonly cmsService: CMS.Service,
private readonly ticketsService: Tickets.Service,
) {}

that we can use to fetch the tickets in the main method:

getTicketsSummaryComponent(query: GetTicketsSummaryComponentQuery):Observable<TicketsSummaryComponent> {
// fetching the static content
const cms = this.cmsService.getTicketsSummaryComponent(query);
// fetching tickets - let's assume that the summary takes
// into the account 1000 latest tickets
const tickets = this.ticketsService.getTicketList({ limit: 1000 });

// we don't need any orchestration here, so
// we can make both requests at the same time
return forkJoin([cms, tickets]).pipe(map(([cms, tickets]) =>
// once we have both responses, we pass them to the mapper
mapTicketsSummary(cms, tickets, query.locale)));
}

Mapping data

The last step is to aggregate the data from the CMS and from the tickets integration into one object for the frontend app. To do that, let's edit ./tickets-summary.mapper.ts file and map the data:

import { CMS, Tickets } from '../../models';

export const mapTicketsSummary = (
cms: CMS.Model.TicketsSummaryComponent.TicketsSummaryComponent,
tickets: Tickets.Model.Tickets,
locale: string,
): TicketsSummaryComponent => {
const latestTicket = tickets.data[0];

// to get the number of open/closed tickets,
// we can just filter them by status
const countTicketsByStatus = (status: Tickets.Model.TicketStatus): number => {
return tickets.data.filter((ticket) => ticket.status === status).length;
};

return {
__typename: 'TicketsSummaryComponent',
// every component should be identified by it's ID
id: cms.id,
title: cms.title,
tickets: {
open: {
label: cms.labels.open,
value: countTicketsByStatus('OPEN'),
},
closed: {
label: cms.labels.closed,
value: countTicketsByStatus('CLOSED'),
},
latest: {
title: cms.labels.latest,
topic: {
// pick the correct label from the CMS
label: cms.fieldMapping.topic?.[latestTicket.topic],
// but let's also pass the value as well
value: latestTicket.topic,
},
type: {
label: cms.fieldMapping.type?.[latestTicket.type],
value: latestTicket.type,
},
editDate: {
// let's format the date for a more user-friendly format
value: formatDateRelative(latestTicket.updatedAt, locale, cms.labels.today, cms.labels.yesterday),
},
},
},
};
};

Extending component list

As a last step, we need to add the new component's name to the Components type so that the frontend app would be able to resolve it correctly (when choosing which component to render). Let's open the apps/api-harmonization/src/modules/page/page.model.ts file and add the component's __typename to the list:

import {
...
TicketsSummary,
} from '@o2s/api-harmonization/components';

...

export type Components =
...
| TicketsSummary.Model.TicketsSummaryComponent['__typename'];

Testing the API

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

  • 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/components/tickets-summary.

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).

Let's again use Postman for this:

postman-get-page.png