Categories
JavaScript Nodejs

How To Make a Simple Back End With User Accounts and Authentication

With single-page front-end apps and mobile apps being more popular than ever, the front end is decoupled from the back end. Since almost all web apps need authentication, there needs to be a way for front-end or mobile apps to store user identity data in a secure fashion.

JSON Web Tokens (JWT) is one of the most common ways to store authentication data on front-end apps. With Node.js, there are popular libraries that can generate and verify the JWT by checking for its authenticity. They do this by checking against a secret key stored in the back end and also by checking for an expiry date.

The token is encoded in a standard format that’s understood by most apps. It usually contains user identity data like user ID, user name, etc. It’s given to the user when the user successfully completes authentication.

In this piece, we will build an app that uses JWT to store authentication data.


Overview

For the back end, we’ll use the Express framework, which runs on Node.js, and for the front end, we’ll use the Angular framework. Both have their own JWT add-ons. On the back end, we have the jsonwebtoken package for generating and verify the token.

On the front end, we have the @auth0/angular-jwt module for Angular. In our app, when the user enters user name and password and they are in our database, then a JWT will be generated from our secret key, returned to the user, and stored on the front-end app in local storage. Whenever the user needs to access authenticated routes on the back end, they’ll need the token.

There will be a function in the back-end app called middleware to check for a valid token. A valid token is one that is not expired and verifies as valid against our secret key. There will also be a sign-up and user credential settings pages, in addition to a login page.


Building the App

With this plan, we can begin.

First, we create the front- and back-end app folders. Make one for each.

Then we start writing the back-end app. First, we install some packages and generate our Express skeleton code. We run npx express-generator to generate the code. Then we have to install some packages. We do that by running npm i @babel/register express-jwt sequelize bcrypt sequelize-cli dotenv jsonwebtoken body-parser cors . @babel/register allows us to use the latest JavaScript features.

express-jwt generates the JWT and verifies it against a secret key.bcrypt does the hashing and salting of our passwords. sequelize is our ORM for doing CRUD. cors allows our Angular app to communicate with our back end by allowing cross-domain communication. dotenv allows us to store environment variables in an .env file. body-parser is needed for Express to parse JSON requests.

Then we make our database migrations. First, we run npx sequelize-cli init to generate skeleton code for our database-to-object mapping. Then we run:

npx sequelize-cli model:generate --name User --attributes username:string, password:string, email:string

We make another migration and put:

'use strict';module.exports = {  
  up: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.addConstraint(  
        "Users",  
        ["email"],  
        {  
          type: "unique",  
          name: 'emailUnique'  
        }),

      queryInterface.addConstraint(  
        "Users",  
        ["userName"],  
        {  
          type: "unique",  
          name: 'userNameUnique'  
        }),  
  },

  down: (queryInterface, Sequelize) => {  
    return Promise.all([  
      queryInterface.removeConstraint(  
        "Users",  
        'emailUnique'  
      ),

      queryInterface.removeConstraint(  
        "Users",  
        'userNameUnique'  
      ),  
    ])  
  }  
};

This makes sure we don’t have two entries with the same username or email.

This creates the User model and will create the Users table once we run npx sequelize-cli db:migrate .

Then we write some code. First, we put the following in app.js :

require("[@babel/register](http://twitter.com/babel/register)");  
require("babel-polyfill");  
require('dotenv').config();  
const express = require('express');  
const bodyParser = require('body-parser');  
const cors = require('cors');  
const user = require('./controllers/userController');  
const app = express();app.use(cors())  
app.use(bodyParser.urlencoded({ extended: true }));  
app.use(bodyParser.json());

app.use((req, res, next) => {  
  res.locals.session = req.session;  
  next();  
});

app.use('/user', user);app.get('*', (req, res) => {  
  res.redirect('/home');  
});

app.listen((process.env.PORT || 8080), () => {  
  console.log('App running on port 8080!');  
});

We need:

require("@babel/register");  
require("babel-polyfill");

to use the latest features in JavaScript.

And we need:

require('dotenv').config();

to read our config in an .env file.

This is the entry point. We will create userController in the controllers folder shortly.

app.use(‘/user’, user); routes any URL beginning with user to the userController file.

Next, we add the userController.js file:

const express = require('express');  
const bcrypt = require('bcrypt');  
const router = express.Router();  
const models = require('../models');  
const jwt = require('jsonwebtoken');  
import { saltRounds } from '../exports';  
import { authCheck } from '../middlewares/authCheck';

router.post('/login', async (req, res) => {  
    const secret = process.env.JWT_SECRET;  
    const userName = req.body.userName;  
    const password = req.body.password;  
    if (!userName || !password) {  
        return res.send({  
            error: 'User name and password required'  
        })  
    }  
    const users = await models.User.findAll({  
        where: {  
            userName  
        }  
    }) 

    const user = users[0];  
    if (!user) {  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    } 

    try {  
        const compareRes = await bcrypt.compare(password, user.hashedPassword);  
        if (compareRes) {  
            const token = jwt.sign(  
                {  
                    data: {  
                        userName,  
                        userId: user.id  
                    }  
                },  
                secret,  
                { expiresIn: 60 * 60 }  
            );  
            return res.send({ token });  
        }  
        else {  
            res.status(401);  
            return res.send({  
                error: 'Invalid username or password'  
            });  
        }  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(401);  
        return res.send({  
            error: 'Invalid username or password'  
        });  
    }});

router.post('/signup', async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const password = req.body.password;  
    try {  
        const hashedPassword = await bcrypt.hash(password, saltRounds)  
        await models.User.create({  
            userName,  
            email,  
            hashedPassword  
        })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }  
});

router.put('/updateUser', authCheck, async (req, res) => {  
    const userName = req.body.userName;  
    const email = req.body.email;  
    const token = req.headers.authorization;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        await models.User.update({  
            userName,  
            email  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});

router.put('/updatePassword', authCheck, async (req, res) => {  
    const token = req.headers.authorization;  
    const password = req.body.password;  
    const decoded = jwt.verify(token, process.env.JWT_SECRET);  
    const userId = decoded.data.userId;  
    try {  
        const hashedPassword = await bcrypt.hash(password, saltRounds)  
        await models.User.update({  
            hashedPassword  
        }, {  
                where: {  
                    id: userId  
                }  
            })  
        return res.send({ message: 'User created' });  
    }  
    catch (ex) {  
        logger.error(ex);  
        res.status(400);  
        return res.send({ error: ex });  
    }});module.exports = router;

The login route searches for the User entry. If it’s found, it then checks for the hashed password with the compare function of bcrypt. If both are successful, then a JWT is generated. The signup route gets the JSON payload of username and password and saves it.

Note that there is hashing and salting on the password before saving. Passwords should not be stored as plain text.

This first is the plain text password, and the second is a number of salt rounds.

updatePassword route is an authenticated route. It checks for the token, and if it’s valid, it will continue to save the user’s password by searching for the User with the user ID from the decoded token.

We will add the authCheck middleware next. We create a middlewares folder and create authCheck.js inside it.

const jwt = require('jsonwebtoken');  
const secret = process.env.JWT_SECRET;export const authCheck = (req, res, next) => {  
    if (req.headers.authorization) {  
        const token = req.headers.authorization;  
        jwt.verify(token, secret, (err, decoded) => {  
            if (err) {  
                res.send(401);  
            }  
            else {  
                next();  
            }  
        });  
    }  
    else {  
        res.send(401);  
    }  
}

You should use the same process.env.JWT_SECRET for generating and verifying the token. Otherwise, verification will fail. The secret shouldn’t be shared anywhere and shouldn’t be checked in to version control.

This allows us to check for authentication in authenticated routes without repeating code. We place it in between the URL and our main route code in each authenticated route by importing and referencing it.

We make an .env file of the root of the back-end app folder, with the following content. (This shouldn’t be checked in to version control.)

DB_HOST='localhost'  
DB_NAME='login-app'  
DB_USERNAME='db-username'  
DB_PASSWORD='db-password'  
JWT_SECRET='secret'

The back-end app is now complete. Now can we can use a front-end app, mobile app, or any HTTP client to sign in.

Categories
JavaScript Rxjs

Rxjs Operators — More Join Operators

Rxjs is a library for doing reactive programming. Creation operators are useful for generating data from various data sources to be subscribed to by Observers.

In this article, we’ll look at some join operators, including exhaust , mergeAll , startsWith and withLatestFrom .

exhaust

The exhaust operator converts higher-order Observables into first-order Observables by dropping inner Observables while previous inner Observable hasn’t completed.

It takes no parameters and returns an Observable that takes the source Observables and propagates values from the first Observable exclusively until it completes before subscribing to the next one.

For example, we can use it as follows:

import { of, interval } from "rxjs";  
import { map, exhaust, take } from "rxjs/operators";

const of$ = of(1, 2, 3);  
const higherOrderObservable = of$.pipe(map(val => interval(500).pipe(take(3))));  
const result = higherOrderObservable.pipe(exhaust());  
result.subscribe(x => console.log(x));

The code above maps the values from the of$ Observable into interval(500).pipe(take(3)) Observables, which emit numbers up to 2 every half a second.

Then we pipe the interval(500).pipe(take(3)) Observables to the exhaust operator. Then all Observables other than the first interval(500).pipe(take(3)) are dropped since it has finished emitting while the next ones are to be executed.

Then we get:

0  
1  
2

outputted from console.log .

mergeAll

mergeAll converts higher-order Observables to first-order Observables which concurrently delivers all values emitted by inner Observables.

It takes the optionalconcurrent argument, which defaults to Number.INFINITY for the maximum number of inner Observables being subscribed to concurrently.

mergeAll subscribes to all inner Observables within higher-order Observables and delivers all the values from them on the output Observable. The returned output Observable only completes when all inner Observables are completed.

Any error emitted by inner Observables will immediately result in errors emitted by the returned Observable.

For instance, we can use it as follows:

import { of } from "rxjs";  
import { map, mergeAll } from "rxjs/operators";

const of$ = of(1, 2, 3);  
const higherOrderObservable = of$.pipe(map(val => of("a", "b", "c")));  
const result = higherOrderObservable.pipe(mergeAll());  
result.subscribe(x => console.log(x));

The code above maps each emitted values from the of$ Observable to of(“a”, “b”, “c”) Observables.

Then the mergeAll operator subscribes to all the of(“a”, “b”, “c”) Observables and then subscribe to each of them and then emit the values of each.

Then we get:

a  
b  
c  
a  
b  
c  
a  
b  
c

from console.log .

We can also change the concurrency by passing in a number to the mergeAll operator.

For example, we can write:

import { of, interval } from "rxjs";  
import { map, mergeAll, take } from "rxjs/operators";

const of$ = of(1, 2, 3);  
const higherOrderObservable = of$.pipe(  
  map(val => interval(1000).pipe(take(2)))  
);  
const result = higherOrderObservable.pipe(mergeAll(1));  
result.subscribe(x => console.log(x));

to make mergeAll subscribe to each child Observable returned from the map operator’s callback sequentially, which will get us:

0  
1  
0  
1  
0  
1

from the console.log .

Or we can change 1 to 5 as follows:

import { of, interval } from "rxjs";  
import { map, mergeAll, take } from "rxjs/operators";

const of$ = of(1, 2, 3);  
const higherOrderObservable = of$.pipe(  
  map(val => interval(1000).pipe(take(2)))  
);  
const result = higherOrderObservable.pipe(mergeAll(5));  
result.subscribe(x => console.log(x));

Then we get:

(3) 0  
(3) 1

outputted from console.log as they’re subscribed to concurrently.

startWith

startsWith returns an Observable that emits the items we want to emit before it begins to emit items from the source Observable.

It takes one argument, which is an array of items that we want to be emitted before the values from the source Observable by the returned Observable.

For example, we can use it as follows:

import { of } from "rxjs";  
import { startWith } from "rxjs/operators";

of(1)  
  .pipe(startWith("foo", "bar"))  
  .subscribe(x => console.log(x));

Then we get:

foo  
bar  
1

as the console.log output.

withLatestFrom

The withLatestFrom operator combines the source Observable with other Observables to create an Observable which emits values that are calculated from the latest values of each, only when the source emits.

It takes a list of arguments, which is an Observable to combine the values with.

For example, we can use it as follows:

import { fromEvent, interval } from "rxjs";  
import { withLatestFrom } from "rxjs/operators";

const clicks = fromEvent(window, "click");  
const timer = interval(1000);  
const result = clicks.pipe(withLatestFrom(timer));  
result.subscribe(x => console.log(x));

We track the clicks of the browser tab with the fromEvent operator. Then we combine the results emitted from the timer Observable with it by using the withLatestFrom operator.

In the end, the result Observable will emit arrays that have the MouseEvent object from the clicks Observable as the first value and the number from the timer Observable as the second value.

The exhaust operator converts higher-order Observables into first-order Observables by dropping inner Observables while previous inner Observable hasn’t completed.

mergeAll converts higher-order Observables to first-order Observables which concurrently delivers all values emitted by inner Observables.

startsWith returns an Observable that emits the items we want to emit before the source Observable begins emitting values.

The withLatestFrom operator combines the source Observable with another Observable and returns an Observable that emits the latest values from both.

Categories
Express JavaScript Testing

Adding Tests to Express Apps with Jest and Supertest

Automated tests are essential to the apps we write since modern apps have so many moving parts.

In this piece, we’ll look at how to write apps to test an Express app that interacts with a database with Jest and SuperTest.


Creating the App We’ll Test

We create a project folder by creating an empty folder and running the following to create a package.json file with the default answers:

npm init -y

Then we run the following to install the packages for our apps:

npm i express sqlite3 body-parser

Then, we create the app.js file for our app and write:

const express = require('express');  
const sqlite3 = require('sqlite3').verbose();  
const bodyParser = require('body-parser');  
const app = express();  
const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;  
let db;  
if (process.env.NODE_ENV === 'test') {  
    db = new sqlite3.Database(':memory:');  
}  
else {  
    db = new sqlite3.Database('db.sqlite');  
}

db.serialize(() => {  
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');  
});

app.use(bodyParser.json());  
app.get('/', (req, res) => {  
    db.serialize(() => {  
        db.all('SELECT * FROM persons', [], (err, rows) => {  
            res.json(rows);  
        });  
    })  
})

app.post('/', (req, res) => {  
    const { name, age } = req.body;  
    db.serialize(() => {  
        const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');  
        stmt.run(name, age);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

app.put('/:id', (req, res) => {  
    const { name, age } = req.body;  
    const { id } = req.params;  
    db.serialize(() => {  
        const stmt = db.prepare('UPDATE persons SET name = ?, age = ? WHERE id = ?');  
        stmt.run(name, age, id);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

app.delete('/:id', (req, res) => {  
    const { id } = req.params;  
    db.serialize(() => {  
        const stmt = db.prepare('DELETE FROM persons WHERE id = ?');  
        stmt.run(id);  
        stmt.finalize();  
        res.json(req.body);  
    })  
})

const server = app.listen(port);  
module.exports = { app, server };

The code above has the app we’ll test.

To make our app easier to test, we have:

const port = process.env.NODE_ENV === 'test' ? 3001 : 3000;  
let db;  
if (process.env.NODE_ENV === 'test') {  
    db = new sqlite3.Database(':memory:');  
}  
else {  
    db = new sqlite3.Database('db.sqlite');  
}

So we can set the process.env.NODE_ENV to 'test' to make our app listen to a different port than it does when the app is running in a nontest environment.

We’ll use the 'test' environment to run our tests.

Likewise, we want our app to use a different database when running unit tests than when we aren’t running them.

This is why we have:

let db;  
if (process.env.NODE_ENV === 'test') {  
    db = new sqlite3.Database(':memory:');  
}  
else {  
    db = new sqlite3.Database('db.sqlite');  
}

We specified that when the app is running in a 'test' environment we want to use SQLite’s in-memory database rather than a database file.


Writing the Tests

Initialization the code

With the app made to be testable, we can add tests to it.

We’ll use the Jest test runner and SuperTest to make requests to our routes in our tests. To add Jest and SuperTest, we run:

npm i jest supertest

Then, we add app.test.js to the same folder as the app.js file we had above.

In app.test.js, we start by writing the following:

const { app } = require('./app');  
const sqlite3 = require('sqlite3').verbose();  
const request = require('supertest');  
const db = new sqlite3.Database(':memory:');
beforeAll(() => {  
    process.env.NODE_ENV = 'test';  
})

In the code above, we included our Express app from our app.js. Then we also included the SQLite3 and SuperTest packages.,

Then, we connected to our in-memory database with:

const db = new sqlite3.Database(':memory:');

Next, we set all the tests to run in the 'test' environment by running:

beforeAll(() => {  
    process.env.NODE_ENV = 'test';  
})

This will make sure we use port 3001 and the in-memory database we specified in app.js for each test.

To make our tests run independently and with consistent results, we have to clean our database and insert fresh data every time.

To do this, we create a function we call on each test:

const seedDb = db => {  
    db.run('CREATE TABLE IF NOT EXISTS persons (id INTEGER PRIMARY KEY, name TEXT, age INTEGER)');  
    db.run('DELETE FROM persons');  
    const stmt = db.prepare('INSERT INTO persons (name, age) VALUES (?, ?)');  
    stmt.run('Jane', 1);  
    stmt.finalize();  
}

The code above creates the persons table if it doesn’t exist and deletes everything from there afterward.

Then we insert a new value in there to have some starting data.


Adding Tests

With the initialization code complete, we can write the tests.

GET request test

First, we write a test to get the existing seed data from the database with a GET request.

We do this by writing:

test('get persons', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        const res = await request(app).get('/');  
        const response = [  
            { name: 'Jane', id: 1, age: 1 }  
        ]  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});

We put everything inside the callback of db.serialize so the queries will be run sequentially.

First, we call seedDb, which we created above to create the table if it doesn’t exist, to clear out the database, and to add new data.

Then, we call the GET request by writing:

await request(app).get('/');

This gets us the res object with the response resolved from the promise.

request(app) will start the Express app so we can make the request.

Next, we have the response for us to check against for correctness:

const response = [  
  { name: 'Jane', id: 1, age: 1 }  
]

Then, we check the responses to see if we get what we expect:

expect(res.status).toBe(200);  
expect(res.body).toEqual(response);

The toBe method checks for shallow equality, and toEqual checks for deep equality. So we use toEqual to check if the whole object structure is the same.

res.status checks the status code returned from the server, and res.body has the response body.

POST request test

Next, we add a test for the POST request. It’s similar to the GET request test.

We write the following code:

test('add person', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        await request(app)  
            .post('/')  
            .send({ name: 'Joe', age: 2 }); 
        const res = await request(app).get('/');  
        const response = [  
            { name: 'Jane', id: 1, age: 1 },  
            { name: 'Joe', id: 2, age: 2 }  
        ]  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});

First, we reset the database with:

seedDb(db);

We made our POST request with:

await request(app)  
  .post('/')  
  .send({ name: 'Joe', age: 2 });

This will insert a new entry into the in-memory database.

Finally, to check for correctness, we make the GET request — like in our first test — and check if both entries are returned:

const res = await request(app).get('/');  
const response = [  
  { name: 'Jane', id: 1, age: 1 },  
  { name: 'Joe', id: 2, age: 2 }  
]  
expect(res.status).toBe(200);  
expect(res.body).toEqual(response);

PUT and DELETE tests

The test for the PUT request is similar to the POST request. We reset the database, make the PUT request with our payload, and then make the GET request to get the returned data, as follows:

test('update person', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        await request(app)  
            .put('/1')  
            .send({ name: 'Joe', age: 2 }); 
        const res = await request(app).get('/');  
        const response = [  
            { name: 'Jane', id: 1, age: 1 }  
        ]  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});

Then we can replace the PUT request with the DELETE request and test the DELETE request:

test('delete person', () => {  
    db.serialize(async () => {  
        seedDb(db);  
        const res = await request(app).delete('/1');  
        const response = [];  
        expect(res.status).toBe(200);  
        expect(res.body).toEqual(response);  
    })  
});

Running the Tests

To run the tests, we add the following to the scripts section:

"test": "jest --forceExit"

We have to add the --forceExit option so Jest will exist after the tests are run. There’s no fix for the issue where Jest tests using SuperTest don’t exit properly yet.

Then we run the following to run the tests:

npm test

And we should get:

PASS  ./app.test.js  
  √ get persons (11ms)  
  √ add person (2ms)  
  √ update person (2ms)  
  √ delete person (6ms)Test Suites: 1 passed, 1 total  
Tests:       4 passed, 4 total  
Snapshots:   0 total  
Time:        2.559s  
Ran all test suites.  
Force exiting Jest: Have you considered using `--detectOpenHandles` to detect async operations that kept running after all tests finished?

We should get the same thing no matter how many times we run the tests since we reset the database and made all database queries run sequentially.

Also, we used a different database and port for our tests than other environments, so the data should be clean.


Conclusion

We can add tests run with the Jest test runner. To do this, we have to have a different port and database for running the tests. Then we create the tables if they don’t already exist, clear all the data, and add seed data so we have the same database structure and content for every test.

With SuperTest, we can run the Express app automatically and make the request we want. Then, we can check the output.

Categories
JavaScript Rxjs

Rxjs Operators — Conditional and Booleans

Rxjs is a library for doing reactive programming. Creation operators are useful for generating data from various data sources to be subscribed to by Observers.

In this article, we’ll look at the defaultIfEmpty , every , find , findIndex and isEmpty operators.

defaultIfEmpty

The defaultIfEmpty operator returns an Observable that emits a value if the source Observable completes without emitting anything. Otherwise, the values from the source Observable are emitted.

It takes one optional value which is the defaultValue . The default for this is null . It’s the value that’ll be emitted if the source Observable is empty.

For example, we can use it as follows:

import { of } from "rxjs";  
import { defaultIfEmpty } from "rxjs/operators";  
of()  
  .pipe(defaultIfEmpty(1))  
  .subscribe(x => console.log(x));

We have an empty of() Observable, which will emit nothing before completing, so we’ll see 1 emitted.

every

The every operator returns an Observable that emits whether or not every item of the source satisfies the condition specified.

It takes up to 2 arguments. The first is a predicate function to determine if the item emitted by the source Observable meets a specified condition.

The second argument is the thisArg , which is optional and defaults to undefined . We set this to set the value of this in the predicate function.

We can use it as follows:

import { of } from "rxjs";  
import { every } from "rxjs/operators";  
const of$ = of(1, 2, 3, 4, 5, 6);  
const result = of$.pipe(every(val => val % 2 === 0));  
result.subscribe(x => console.log(x));

The code above checks if every value of the of$ Observable is an even number since we passed in val => val % 2 === 0 as the predicate function.

The result Observable emits false since not every number is even in the of$ Observable.

find

The find operator returns the first value emitted by the source Observable that matches the condition.

It takes up to 2 arguments. The first is a predicate function that returns the condition to check for matching the emitted values from the source Observable.

The second argument is the thisArg , which is optional and defaults to undefined . We set this to set the value of this in the predicate function.

We can use it as follows:

import { of } from "rxjs";  
import { find } from "rxjs/operators";  
const of$ = of(1, 2, 3, 4, 5, 6);  
const result = of$.pipe(find(val => val % 2 === 0));  
result.subscribe(x => console.log(x));

We should get 2 since it’s the first even number emitted by of$ , since we specified that val => val % 2 === 0 is the predicate that we want to check.

findIndex

The findIndex operator returns an Observable that emits of the first match of the value emitted from the source Observable that matches the condition returned by the predicate function.

It takes up to 2 arguments. The first is a predicate function that returns the condition to check for matching the emitted values from the source Observable.

The second argument is the thisArg , which is optional and defaults to undefined . We set this to set the value of this in the predicate function.

We can use it as follows:

import { of } from "rxjs";  
import { findIndex } from "rxjs/operators";  
const of$ = of(1, 2, 3, 4, 5, 6);  
const result = of$.pipe(findIndex(val => val % 2 === 0));  
result.subscribe(x => console.log(x));

We should get 1 since it’s the first index with an even number emitted by of$ , since we specified that val => val % 2 === 0 is the predicate that we want to check.

isEmpty

The isEmpty operator returns an Observable that emits false if the source Observable emit any values. Otherwise, true is emitted by the returned Observable.

It takes no arguments.

For example, we can use it as follows:

import { of } from "rxjs";  
import { isEmpty } from "rxjs/operators";  
const of$ = of();  
const result = of$.pipe(isEmpty());  
result.subscribe(x => console.log(x));

Then we should get true since of$ emits nothing.

The defaultIfEmpty operator returns an Observable that emits a default value if the source Observable emits nothing or emits the values of the source Observable if it emits something.

The every operator returns an Observable that emits whether or not every item of the source satisfies the condition specified.

The find and findIndex operators both search for the first value emitted by the source Observable that meets a condition, but find emits the object that meets the condition and findIndex emits the index of the object.

The isEmpty operator returns an Observable that emits true if it emits nothing and false otherwise.

Categories
JavaScript Rxjs

Rxjs Operators — Calculation and Aggregation

Rxjs is a library for doing reactive programming. Creation operators are useful for generating data from various data sources to be subscribed to by Observers.

In this article, we’ll look at some calculation and aggregation operators including count , max , min and reduce .

count

The count operator returns an Observable that counts the number of emissions on the source and emits the number when the source completes.

It takes an optional predicate function which returns a boolean with the condition for counting.

The predicate has 3 parameters, which is the value emitted by the source Observable, the index , which is the zero-based ‘index’ of the value from the source Observable, and the source , which is the source Observable itself.

For example, we can use it as follows to count the total number of emitted values:

import { of } from "rxjs";  
import { count } from "rxjs/operators";
const of$ = of(1, 2, 3, 4, 5, 6);  
const result = of$.pipe(count());  
result.subscribe(x => console.log(x));

The count operator without a predicate function passed in will count all emissions from the source Observable.

This should get us 6 since we have 6 values in the of$ Observable.

We can also pass in a predicate function as follows:

import { of } from "rxjs";  
import { count } from "rxjs/operators";
const of$ = of(1, 2, 3, 4, 5, 6);  
const result = of$.pipe(count(val => val % 2 === 0));  
result.subscribe(x => console.log(x));

Then we should get 3 since we’re only the number of even numbers emitted from of$ .

max

The max operator takes value from a source Observable that emits the numbers or items that can be compared with a comparer function, and gets the largest one from it and emits it with the returned Observable.

It takes an optional comparer function, which we can use to compare the value of 2 items.

For example, we can use it to get the highest number emitted from an Observable as follows:

import { of } from "rxjs";  
import { max } from "rxjs/operators";
of(2, 3, 4, 100)  
  .pipe(max())  
  .subscribe(x => console.log(x));

We should get 100 since 100 is the biggest number in the of(2, 3, 4, 100) Observable.

Also, we can pass in a function to the max operator to get the largest value from a list of objects as follows:

import { of } from "rxjs";  
import { max } from "rxjs/operators";

const people = [  
  { age: 17, name: "Joe" },  
  { age: 25, name: "Jane" },  
  { age: 19, name: "Mary" }  
];

of(...people)  
  .pipe(max((a, b) => a.age - b.age))  
  .subscribe(x => console.log(x));

Then we should get:

{age: 25, name: "Jane"}

from the console.log .

The comparer function works like the callback of the array’s sort method. We keep the order if comparer returns a negative number. We switch the order if comparer returns a positive number. Otherwise, they’re the same. Then the largest one is picked from the end.

min

The min operator is the opposite of the max operator. It gets the smallest value from the source Observable.

The arguments are the same as the max operator.

We can use it like the max operator as follows:

import { of } from "rxjs";  
import { min } from "rxjs/operators";
of(2, 3, 4, 100)  
  .pipe(min())  
  .subscribe(x => console.log(x));

Then we get 2.

Like the max operator, we can use the min operator with a comparer function as follows:

import { of } from "rxjs";  
import { min } from "rxjs/operators";  
const people = [  
  { age: 17, name: "Joe" },  
  { age: 25, name: "Jane" },  
  { age: 19, name: "Mary" }  
];  
of(...people)  
  .pipe(min((a, b) => a.age - b.age))  
  .subscribe(x => console.log(x));

Then we get:

{age: 17, name: "Joe"}

The comparer function works the same way as the one we pass into the max operator, except that the first one is picked instead of the last.

reduce

The reduce operator applies an accumulator function over all the values emitted by the source Observable to combine them into a single value, which is emitted by the returned Observable.

It takes up to 2 arguments. The first is the accumulator function, which is the function we use to combine the values.

The second argument is an optional seed value, which is the initial value of the accumulation.

For example, we can use it as follows:

import { of } from "rxjs";  
import { reduce } from "rxjs/operators";  
of(2, 3, 4, 100)  
  .pipe(reduce((a, b) => a + b, 0))  
  .subscribe(x => console.log(x));

The code above will sum up of the values from the of(2, 3, 4, 100) Observable and emits 109, which are the sum of 2, 3, 4, and 100.

a and b are the values emitted from the source Observable.

The count operator returns an Observable to count the number of times a source Observable emits or source Observable emitting a certain kind of value depending on the condition we specify.

min and max are used to get the minimum and maximum value according to a sorting function or from a group of numbers emitted respectively.

The reduce operator returns an Observable that takes the values from the source Observable and combine them into one in a way that we specify in the function we pass into it.