Skip to main content

Custom modules

Most of the integrations available in Falcon Platform are implemented as extensions + modules. When you want to add new features or change the existing behavior you'll need to add Extension and module that implements features for that Extension.

Creating new Module

To create the most basic (an empty) module it is enough to export implementation of FalconModule abstract class. Following module will be correctly loaded and registered in Falcon Server, even it does nothing.

import { FalconModule } from '@deity/falcon-server-env';

export class CustomModule extends FalconModule<any> {
constructor(config: any) {
super(config);
}
}

However, as described in about, Falcon Module can provide implementation of various common services and define any custom services too. The following examples show how to register a particular one, but it is perfectly fine, to define any number of them, inside one Falcon Module. So feel free to mix them in order to provide consistent business feature implementation.

Adding GraphQL resolver

Lets assume that you already have registered corresponding Falcon Extensions which defines following GraphQl schema:

extend type Query {
fooList: FooList!
}

type FooList {
items: [Foo!]!
}

type Foo {
id: ID!
name: String!
}

To add resolver implementation of fooList query, you need to define resolver implementation via gqlResolvers method:

import { FalconModule, GqlResolversMap } from '@deity/falcon-server-env';

export class CustomModule extends FalconModule<{}> {
constructor(config: {}) {
super(config);
}

gqlResolvers(): GqlResolversMap {
const resolversMap: GqlResolversMap = {
Query: {
fooList: () => [
{ id: 1, name: 'foo 1' },
{ id: 2, name: 'foo 2' },
{ id: 3, name: 'foo 3' }
]
}
};

return this.mergeGqlResolvers(super.gqlResolvers(), resolversMap);
}
}

Adding GraphQL Type resolver

Very ofter there is an need to do a mapping of data returned from particular API to model defined via GraphQL schema. Resolvers map allows to define GraphQL Type resolver, which make possible to define mapping once for entire schema, no matter how many queries and/or mutations will return specific type, result of those will be mapped according to defined Type resolver.

Lest say there is a need to capitalize Foo.name. To achieve that Foo.name GraphQL Type resolver should be added into resolversMap:

const resolversMap: GqlResolversMap = {
Query: {
fooList: () => [
{ id: 1, name: 'foo 1' },
{ id: 2, name: 'foo 2' },
{ id: 3, name: 'foo 3' }
]
},
Foo: {
name: root => toTitleCase(root.name)
}
};

Using DataSource

During development, amount of GraphQL resolvers, data manipulation mappers, business logic functions is growing relay fast. And there is a need to organize code base somehow in a better way. Here Data Sources appears, to read more about idea behind it go into Data Sources section. Following example shows them in action:

import { FalconModule, GqlResolversMap, GraphQLContext } from '@deity/falcon-server-env';
import { FooDataSource } from './FooDataSource';

export type CustomGraphQLContext = GraphQLContext<{ foo: FooDataSource }>;
export class CustomModule extends FalconModule<{}> {
constructor(config: {}) {
super(config);
}

servicesRegistry(registry) {
super.servicesRegistry(registry);

registry.bind('FooDataSource').toDataSource(FooDataSource);
}

gqlResolvers(): GqlResolversMap<CustomGraphQLContext> {
const resolversMap: GqlResolversMap<CustomGraphQLContext> = {
Query: {
fooList: (root, args, context) => context.dataSources.foo.getFooList()
},
Foo: {
name: root => toTitleCase(root.name)
}
};

return this.mergeGqlResolvers(super.gqlResolvers(), resolversMap);
}
}

Adding Module Common Services

Besides Data Sources, Falcon Module can have Event Handlers and Rest Endpoint Handlers. To find out how to implements them please see:

Once you implement any of these listed above, in order to register them, you need to create binding in servicesRegistry method:

import { FalconModule, GqlResolversMap, GraphQLContext } from '@deity/falcon-server-env';
import { FooDataSource } from './FooDataSource';
import { FooRestEndpointHandler } from './FooRestEndpointHandler';
import { AfterInitializationEventHandler } from './AfterInitializationEventHandler';

export type CustomGraphQLContext = GraphQLContext<{ foo: FooDataSource }>;
export class CustomModule extends FalconModule<{}> {
constructor(config: {}) {
super(config);
}

servicesRegistry(registry) {
super.servicesRegistry(registry);

registry.bind('FooDataSource').toDataSource(FooDataSource);
registry.bind('FooRestEndpointHandler').toEndpointManager(FooRestEndpointHandler);
registry.bind('AfterInitializationEventHandler').toEventHandler(AfterInitializationEventHandler);
}
}

Adding Module Custom Service

Falcon Module services registry allows to define any custom service, not only Data Sources, Rest Endpoint Handlers or Event Handlers. We use that feature inside @deity/falcon-payment-service-module in order to implement PaymentServiceClient. Following example shows how to do this

import { injectable } from 'inversify';
import { FalconModule, FalconModuleRegistryProps } from '@deity/falcon-server-env';

@injectable()
export class FooClient {
getFooById(id: number) {
return { id };
}
}

export class CustomModule extends FalconModule<{}> {
servicesRegistry(registry: FalconModuleRegistryProps) {
super.servicesRegistry(registry);

registry.bind('FooClient').to(FooMapper);
}
}

Bare in mind, since custom services are not recognizable by Falcon Server, these will not be used until at least one of the common services (Data Source, Rest Endpoint Handler or Event Handler) will use them.

Creating new Module with services auto-discovery

Falcon Module common services auto-discovery feature, lets you just export classes which extends:

Then Falcon Server during the startup will analyse exported by particular module class, and will do the required services registration by himself.

import { FooDataSource } from './FooDataSource';
import { FooRestEndpointHandler } from './FooRestEndpointHandler';
import { AfterInitializationEventHandler } from './AfterInitializationEventHandler';

export { FooDataSource, FooRestEndpointHandler, AfterInitializationEventHandler };

Extending Module

Extending existing module with manual binding example

import { injectable } from 'inversify';
import {
CommerceToolsModule: FalconCommerceToolsModule,
CommercetoolsDataSource: FalconCommercetoolsDataSource
} from `@deity/falcon-commercetools-module`;

@injectable()
class CommercetoolsDataSource extends FalconCommercetoolsDataSource {
project(...args){
console.log('Fetching project');

return super.project(...args);
}
}

export class CommerceToolsModule extends CommerceToolsModule {
servicesRegistry(registry) {
super.servicesRegistry(registry);

rebind('CommercetoolsDataSource').toDataSource(CommercetoolsDataSource);
}

gqlResolvers() {
return this.mergeGqlResolvers(super.gqlResolvers(), {
Project: {
name: (root, params, context, info) => {
return root.name.toUpperCase()
}
}
});
}
}

Module common services auto-discovery

As mentioned earlier Falcon Server 3 modules can expose multiple things at once.

The easiest way to extend Falcon Server with custom module is to extend the classes provided by Falcon Server and export these from a module. During startup Falcon Server will read everything from within that module and base on the types of exported things it will register these as proper things in IOC container.

Using Service Registry bindings

Falcon Module services registry is powerful tool when it comes into code organization. As described in introduction to Falcon Module API it lets you extract any kind of dependency and also have access to dependencies defined via FalconServer itself.

In order to use any of registered dependency you need to resolve them via constructor argument injection using @inject() decorator.

import { injectable, inject } from 'inversify';
import { FalconModule, FalconModuleRegistryProps, DataSource } from '@deity/falcon-server-env';

@injectable()
class FooMapper {
mapFoo(foo) {
return foo; // perform some mapping
}
}

@injectable()
class FooDataSource extends DataSource {
constructor(@inject('fetch') protected fetch, @inject('FooMapper') protected mapper: FooMapper) {}

async getFooById(id: string) {
const response = await this.fetch(`https://foo.com/api/foo/${id}`);

return this.mapper.mapFoo(response);
}
}

export class FooModule extends FalconModule {
servicesRegistry(registry: FalconModuleRegistryProps) {
super.servicesRegistry(registry);

registry.bind('FooDataSource').toDataSource(FooDataSource);
registry.bind('FooMapper').to(FooMapper);
}
}

To see more advanced examples please see Custom Modules section.

To see full list of services provided by default via Falcon Server please see Falcon Server services section.

Cross Modules services access

Falcon Server injectable services

Fetch

Logger

Config

Container

Event Emitter