@ringcentral-integration/commons
v0.15.0
Published
RingCentral Integration Common Javascript Core Library
Downloads
185
Readme
ringcentral-js-integration-commons
Introduction
RingCentral integration common library aims to provide reusable modules to allow developers to integrate RingCentral unified communication service into third party processes or tools more easily.
This project is built based on RingCentral Client and Redux. The basic idea is to wrap RingCentral REST API into highly reusable modules based on common application scenarios and provide an unified application state.
Get Started
To use this library, please follow below steps
Install from NPM
npm install @ringcentral-integration/commons
Create your own Phone object by adding modules
import { combineReducers, createStore } from 'redux';
class Phone extends RcModule {
constructor() {
super();
this.addModule('${module}');
this._reducer = combineReducers({
${moduleName}: this.${module}.reducer,
});
}
}
const phone = new Phone();
const store = createStore(phone.reducer);
phone.setStore(store);
Now you are armed with a set of RingCentral services.
Notice: If you have no idea what this section is talking about, which is the case most of the time, please reference next section for more info.
High Level Concept
Module
Module is the basic component, which usually wraps one ore more API calls to provide common used features. A good example to understand module is to compare Call Log related features in RingCentral JS Client
and RingCentral Integration Common Library
. In RingCentral JS Client
, you can get call log with following code
client.account().extension().callLog().list({
...param
})
There are three kind of modules:
- Root module, which provides all other modules for app. Root module also have a duty to provide redux store, this will be discussed more in later sections.
- Common modules, which holds a part of functions provided by api.
- Custom modules, when common modules can not fulfill your need, you develop one yourself.
Root Module
All needed common modules which are provided by @ringcentral-integration/commons
can be listed here. And also other modules composed by you.
import { Alert } from '@ringcentral-integration/commons/modules/Alert';
import { Brand } from '@ringcentral-integration/commons/modules/Brand';
// import other libs
// other variables initialized here
@ModuleFactory({
providers: [
{ provide: 'Alert', useClass: Alert },
{ provide: 'Brand', useClass: Brand },
{
provide: 'SdkConfig',
useValue: {
...apiConfig,
cachePrefix: `sdk-${prefix}`,
clearCacheOnRefreshError: false,
},
},
{
provide: 'ContactSources',
useFactory: ({ glipContacts }) =>
[glipContacts],
deps: ['GlipContacts']
},
]
})
export default class BaseRoot extends RcModule {}
Custom Modules
There are two ways to custom modules:
- Extends
RcModule
and set dependencies by decorators - Extends directly a build-in module
Extends RcModule
@Module({
deps: [
'CompanyContacts',
'GlipPersons',
{ dep: 'GlipContactsOptions', optional: true }
]
})
export default class GlipContacts extends RcModule {
}
Extends Build-in Module
@Module({
deps: []
})
export default class NewGlipGroups extends GlipGroups {
}
Module Providers
Two steps to set providers for root module.
Step 1. Set common moduels
@ModuleFactory({
providers: [
{ provide: 'Alert', useClass: Alert },
{ provide: 'Brand', useClass: Brand },
]
})
export default class BaseRoot extends RcModule {
initialize() {
// ...initialize stuff here
}
}
Step 2. Set self composed modules by HOC
function createRootModule({ apiConfig, redirectUri }) {
@ModuleFactory({
providers: [
{provide: 'SDKConfig', useValue: {...apiConfig, cachePrefix: `sdk-${prefex}`}},
{ provide: 'OAuthOptions', useValue: { redirectUri } },
]
})
class Root extends RcModule {}
return Root.create();
}
How It Works
There's no action creator in modules. All actions are in an Enum instance.
RcModule
's default implementation included a method: initialize()
, it's definition is:
initialize() {
this.store.subscribe(() => this._onStateChange());
}
Generally you dont have to do any thing to change this implementation. Just get your _onStateChange
method done.
_onStateChange() {
if (this._shouldInit()) {
this.store.dispatch({
type: this.actionTypes.initSuccess,
});
} else if (this._shouldReset()) {
this.store.dispatch({
type: this.actionTypes.resetSuccess,
});
}
}
_shouldInit()
checks if all dependencies are initialized and when all done, send an initSuccess
action.
This action will inform the store that your own module is initialized successfuly. Although the action is named initSuccess
but it has it's prefix, which is your module's name or something else you specified.
_shouleReset()
is the same.
Selector
Get what you want from state.
Module Store
- Store can only be set to Root Module.
- Store can noly be set once
setStore(store) {
if (this._modulePath !== 'root') {
throw new Error('setStore should only be called on root module');
}
if (!store) {
throw new Error('setStore must accept a store object');
}
if (this._store) {
throw new Error('setStore should only be called once');
}
this._setStore(store);
this._initModule();
}
_setStore(store) {
this._store = store;
for (const subModule in this) {
if (
Object.prototype.hasOwnProperty.call(this, subModule) &&
this[subModule] instanceof RcModule
) {
this[subModule]._setStore(store);
}
}
}
setStore
methods also triggers module initialization.
- All sub modules'
_setStore
methods are called too. - All sub modules. And in every module, the
initizlize
methods is called.
setStore(store) {
// ...
this._setStore(store);
this._initModule();
}
_initModule() {
if (
!this._suppressInit &&
!this._initialized
) {
this._initialized = true;
this.initialize();
}
for (const subModule in this) {
if (
Object.prototype.hasOwnProperty.call(this, subModule) &&
this[subModule] instanceof RcModule
) {
this[subModule]._initModule();
}
}
}
initialize() {
this.store.subscribe(() => this._onStateChange());
}
In initialize
method, it may subscribe the store to state change.
Module's create method
- Get all modules' value (if this module is
ValueProvider
) or instances. In this step. all providers are resolved and set them to Root Module instance, key is the provider's token and instance is the provider's instance. - Get all sub modules' reducers, combine them then set it to Root Module's
_reducer
field.
Phone
Phone is an aggregator of modules which provides a full functional object in application level. As different application requires different features, the Phone object needs to be constructed in application level by adding required modules. A typical way to create Phone object is something like below
class Phone extends RcModule {
constructor() {
super();
this.addModule('${module}');
this._reducer = combineReducers({
${moduleName}: this.${module}.reducer,
});
}
}
const phone = new Phone();
Store
As Phone object is built up with Redux, after Phone object is created, you need to use following code to create Redux store
const store = createStore(phone.reducer);
phone.setStore(store);
Action
Actions are defined with ObjectMap class, which will be discussed in next section.
export const ObjectMap.prefixKeys([
...ObjectMap.keys(moduleActionTypes),
'alert',
'dismiss',
'dismissAll',
], 'alert');
The prefixKeys function is a factory function to create ObjectMap instances. The instance will have the object shape of:
{
alert: 'alert-alert',
dismiss: 'alert-dismiss',
dismissAll: 'alert-dismissAll',
[key in moduleActionTypes]: `alert-${moduleActionTypes[key]}`,
}
You can use it like:
export function getMessagesReducer(types) {
return (state = [], type) => {
switch (type) {
case types.alert:
return [
...state,
{
// ...
},
];
case types.dismiss:
return // state
case types.dismissAll:
return [];
default:
return state;
}
};
}
Please notice getMessagesReducer
is a high order function. It will return a function which is the reducer you want.
Every module has its needed actions defined within the module with Enum
.
ObjectMap
ObjectMap is a class representation of a read-only dictionary implemented with an object mapping. There are several uses of ObjectMaps:
- Define a lookup table with an object definition:
const table = ObjectMap.fromObject({
foo: 'bar',
alpha: 1,
beta: 2,
isObject: true,
} as const);
// with as const in typescript, the instance will clearly show the correct key-value relationship in its shape
- Define a set of values by using the value as the key:
const callResults = ObjectMap.fromKeys([
'hangUp',
'missed',
'voicemail',
'completed',
]);
/* The shape of the object would be as the following, with complete key-value shape in the typescript typing system.
{
hangUp: 'hangUp',
missed: 'missed',
voicemail: 'voicemail',
completed: 'completed',
}
*/
- Prefix a set of values:
const actionTypes = ObjectMap.prefixKeys([
'init',
'initSuccess',
'reset',
'resetSuccess'
], 'authModule');
/* The shape of the object would be the following:
{
init: 'authModule-init',
initSuccess: 'authModule-initSuccess',
reset: 'authModule-reset',
resetSuccess: 'authModule-resetSuccess',
}
However, in the typing system, the shape of the object is:
{
init: string;
initSuccess: string;
reset: string;
resetSuccess: string;
}
DI
- Module decorator ==> registerModule
- Lib decorator ==> registerModule
- ModuleFactory decorator ==> registerModuleFactory
What's in common is that all these registered modules are stored in a map instance with the Class
as key
and metadata
as the value.
When does DI work is when Root Module called create()
method. In the method, Injector
class started to handle those Module
, Library
and ModuleFactory
decorator and to make dependency injection by its addModule
method
.
In the same time, modules' reducers and state are also initialized and set to module by the methods prefixed by _.
Module
When one module has dependencies on other modules.
How to use:
@Module({
deps: [{ dep: 'AlertOptions', optional: true }]
})
export default class Alert extends RcModule {
// ..
}
In the map, key is `Alert` class and value is `{ dep: 'AlertOptions', optional: true }.
Provider
ModuleFactory
This decorator can be used on any kind modules, no matter it's root module or not.
Dependency Injection
Please refer to Dependency Injection for more details.
Contribution
Please fork the project and read the following: