[![codecov](https://codecov.io/gh/bringg/dashboard-sdk/branch/master/graph/badge.svg?token=T61tuD5MYe)](https://codecov.io/gh/bringg/dashboard-sdk)

# BRINGG DASHBOARD SDK

## Examples:

#### Web ( >= ES5 )
```html
<!doctype html>
<html lang="en">
<head>
  <!-- ... -->
  <script src="/bringg-dashboard-sdk.js"></script>
</head>
<body>
<!-- ... -->
<script>
BringgDashboardSDK.initWithEmail(email, password).then(bringgDashboardSDK => {
  bringgDashboardSDK.merchant.get().then(console.log).catch(console.error);
  bringgDashboardSDK.merchantConfiguration.get().then(console.log).catch(console.error);
});

BringgDashboardSDK.initWithAuthToken(region, authToken).then(bringgDashboardSDK => {
  bringgDashboardSDK.merchant.get().then(console.log).catch(console.error);
  bringgDashboardSDK.merchantConfiguration.get().then(console.log).catch(console.error);
});
</script>
</body>
</html>
```

#### Node.js ( >= ES5 )
```javascript
const BringgDashboardSDK = require("bringg-dashboard-sdk");

BringgDashboardSDK.initWithEmail(email, password).then(bringgDashboardSDK => {
  bringgDashboardSDK.merchant.get().then(console.log).catch(console.error);
  bringgDashboardSDK.merchantConfiguration.get().then(console.log).catch(console.error);
});

BringgDashboardSDK.initWithAuthToken(region, authToken).then(bringgDashboardSDK => {
  bringgDashboardSDK.merchant.get().then(console.log).catch(console.error);
  bringgDashboardSDK.merchantConfiguration.get().then(console.log).catch(console.error);
});
```

#### TypeScript ( >= 2.6 )
```javascript
import BringgDashboardSDK = require("bringg-dashboard-sdk");

async function runAwait() {
  try {
    const bringgDashboardSDK = await BringgDashboardSDK.initWithEmail(email, password);
    console.log(await bringgDashboardSDK.merchant.get());
    console.log(await bringgDashboardSDK.merchantConfiguration.get());
  } catch (e) {
    console.error(e);
  }

  try {
    const bringgDashboardSDK = await BringgDashboardSDK.initWithAuthToken(region, authToken);
    console.log(await bringgDashboardSDK.merchant.get());
    console.log(await bringgDashboardSDK.merchantConfiguration.get());
  } catch (e) {
    console.error(e);
  }
}

runAwait();
```

### Running tests

```bash
yarn test
```

### Running test in watch mode

```bash
yarn jest
```

### Linting your code with tsLint

```bash
yarn lint
```

### Fixing your code with prettier

```bash
yarn prettier
```

### Linting your code and fixing it with one command(will run lint and prettier)

```bash
yarn fix
```

### Compile your awesome code for production

```bash
yarn prod
```

# DashboardSDK - Guidelines to writing new entities - 10/4/19

Each entity should have 3 parts - Public API, Entity Store, Http Service.


# Previous Status
For example we want to expose ```VehicleType``` to our end users,
we go to the sdk and first add ```Actions``` folder.

<details>
<summary>Code Example</summary>

```javascript
export enum VehicleTypesActionTypes {
  GET_VEHICLE_TYPES = 'GET_VEHICLE_TYPES',
  MARK_VEHICLE_TYPES_FETCHED = 'MARK_VEHICLE_TYPES_FETCHED',
  SAVE_REQUEST_ERROR = 'SAVE_REQUEST_ERROR',
}

export const Routes = {
  GET_VEHICLE_TYPES: new Route('/vehicle_types', HttpMethod.GET),
};

export function storeVehicleTypesAction(vehicleTypes: VehicleType[]): Actions<VehicleTypesActionTypes> {
  return {
    type: VehicleTypesActionTypes.GET_VEHICLE_TYPES,
    payload: vehicleTypes,
  };
}

export function markVehicleTypesFetchedAction(isFetched: boolean): Actions<VehicleTypesActionTypes> {
  return {
    type: VehicleTypesActionTypes.MARK_VEHICLE_TYPES_FETCHED,
    payload: isFetched,
  };
}

export function saveRequestErrorAction(requestType: VehicleTypesRequests, error: Error): Actions<VehicleTypesActionTypes> {
  return {
    type: VehicleTypesActionTypes.SAVE_REQUEST_ERROR,
    payload: {
      requestType,
      error,
    },
  };
}

export const vehicleTypessExtractor = (response: VehicleTypesResponse) => response.vehicle_types;

export const getVehicleTypesRequestAction = (sessionService: SessionBasedService): any => async (dispatch): Promise<Actions<VehicleTypesActionTypes>> => {
  {
    try {
      const vehicleTypes: VehicleType[] = await sessionService.handleAuthorizedRequest<VehicleTypesResponse, VehicleType[]>(
        Routes.GET_VEHICLE_TYPES,
        vehicleTypessExtractor,
        BringgException.serviceException('failed to get vehicles'),
      );

      dispatch(markVehicleTypesFetchedAction(true));

      return await dispatch(storeVehicleTypesAction(vehicleTypes));
    } catch (e) {
      await dispatch(saveRequestErrorAction(VehicleTypesRequests.GET_VEHICLE_TYPES, e));
      throw e;
    }
  }
};

```
</details>

We continue to next folder ```Reducers```.

<details>
<summary>Code Example</summary>

```javascript
export const initState = fromJS({
  items: {},
  requests: {},
  fetched: false,
});

type requestErrorLog = requestErrors<VehicleTypesRequests>;

export type vehicleTypesState = ImmutableMap<{
  items: Map<number, VehicleType>;
  requests: ImmutableMap<requestErrorLog>;
  fetched: boolean;
}>;

export const storeVehicleTypes = (state: vehicleTypesState, vehicleTypes: VehicleType[]): vehicleTypesState =>
  state.set('items', immutableKeyBy(vehicleTypes, 'id'));

export const markVehicleTypesFetched = (state: vehicleTypesState, isFetched: boolean = true) => state.setIn(['fetched'], isFetched);

export const logRequestError = (state, requestType: VehicleTypesRequests, error: Error) => {
  return state.withMutations(state =>
    state.setIn(['requests', requestType, 'error'], error.toString()).setIn(['requests', requestType, 'timestamp'], Date.now()),
  );
};

export const vehicleTypesReducers = (state: vehicleTypesState = initState, action): vehicleTypesState => {
  switch (action.type) {
    case VehicleTypesActionTypes.SAVE_REQUEST_ERROR:
      return logRequestError(state, action.payload.requestType, action.payload.error);
    case VehicleTypesActionTypes.GET_VEHICLE_TYPES:
      return storeVehicleTypes(state, action.payload);
    case VehicleTypesActionTypes.MARK_VEHICLE_TYPES_FETCHED:
      return markVehicleTypesFetched(state, action.payload);
    default:
      return state;
  }
};

```
</details>

Then we go on and add a ```Service``` folder.
<details>
<summary>Code Example</summary>

```javascript
export default class VehicleTypesService extends SessionBasedService {
  private store: Redux.Store<CompleteStateStructure>;

  constructor(session: Session) {
    super(session.config.getApiEndpoint(), session);
    this.store = session.state.store;
  }

  @singleConcurrent()
  public async getAll(): Promise<VehicleType[]> {
    if (!this.areVehicleTypesFetched()) {
      await this.store.dispatch(getVehicleTypesRequestAction(this));
    }

    return this.getAllVehicleTypes();
  }

  private getAllVehicleTypes = (): VehicleType[] => {
    return this.store
      .getState()
      .vehicleTypes.getIn(['items'])
      .toList()
      .toJS();
  };

  private areVehicleTypesFetched = (): boolean => {
    return this.store.getState().vehicleTypes.getIn(['fetched']);
  };
}


```
</details>

Then we finally can expose our ```API``` folder :)

<details>
<summary>Code Example</summary>

```javascript
export default class VehicleTypes {
  private vehicleTypesService: VehicleTypesService;

  constructor(session: Session) {
    this.vehicleTypesService = new VehicleTypesService(session);
  }

  public async getAll(): Promise<VehicleType[]> {
    return await this.vehicleTypesService.getAll();
  }
}
```
</details>

Almost 200 hundred lines of code just to expose simply ```getAll``` of ```VehicleTypes```.

We tried to simplify the proccess by creating ```BaseStore``` and using it in each new entity store instead of rewriting it each time.


## Entity Store
We want to keep each entity state up to date always(http requests, realtime events),
so we use store managment class to do it.

From now on we can use a new class named ```BaseStore``` which will be our base of every entity and manage its state.

For Example:

 ```typescript
export default class ChatConversationStore extends BaseStore<ChatConversation>
```

Here we can extend our ```BaseStore```  and use it however we see fit to the new entity we develop(take from store if exists, if not fetch from server and store it).


```typescript
  async getConversation(id: number): Promise<ChatConversation> {
    const chatConversation = this.getItem(id) || this._fetchConversation(id);

    return await chatConversation;
  }
```

## Http Service
Http service is pretty much the same all over the sdk, here is simple example.

```typescript
  GET_CHAT_CONVERSATION: new Route('/chat_conversations/{:id}', HttpMethod.GET)

  public async getChatConversation(id): Promise<ChatConversation> {
    const chatConversationResponse:  ChatConversationResponse = await this.handleAuthorizedRequest<ChatConversationResponse, ChatConversationResponse>(
      Routes.GET_CHAT_CONVERSATION, //route
      defaultExtractor, // extractor func can be passed here
      BringgException.serviceException('Failed to get chat conversation'),
      undefined, //payload
      { id }, //route params
      {}, // query params
    );

    return chatConversationResponse.conversation;
  }
```

## Public API

Here we decide which endpoints and events we want to expose to the end user, which will be called from the entity store.

Example from ```ChatConversation```

```typescript
this.onItemRemoved = this.chatConversationStore.onItemRemoved; // remove event

public getConversation(id: number): Promise<ChatConversation> {
    return this.chatConversationStore.getConversation(id); // get item endpoint
}
```

## BaseStore
Here we implemented [RxJS](http://reactivex.io/),

we have our main stream of items ```$items``` which on each action(create,update,remove) we emit to the stream and accordingly aggregate the most update state.

Each store extends this class can use ```$items``` stream to aggregate which data he wants.(for example from tasks stream aggreate only open tasks.. )
```typescript
  state$: Observable<Map<number, T>>; // aggregated state from main stream
  protected items$: Subject<Action>; // 1 main stream of items

  // Event Handlers
  protected onItemUpdateEvent: (item: T) => void = _.noop;
  protected onItemCreatedEvent: (item: T) => void = _.noop;
  protected onItemRemovedEvent: (item: number) => void = _.noop;
  protected onListUpdateEvent: (items: T[]) => void = _.noop;

  getItem = (itemId): T // Get Item From Store By Id

  addToStore(item: T) // Add item to store

  updateInStore(item: T) // update item in store

  getItems = (): T[] // get array of items of latest state in store

  addItems(items: T[]) // add array of items

  onItemCreated = (fn: (item: T) => void) => // set event handler for item removed

  onItemUpdate = (fn: (item: T) => void) => // set event handler for item updated

  onItemRemoved = (fn: (itemId: number) => void) // set event handler for item removed

  onListUpdate = (fn: (item: T[]) => void) // set event handler for list update

```

## Entities Generator
In order to save time and make your life easier, there is a script to generate the basic
 implementation needed.
 
You can run it with : npm run entity-generator [name] [bringg-type-name]

The script will create a folder called [name] inside src folder.

The folder will include Service, Store and a few more files with basic implementation
and according to our convention. 

## Next Steps:
In order to improve our SDK and our developing proccess.

- Combine Store and Service to one.(instead of copy pasting same service all the time)
