Firebase using Angular4 – Part 4

Welcome back to the new blog from our “Learn Firebase: The Data Engineer Way” series !! Hope you were looking forward to this. Happy Learning…

In previous blog, we have learned basic CRUD operations on Firebase using Angular4. In this latest blog, we will cover some important aspects of Firebase like Firebase joins and corresponding data model for joins. We will also cover transactions so as to run atomic update on data fields.

In this blog post, we will learn to develop two features of our inventory app – Request & Approve.

MOCKUP

Request Item

Approve Item

DATA MODEL

Request page will be rendered from items. When user will request a particular item, we have to keep an entry. As Firebase is NoSQL database, there could be more than one way to store request entry.

One option could be to add request json under requested item itself. This would make our item object larger if there are many request for a particular item and also it will add one level of nesting which is not recommended in Firebase.

Other option could be to add request json in separate $ref and embed item object. In this approach, we have to duplicate item object in every request and if admin edits metadata of any item then changes have to be propagated to every request object.

Another option could be similar to option 2 except instead of embedding whole object, we only embed itemId($key) in request object. With this, even if admin makes an update in item metadata we don’t have to change anything in request objects. In first two approaches, we don’t have to join/lookup value but this approach has introduced join. Let’s look at data model for this approach.

  • itemId: string #$key
  • email: string #emailid of requested user
  • quantity: number #for now, it will be default 1
  • status: string #{new,approved}

When user makes a request for an item, entry should be added in request $ref and status would be new. If admin approves the request status should become ‘approved’ and in case of decline, entry should be removed from database.

TYPESCRIPT OBJECT

Below is typescript object based on data model.

  1. export class Request {
  2. public $key: string;
  3. constructor(public email:string, public itemId:string, public quantity: number, public status: string){}
  4. }

REQUEST PAGE

Let’s implement request page first.

request.component.html

  1. <div class="container">
  2. <div class="row" >
  3. <table class="table table-stripped table-hover">
  4. <thead>
  5. <th>Key</th>
  6. <th>Name</th>
  7. <th>Serial Number</th>
  8. <th>Available Items</th>
  9. </thead>
  10. <tbody>
  11. <tr *ngFor="let item of itemList; let i=index">
  12. <td>{{ item.$key }}</td>
  13. <td>{{ item.name }} </td>
  14. <td>{{ item.serialNum }}</td>
  15. <td>{{ item.totalItems }}</td>
  16. <td><button type="button" class="btn btn-warning" (click)="onRequestItem(item.$key)">Request</button>
  17. </tr>
  18. </tbody>
  19. </table>
  20. </div>
  21. </div>

Just a html boilerplate code.

request.component.ts

  1. export class RequestComponent implements OnInit {
  2.  
  3. itemList: Item[] = [];
  4. constructor(private inventoryService: InventoryService, private authService: AuthService) {
  5. inventoryService.getAllItems().subscribe(
  6. data => this.itemList = data
  7. )
  8. }
  9. ngOnInit() {
  10. }
  11.  
  12. onRequestItem($key: string) {
  13. let request = new Request(this.authService.getEmail(),$key,1,'new');
  14. this.inventoryService.requestItem(request);
  15. }
  16. }

inventory.service.ts

  1. requestItem(request: Request) {
  2. this.requests.push(request);
  3. }

Lets run the updated app using angular-cli command ng-serve.

Go to request screen and request any item.

Once you click on request, request entry will be added in request $ref. You can check that from Firebase console.

APPROVE PAGE

Implementation of this page will be little bit tricky because to show data on page we have to join items $ref and requests $ref. As there is no inbuilt join functionality available in Firebase like traditional databases such as MySQL, we have to implement it at application level.

First we will implement service and then html and associated typescript code.

inventory.service.ts
  1. getItemsByStatus(status: string) {
  2. const queryList$ = this.db.list('/requests', {
  3. query: {
  4. orderByChild: 'status',
  5. equalTo: status
  6. }
  7. })
  8. return queryList$.map(
  9. requestList => requestList.map(request => this.db.object('items/' + request.itemId)
  10. .map((item) => {
  11. return new RequestItem(request as Request, item as Item)
  12. }
  13. )))
  14. .flatMap(fobjs => Observable.combineLatest(fobjs));
  15. }
Above code will first fetch all request with given status (e.g. ‘new’) and then for every return request, look for corresponding item object from $ref using $key of item and create a new joined object RequestItem. Finally it packages result using flatmap.
approve.component.ts
  1. export class ApproveComponent implements OnInit {
  2. requests$: Observable<RequestItem[]> ;
  3. constructor(private inventoryService: InventoryService) {
  4. this.requests$ = inventoryService.getItemsByStatus('new');
  5. }
  6. ngOnInit() {
  7. }
  8. }
approve.component.html
  1. <div class="container">
  2. <div class="row" >
  3. <table class="table table-stripped table-hover">
  4. <thead>
  5. <th>Key</th>
  6. <th>Name</th>
  7. <th>Available Items</th>
  8. <th>Requested Quantity</th>
  9. <th>Requested By</th>
  10. </thead>
  11. <tbody>
  12. <tr *ngFor="let req of (requests$ | async)">
  13. <td>{{ req.item.$key }}</td>
  14. <td>{{ req.item.name }} </td>
  15. <td>{{ req.item.totalItems }}</td>
  16. <td>{{ req.request.quantity }} </td>
  17. <td>{{ req.request.email }} </td>
  18. <td><button type="button" class="btn btn-primary" (click)="inventoryService.approveItem(req)">Approve</button>
  19. <td><button type="button" class="btn btn-danger" (click)="inventoryService.declineItem(req.request.$key)">Decline</button>
  20. </tr>
  21. </tbody>
  22. </table>
  23. </div>
  24. </div>

Refresh the app, you will see approve page.

Let’s implement approveItem and declineItem methods in inventory service.

invenntory.service.ts
  1. approveItem(req: RequestItem) {
  2. this.db.object('requests/' + req.request.$key + '/status').set('approved').then
  3. this.db.object('items/' + req.item.$key + '/totalItems').$ref
  4. .transaction(quantity => { return quantity-req.request.quantity } );
  5. }
  6.  
  7. declineItem($key: string) {
  8. this.requests.remove($key);
  9. }
Logic for approve item would be to first make status of request from ‘new’ to ‘approved’ and then reduce quantity by requested quantity. We have used transaction so that quantity should be updated atomic way in case of many user approving request referring to same item.

That’s it, we have implemented all needed features. There is still some work to be done to make our app robust.

Hope this blog help you understand Firebase joins and implement it. Please do write to us in below comment section in case of any queries.

In next blog we will explore security rules and indexes.