Skip to main content

Creating the harmonizing block

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

Declaring dependencies

The very first thing we need to do is to add the required modules to the block dependencies. This block 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 TicketsSummaryBlockModule {
static register(_config: ApiConfig): DynamicModule {
return {
module: TicketsSummaryBlockModule,
// adding `Tickets.Service` as a provider to this block
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 block. Let's open the ./tickets-summary.model.ts and a few properties:

export class TicketsSummaryBlock extends Block.Block {
// the `__typename` property uniquely identifies this block
__typename!: 'TicketsSummaryBlock';
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 block 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:

getTicketsSummaryBlock(query: GetTicketsSummaryBlockQuery):Observable<TicketsSummaryBlock> {
// fetching the static content
const cms = this.cmsService.getTicketsSummaryBlock(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.TicketsSummaryBlock.TicketsSummaryBlock,
tickets: Tickets.Model.Tickets,
locale: string,
): TicketsSummaryBlock => {
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: 'TicketsSummaryBlock',
// every block 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 block list

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

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

...

export type Blocks =
...
| TicketsSummary.Model.TicketsSummaryBlock['__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 block 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 block is declared in the ./index.ts file (default '/blocks/tickets-summary'),

which gives a final URL of http://localhost:3001/api/blocks/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