@ringcentral-integration/commons

0.14.0 • Public • Published

ringcentral-js-integration-commons

Build Status Coverage Status Codacy Badge NPM Version

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:

  1. 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.
  2. Common modules, which holds a part of functions provided by api.
  3. 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:

  1. Extends RcModule and set dependencies by decorators
  2. 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

  1. Store can only be set to Root Module.
  2. 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.

  1. All sub modules' _setStore methods are called too.
  2. 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

  1. 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.
  2. 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:

  1. 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
  1. 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',
}
*/
  1. 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

  1. Module decorator ==> registerModule
  2. Lib decorator ==> registerModule
  3. 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:

Readme

Keywords

none

Package Sidebar

Install

npm i @ringcentral-integration/commons

Weekly Downloads

7

Version

0.14.0

License

MIT

Unpacked Size

8.64 MB

Total Files

1170

Last publish

Collaborators

  • u9520107
  • embbnux