Categories
JavaScript Nodejs

Node.js FS Module — Renaming Item sand Removing Directories

Manipulating files and directories are basic operations for any program. Since Node.js is a server side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature. Fortunately, Node.js has a fs module built into its library. It has many functions that can help with manipulating files and folders. File and directory operation that are supported include basic ones like manipulating and opening files in directories. Likewise, it can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API that have functions that support promises. Also it can show statistics for a file. Almost all the file operations that we can think of can be done with the built in fs module. In this article, we will rename items stored on disk with the rename family of functions and remove directories with the rmdir family of functions.

Renaming Items with fs.rename and fs.renameSync

To rename items stored on disk in a Node.js program, we can call the rename function asynchronously. It takes 3 arguments. The first argument is the old path of the file. which can be a string, a Buffer object, or an URL object.

The second argument is the new path of the file, which also can be a string, a Buffer object, or an URL object.

The last argument is a callback function that’s called when the item rename operation ends. The callback function takes an err parameter which has the error data if the rename operation ends with an error, otherwise, the err object is null .

The original file must exist before renaming it. If the path of the item you want to rename to already exists, then that item will be overwritten. If the destination path is a directory, then an error will be raised.

For example, we can use it like in the following code:

const fs = require("fs");  
const sourceFile = "./files/originalFile.txt";  
const destFile = "./files/renamedFile.txt";

fs.rename(sourceFile, destFile, err => {  
  if (err) throw err;  
  console.log("Rename complete!");  
});

We can do the same for directories:

const fs = require("fs");  
const oldDirectory = "./files/oldFolder";  
const newDirectory = "./files/newFolder";

fs.rename(oldDirectory, newDirectory, err => {  
  if (err) throw err;  
  console.log("Directory rename complete!");  
});

The synchronous version of the rename function is the renameSync function. It takes the same arguments as the rename but without the callback. The first argument is the old path of the file. which can be a string, a Buffer object, or an URL object. The second argument is the new path of the file, which also can be a string, a Buffer object, or an URL object. It returns undefined .

For example, we can rename a file with the renameSync function like in the following code:

const fs = require("fs");  
const sourceFile = "./files/originalFile.txt";  
const destFile = "./files/renamedFile.txt";

try {  
  fs.renameSync(sourceFile, destFile);  
  console.log("Rename complete!");  
} catch (error) {  
  console.log(error);  
}

There’s also a promise version of the rename function, which also does the rename operation asynchronously. It takes 2 arguments.

The first argument is the old path of the file. which can be a string, a Buffer object, or an URL object.

The second argument is the new path of the file, which also can be a string, a Buffer object, or an URL object. The promise version of the rename function returns a promise that resolves without argument with the rename operation is successful. For example, we can use it like in the following code:

const fsPromises = require("fs").promises;  
const sourceFile = "./files/originalFile.txt";  
const destFile = "./files/renamedFile.txt";

(async () => {  
  try {  
    await fsPromises.rename(sourceFile, destFile);  
    console.log("Rename complete!");  
  } catch (error) {  
    console.log(error);  
  }  
})();

This is a better choice than renameSync for running sequential operations because asynchronous operations like promises won’t holding the program’s execution when it’s running, which means that other parts of the program can run if the operation isn’t finished.

Removing Directories with fs.rmdir and fs.rmdirSync

To remove directories asynchronously we can use the rmdir function. It takes 3 arguments.

The first is the path of the directory, which can be a string, a Buffer object or an URL object.

The second argument is an object that takes a few option properties. The emFileWait property is an integer that let our program retry if an EMFILE error is encountered.

It is the maximum number of milliseconds that we wait to try deleting the directory again. The rmdir function will retry every 1ms until the emFileWait value is reached.

Default value is 1000. The maxBusyTries is an integer is the number of retries when the EBUSY , ENOTEMPTY or EPERM error is encountered. It will retry every 100 milliseconds up to the maxBusyTries value. The recursive property is a boolean property.

If it’s set to true , then it will recursively delete data inside the directory along with the directory itself. In recursive mode, errors aren’t reported if path doesn’t exist and operations are retried on failure. The default value is false .

Recursive mode is an experimental feature. The last argument is a callback function which has an err parameter. It’s called when the removal operation ends. It’s null if the directory removal operation succeeds.

Otherwise, it returns an object with the error information. Using the regular asynchronous version of the rmdir function with files results in the promise being rejected with the ENOENT error on Windows and an ENOTDIR error on POSIX operating systems.

For example, we can use it like in the following code:

const fs = require("fs");  
const dirToDelete = "./files/deleteFolder";

fs.rmdir(  
  dirToDelete,  
  {  
    emfileWait: 2000,  
    maxBusyTries: 5,  
    recursive: false  
  },  
  err => {  
    if (err) {  
      throw err;  
    }  
    console.log("Removal complete!");  
  }  
);

The directory with the given path should be gone when the code above is ran if it exists and it’s not being used by other programs.

The synchronous version of the rmdir function is the rmdirSync function. It takes similar arguments as the rmdir function. The first argument is the path to the directory, which can be a string, a Buffer object or an URL object.

The second argument is an object that takes one option property. The recursive property is a boolean property. If it’s set to true , then it will recursively delete data inside the directory along with the directory itself. In recursive mode, errors aren’t reported if path doesn’t exist and operations are retried on failure. The default value is false .

Recursive mode is an experimental feature. It returns undefined .

We can use the rmdirSync function like in the following code:

const fs = require("fs");  
const dirToDelete = "./files/deleteFolder";

fs.rmdirSync(dirToDelete, {  
  recursive: false  
});  
console.log("Removal complete!");

The directory with the given path should be gone when the code above is ran if it exists and it’s not being used by other programs.

The promise version of the rmdir function does the same thing as the regular rmdir function. It takes 2 arguments. The first is the path of the directory, which can be a string, a Buffer object or an URL object. The second argument is the an object that takes a few option properties.

The emFileWait property is an integer that let our program retry if an EMFILE error is encountered. It is the maximum number of milliseconds that we wait to try deleting the directory again. The rmdir function will retry every 1ms until the emFileWait value is reached. Default value is 1000.

The maxBusyTries is an integer is the number of retries when the EBUSY , ENOTEMPTY or EPERM error is encountered. It will retry every 100 milliseconds up to the maxBusyTries value.

The recursive property is a boolean property. If it’s set to true , then it will recursively delete data inside the directory along with the directory itself. In recursive mode, errors aren’t reported if path doesn’t exist and operations are retried on failure. The default value is false .

Recursive mode is an experimental feature. It returns a promise which resolves with no argument when the directory removal operation succeeds. Using the promise version of the rmdir function with files results in the promise being rejected with the ENOENT error on Windows and an ENOTDIR error on POSIX operating systems.

We can use it like in the following code:

const fsPromises = require("fs").promises;  
const dirToDelete = "./files/deleteFolder";

(async () => {  
  try {  
    await fsPromises.rmdir(dirToDelete, {  
      emfileWait: 2000,  
      maxBusyTries: 5,  
      recursive: false  
    });  
    console.log("Removal complete!");  
  } catch (error) {  
    console.error(error);  
  }  
})();

The directory with the given path should be gone when the code above is run if it exists and it’s not being used by other programs. We used the try...catch block to catch errors with the async and await syntax with the promise version of the rmdir .

This is a better choice than rmdirSync for running sequential operations because asynchronous operations like promises won’t hold up the program’s execution when it’s running, which means that other parts of the program can run if the operation isn’t finished.

We renamed items stored on disk with the rename family of functions and remove directories with the rmdir family of functions.

With the rename family of functions, we just pass in the original path and the path that we want to rename to and then anything that’s passed in will be renamed if it’s valid.

The rmdir family let us remove directories by specifying the path. The asynchronous versions of the rmdir functions, which include the regular and the promise version let us specify how it will retry when an error occurs. This is very handy for handling errors gracefully.

Categories
JavaScript Nodejs

Node.js FS Module — Read Streams

Manipulating files and directories are basic operations for any program. Since Node.js is a server-side platform and can interact with the computer that it’s running on directly, being able to manipulate files is a basic feature.

Fortunately, Node.js has a fs module built into its library. It has many functions that can help with manipulating files and folders. File and directory operations that are supported include basic ones like manipulating and opening files in directories.

Likewise, it can do the same for files. It can do this both synchronously and asynchronously. It has an asynchronous API that has functions that support promises.

Also, it can show statistics for a file. Almost all the file operations that we can think of can be done with the built-in fs module. In this article, we will create read streams to read a file’s data sequentially and listen to events from a read stream. Since Node.js ReadStreams are descendants of the Readable object, we will also listen to events to it.

Streams are collections of data that may not be available all at once and don’t have to fit in memory. This makes stream handy for processing large amounts of data.

It’s handy for files because files can be big and streams can let us get a small amount of data at one time. In the fs module, there are 2 kinds of streams. There’s the ReadStream and the WriteStream.

ReadStream

ReadStreams are for reading in data from a file and then outputting them a small part at a time. A ReadStream can read a small part of a file or it can read in the whole file.

To create a ReadStream, we can use the fs.createReadStream function. The function takes in 2 arguments. The first argument is the path of the file.

The path can be in the form of a string, a Buffer object, or an URL object.

The second argument is an object that can have a variety of options as properties. The flag option is the file system flag for setting the mode for opening the file. The default flag is r. The list of flags are below:

  • 'a' – Opens a file for appending, which means adding data to the existing file. The file is created if it does not exist.
  • 'ax' – Like 'a' but exception is thrown if the path exists.
  • 'a+' – Open file for reading and appending. The file is created if it doesn’t exist.
  • 'ax+' – Like 'a+' but exception is thrown if the path exists.
  • 'as' – Opens a file for appending in synchronous mode. The file is created if it does not exist.
  • 'as+' – Opens a file for reading and appending in synchronous mode. The file is created if it does not exist.
  • 'r' – Opens a file for reading. An exception is thrown if the file doesn’t exist.
  • 'r+' – Opens a file for reading and writing. An exception is thrown if the file doesn’t exist.
  • 'rs+' – Opens a file for reading and writing in synchronous mode.
  • 'w' – Opens a file for writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx' – Like 'w' but fails if the path exists.
  • 'w+' – Opens a file for reading and writing. The file is created (if it does not exist) or overwritten (if it exists).
  • 'wx+' – Like 'w+' but exception is thrown if the path exists.

The encoding option is a string that sets the character encoding in the form of the string. The default value is null .

The fd option is the integer file descriptor which can be obtained with the open function and its variants. If the fd option is set, then the path argument will be ignored. The default value is null .

The mode option is the file permission and sticky bits of the file, which is an octal number that are the same as Unix or Linux file permissions. It’s only set if the file is created. The default value is 0o666. The autoClose option specifies that the file descriptor will be closed automatically. The default value is true .

If it’s false , then the file descriptor won’t be closed even if there’s an error. It’s completely up to us to close it it autoClose is set to false to make sure there’s no file descriptor leak. Otherwise, the file descriptor will be closed automatically if there’s an error or end event emitted.

The emitClose option will emit the close event when the read stream ends. The default value is false .

The start and end options specifies the beginning and end parts of the file to read. Everything in between will be read in addition to the start and end . start and end are numbers that are the starting and ending bytes of the file to read.

The highWaterMark option is limit to the number of bytes that are read in the stream. The read stream will continue to be read and buffered if the highWaterMark value is reached, but the memory usage will be high and the garbage collection performance will be poor, or it can crash your program with the Allocation failed - JavaScript heap out of memory error.

The createReadStream function returns a ReadStream object where you can attach event handlers to it.

To create a ReadStream, we can use the createReadStream like in the following code:

const fs = require("fs");  
const file = "./files/file.txt";  
const stream = fs.createReadStream(file, {  
  flags: "r",  
  encoding: "utf8",  
  mode: 0o666,  
  autoClose: true,  
  emitClose: true,  
  start: 0  
});

stream.on("open", () => {  
  console.log("Stream opened");  
});

stream.on("ready", () => {  
  console.log("Stream ready");  
});

stream.on("data", data => {  
  console.log(data);  
});

stream.on("readable", () => {  
  while ((chunk = stream.read())) {  
    console.log(chunk);  
  }  
});

stream.on("close", () => {  
  console.log("Stream closed");  
});

When we run the code above, we should get something like the following outputted to the screen, assuming that you have ‘datadatadatadata’ written your a files.txt file:

Stream opened  
Stream ready  
datadatadatadata  
datadatadatadata  
Stream closed  
Stream closed

ReadStream Events

With a ReadStream, we can listen to the following events. There’s a close event that is emitted when the close event is emitted after the file is read.

The open event is emitted when the stream is opened. The file descriptor number fd will be passed with the event when it’s emitted. The ready event is emitted when the ReadStream is ready to be used. It’s fired immediately after the open event is fired.

The ReadStream extends the stream Readable object, which emits events of its own. The data event is emitted whenever the stream data is sent to the consumer. It’s emitted when the readable.pipe() function or readable.resume() are called, or by attaching a listener callback to the data event.

The data event will also be emitted when the readable.read() function is called and a chunk of data is available to be returned. The end event is emitted when there’s no more data to be consumed from the stream. It won’t be emitted until the data is completely consumed.

This can be done by switch the stream to flowing more or calling stream.read() repeated until all the data are consumed.

The error event is emitted whenever an error occurs during the streaming or consumption of the stream. It can be because the stream can’t generate data due to internal failure or a stream attempts to push invalid chunks of data. The pause event is emitted whenever ReadStream.pause() is called and readableFlowing isn’t false .

readableFlowing can have one of 3 states. One is null. When it’s null , this means that no mechanism for consuming the stream’s data is provided and therefore the stream won’t generate data.

When readableFlowing is null, attaching a listener for the 'data' event, calling the readable.pipe() method, or calling the readable.resume() method will switch readable.readableFlowing to true, causing the ReadStream to start emitting events as data is generated.

Calling readable.pause(), readable.unpipe(), or receiving backpressure, which is the situation where data fills the buffer, readable.readableFlowing to be set as false, temporarily halting the flow of events but not halting the generation of data.

Attaching a listener for the 'data' event will not switch readable.readableFlowing to true when readable.readableFlowing is set as false .

The readable event is emitted when there’s data available to be read from the stream or the end of the stream has been reached. Attaching an event listener for the readable event may cause some amount of data to be read into an internal buffer.

It will also be emitted when the end of the stream is reached but before the end event is emitted. The resume event is emitted when ReadStream.resume() is called and the readableFlowing isn’t true .

A ReadStream object also has the following properties. The bytesRead property let us get the number of bytes read so far.

The path property is a string or a buffer that gets us the reference to the file. It’s the same as the first argument of createReadStream() .

The data type will also be the same as what we pass in as the first argument. The pending property is a boolean which is true if the underlying file hasn’t been opened yet, or before the ready event is emitted.

By using the fs.createReadStream function, we created read streams to read a file’s data sequentially and listen to events from a read stream. Since Node.js ReadStreams are descendants of the Readable object, we will also listen to events to it.

We have lots of control over how the read stream is created. We can set the path or file descriptor of the file. Also, we can set the mode of the file to be read and the permission and sticky bit of the file being read.

Also, we can choose to close the streams automatically or not or emit close event automatically. We can also set the highWaterMark option which sets the event of maximum buffer size for storing the read data.

Also, we can call pipe to move data to a writable stream, and pause the streaming of data with the pause function, and resume streaming with the resume function.

Categories
Express JavaScript Nodejs

Use body-parser Express Middleware to Parse JSON and Raw Requests

By default, Express 4.x or later doesn’t come with anything to parse request bodies. Therefore, we need to add something to do this.

In this article, we’ll look at how to use the body-parser middleware to do this with JSON and raw bodies.

Adding the Body-Parser Package

body-parser is a Node package that we can add onto our Express app to parse request bodies.

It doesn’t support multipart bodies, so we have to use other middleware packages to parse those.

However, it can parse JSON bodies, raw request bodies, text, and URL-encoded bodies.

To install the package, we run:

npm install body-parser

Then we can include by adding:

const bodyParser = require('body-parser');

Parsing JSON Bodies

We can parse JSON bodies by calling the JSON method. It takes an optional object with a few properties.

The options include:

  • inflate — compressed request bodies will be inflated when this is set to true . Otherwise, they’ll be rejected.
  • limit — controls the maximum request body size. If it’s number, then it’s measured in bytes. If it’s a string then it can be parsed into a number of bytes.
  • reviver — this is a function that’s passed into JSON.parse as the second argument to map values to what we want
  • strict — only accepts objects and arrays when set to true . Otherwise, it’ll accept anything that JSON.parse accepts. Defaults to true .
  • type — this is used to determine what media type it’ll parse. It can be a string, array of strings or a function. If it’s not a function, then it’s directly passed into the type-is library. Otherwise, the request is parsed if the data type the function is called with returns a truthy value
  • verify — this is a function with signature (req, res, buf, encoding) , where buf is a Buffer object of the raw request body. The parsing can be aborted by throwing an error in the function.

For example, we can use it as follows:

const express = require('express');  
const bodyParser = require('body-parser');  
const app = express();  
const options = {  
  inflate: true,  
  limit: 1000,  
  reviver: (key, value) => {  
    if (key === 'age') {  
      if (value < 50) {  
        return 'young'  
      }  
      else {  
        return 'old';  
      }  
    }  
    else {  
      return value;  
    }  
  }  
};  
app.use(bodyParser.json(options));
app.post('/', (req, res) => {  
  res.send(req.body);  
});
app.listen(3000);

Then when we make a POST request to / , we get back:

{  
    "name": "foo",  
    "age": "young"  
}

as the response since we check the age field in the reviver function and return 'young' or 'old' depending on the value . Otherwise, we return the value as-is.

The request body is parsed and set as the value of req.body .

Parsing Raw Bodies

We can parse raw bodies as a buffer. It supports the automatic inflation of gzip and deflate encodings.

The parsed body containing the parsed data is populated on the request object, i.e. it’ll be set as the value of req.body .

It takes an optional option object that can take the following properties:

  • inflate — compressed request bodies will be inflated when this is set to true . Otherwise, they’ll be rejected.
  • limit — controls the maximum request body size. If it’s number, then it’s measured in bytes. If it’s a string then it can be parsed into a number of bytes.
  • type — this is used to determine what media type it’ll parse. It can be a string, array of strings or a function. If it’s not a function, then it’s directly passed into the type-is library. Otherwise, the request is parsed if the data type the function is called with returns a truthy value
  • verify — this is a function with signature (req, res, buf, encoding) , where buf is a Buffer object of the raw request body. The parsing can be aborted by throwing an error in the function.

For example, we can use it as follows:

const express = require('express');  
const bodyParser = require('body-parser');  
const app = express();  
const options = {  
  inflate: true,  
  limit: 1000,  
  type: 'text/plain'  
};  
app.use(bodyParser.raw(options));
app.post('/', (req, res) => {  
  res.send(req.body);  
});
app.listen(3000);

Then when we send a POST request to / with the body foo , we get back foo as the response.

This is because we specified text/plain as the type to parse the raw data.

It also smart enough to parse multiple body types. We can do that by passing in an array of type strings as follows:

const express = require('express');  
const bodyParser = require('body-parser');  
const app = express();  
const options = {  
  inflate: true,  
  limit: 1000,  
  type: ['text/plain', 'text/html']  
};  
app.use(bodyParser.raw(options));
app.post('/', (req, res) => {  
  res.send(req.body);  
});
app.listen(3000);

Then when we make a POST request to / with the body <b>foo</b> . Then we get back <b>foo</b> .

Conclusion

We can use the body-parser middleware to parse JSON and raw text bodies.

It also takes a variety of options to let us control whether to inflate compressed request bodies, map JSON values to something else, limit the size of a request body, and so on.

For raw request bodies, we can use body-parser to specify the type that the data is so we can parse it to that type of object and set it to req.body .

Categories
JavaScript Nodejs

How To Create a Simple Front End With Authenticated Routes

Creating pages that require authentication is simple in Angular. A guard is a piece of Angular code that is designed to control access to routes.

In app-routing.module.ts, we have an array of Route objects and in each entry, you can add a canActivate property where you can pass an array of guards.

The guard allows you to check for the requirements to access your routes. You return true, or a promise that resolves to true, to allow people to access your route. Otherwise, return false or return your promise to false.

In this story, we use the API we wrote in a previous piece.

To build the Angular app, you need the Angular CLI. To install it, run npm i -g @angular/cli in your Node.js command prompt. Then, run ng new frontend to generate the skeleton code for your front-end app.

Also, install @angular/material according to the Angular documentation to make our UI look pretty.

After that, replace the default app.module.ts with the following:

import { BrowserModule } from '@angular/platform-browser';  
import { NgModule } from '@angular/core';  
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';  
import {  
  MatButtonModule,  
  MatCheckboxModule,  
  MatInputModule,  
  MatMenuModule,  
  MatSidenavModule,  
  MatToolbarModule,  
  MatTableModule,  
  MatDialogModule,  
  MAT_DIALOG_DEFAULT_OPTIONS,  
  MatDatepickerModule,  
  MatSelectModule,  
  MatCardModule  
} from '@angular/material';  
import { MatFormFieldModule } from '@angular/material/form-field';  
import { AppRoutingModule } from './app-routing.module';  
import { AppComponent } from './app.component';  
import { StoreModule } from '@ngrx/store';  
import { reducers } from './reducers';  
import { FormsModule } from '@angular/forms';  
import { TopBarComponent } from './top-bar/top-bar.component';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { HttpClientModule, HTTP_INTERCEPTORS } from '@angular/common/http';  
import { SessionService } from './session.service';  
import { HttpReqInterceptor } from './http-req-interceptor';  
import { UserService } from './user.service';  
import { CapitalizePipe } from './capitalize.pipe';

@NgModule({  
  declarations: [  
    AppComponent,  
    TopBarComponent,  
    HomePageComponent,  
    LoginPageComponent,  
    SignUpPageComponent,  
    SettingsPageComponent,  
  ],  
  imports: [  
    BrowserModule,  
    AppRoutingModule,  
    StoreModule.forRoot(reducers),  
    BrowserAnimationsModule,  
    MatButtonModule,  
    MatCheckboxModule,  
    MatFormFieldModule,  
    MatInputModule,  
    MatMenuModule,  
    MatSidenavModule,  
    MatToolbarModule,  
    MatTableModule,  
    FormsModule,  
    HttpClientModule,  
    MatDialogModule,  
    MatDatepickerModule,  
    MatMomentDateModule,  
    MatSelectModule,  
    MatCardModule,  
    NgxMaterialTimepickerModule  
  ],  
  providers: [  
    SessionService,  
    {  
      provide: HTTP_INTERCEPTORS,  
      useClass: HttpReqInterceptor,  
      multi: true  
    },  
    UserService,  
    {  
      provide: MAT_DIALOG_DEFAULT_OPTIONS,  
      useValue: { hasBackdrop: false }  
    },  
  ],  
  bootstrap: [AppComponent],  
})  
export class AppModule { }

This creates all our dependencies and components that we’ll add. To make authenticated requests easy with our token, we create an HTTP request interceptor by creating http-req-interceptor.ts:

import { Injectable } from '@angular/core';  
import {  
    HttpEvent,  
    HttpInterceptor,  
    HttpHandler,  
    HttpResponse,  
    HttpErrorResponse,  
    HttpRequest  
} from '@angular/common/http';  
import { Observable } from 'rxjs';  
import { environment } from '../environments/environment'  
import { map, filter, tap } from 'rxjs/operators';  
import { Router } from '@angular/router';

@Injectable()  
export class HttpReqInterceptor implements HttpInterceptor {  
    constructor(  
        public router: Router  
    ) { }

    intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {  
        let modifiedReq = req.clone({});if (localStorage.getItem('token')) {  
            modifiedReq = modifiedReq.clone({  
                setHeaders: {  
                    authorization: localStorage.getItem('token')  
                }  
            });  
        }return next.handle(modifiedReq).pipe(tap((event: HttpEvent<any>) => {  
            if (event instanceof HttpResponse) {}  
        });  
    }  
}

We set the token in all requests, except the login request.

In our environments/environment.ts, we have:

export const environment = {  
  production: false,  
  apiUrl: 'http://localhost:8080'
};

This points to our back end’s URL.

Now, we need to make our side nav. We want to add @ngrx/store to store the side nav’s state. We install the package by running npm install @ngrx/store --save.

We add our reducer by running ng add @ngrx/store to add our reducers.

We add menu-reducers.ts to set state centrally in our flux store and in that file we enter:

const TOGGLE_MENU = 'TOGGLE_MENU';

function menuReducer(state, action) {  
    switch (action.type) {  
        case TOGGLE_MENU:  
            state = action.payload;  
            return state;  
        default:  
            return state  
    }  
}

export { menuReducer, TOGGLE_MENU };

In index.ts of the same folder, we put:

import { menuReducer } from './menu-reducer';  
import { tweetsReducer } from './tweets-reducer';export const reducers = {  
  menu: menuReducer,  
};

To link our reducer to other parts of the app.

In style.css, to get our Material Design look, we put:

/* You can add global styles to this file, and also import other style files */  
@import "~@angular/material/prebuilt-themes/indigo-pink.css";  
body {  
  font-family: "Roboto", sans-serif;  
  margin: 0;  
}

form {  
  mat-form-field {  
    width: 95vw;  
    margin: 0 auto;  
  }  
}

.center {  
  text-align: center;  
}

In index.html, we add the following in between the head tags:

<link href="https://fonts.googleapis.com/css?family=Roboto&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">

Then, we add a service for the user functions by running ng g service user. That will create user.service.ts. We then put:

import { Injectable } from '@angular/core';  
import { HttpClient } from '@angular/common/http';  
import { environment } from 'src/environments/environment';  
import { Router } from '@angular/router)';  
import { JwtHelperService } from "@auth0/angular-jwt";

const helper = new JwtHelperService();
@Injectable({  
  providedIn: 'root'  
})  
export class UserService {

  constructor(  
    private http: HttpClient,  
    private router: Router  
  ) { }

  signUp(data) {  
    return this.http.post(`${environment.apiUrl}/user/signup`, data);  
  }

  updateUser(data) {  
    return this.http.put(`${environment.apiUrl}/user/updateUser`, data);  
  }

  updatePassword(data) {  
    return this.http.put(`${environment.apiUrl}/user/updatePassword`, data);  
  }

  login(data) {  
    return this.http.post(`${environment.apiUrl}/user/login`, data);  
  }

  logOut() {  
    localStorage.clear();  
    this.router.navigate(['/']);  
  }

  isAuthenticated() {  
    try {  
      const token = localStorage.getItem('token');  
      const decodedToken = helper.decodeToken(token);  
      const isExpired = helper.isTokenExpired(token);  
      return !!decodedToken && !isExpired;  
    }  
    catch (ex) {  
      return false;  
    }  
  }}

Each function requests a subscription for an HTTP request, except for the isAuthenticated function which is used to check for the token’s validity.

We also need routing for our app so we can see the pages when we go to the URLs listed below.

In app-routing.module.ts, we put:

import { NgModule } from '@angular/core';  
import { Routes, RouterModule } from '@angular/router';  
import { HomePageComponent } from './home-page/home-page.component';  
import { LoginPageComponent } from './login-page/login-page.component';  
import { SignUpPageComponent } from './sign-up-page/sign-up-page.component';  
import { TweetsPageComponent } from './tweets-page/tweets-page.component';  
import { SettingsPageComponent } from './settings-page/settings-page.component';  
import { PasswordResetRequestPageComponent } from './password-reset-request-page/password-reset-request-page.component';  
import { PasswordResetPageComponent } from './password-reset-page/password-reset-page.component';  
import { IsAuthenticatedGuard } from './is-authenticated.guard';

const routes: Routes = [  
  { path: 'login', component: LoginPageComponent },  
  { path: 'signup', component: SignUpPageComponent },  
  { path: 'settings', component: SettingsPageComponent, canActivate: [IsAuthenticatedGuard] },  
  { path: '**', component: HomePageComponent }];

@NgModule({  
  imports: [RouterModule.forRoot(routes)],  
  exports: [RouterModule]  
})  
export class AppRoutingModule { }

Now, we create the parts that are referenced in the file above.

We need to prevent people from accessing authenticated routes without a token, so we need a guard in Angular. We make that by running ng g guard isAuthenticated. This generates is-authenticated.guard.ts.

We put the following in is-authenticated.guard.ts:

import { Injectable } from '@angular/core';  
import { CanActivate, ActivatedRouteSnapshot, RouterStateSnapshot, UrlTree, Router } from '@angular/router';  
import { Observable } from 'rxjs';  
import { UserService } from './user.service';

@Injectable({  
  providedIn: 'root'  
})  
export class IsAuthenticatedGuard implements CanActivate {  
  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  canActivate(  
    next: ActivatedRouteSnapshot,  
    state: RouterStateSnapshot  
  ): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {  
    const isAuthenticated = this.userService.isAuthenticated();  
    if (!isAuthenticated) {  
      localStorage.clear();  
      this.router.navigate(['/']);  
    }  
    return isAuthenticated;  
  }}

This uses our isAuthenticated function from UserService to check for a valid token. If it’s not valid, we clear it and redirect back to home page.

Now, we create the forms for logging in setting the user data after logging in.

We run ng g component homePage, ng g component loginPage, ng g component topBar, ng g component signUpPage, and ng g component settingsPage. These are for the forms and the top bar components.

The home page is just a static page. We should have home-page.component.html and home-page.component.ts generated after running the commands in our last paragraph.

In home-page.component.html, we put:

<div class="center">  
    <h1>Home Page</h1>  
</div>

Now we make our login page. In login-page.component.ts, we put:

<div class="center">  
    <h1>Log In</h1>  
</div>  
<form #loginForm='ngForm' (ngSubmit)='login(loginForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='loginData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='loginData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Log In</button>  
    <a mat-raised-button routerLink='/passwordResetRequest'>Reset Password</a>  
</form>

In login-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';
@Component({  
  selector: 'app-login-page',  
  templateUrl: './login-page.component.html',  
  styleUrls: ['./login-page.component.scss']  
})  
export class LoginPageComponent implements OnInit {  
  loginData: any = <any>{};

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  }

  login(loginForm: NgForm) {  
    if (loginForm.invalid) {  
      return;  
    }  
    this.userService.login(this.loginData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/settings']);  
      }, err => {  
        alert('Invalid username or password');  
      })  
  }  
}

We make sure that all fields are filled. If they are, the login data will be sent and the token will be saved to local storage if authentication is successful. Otherwise an error alert will be displayed.

In our sign-up page, sign-up-page.component.html, we put:

<div class="center">  
    <h1>Sign Up</h1>  
</div>  
<br>  
<form #signUpForm='ngForm' (ngSubmit)='signUp(signUpForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='signUpData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='signUpData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='signUpData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Sign Up</button>  
</form>

And, in sign-up-page.component.ts, we put:

import { Component, OnInit } from '@angular/core';  
import { UserService } from '../user.service';  
import { NgForm } from '@angular/forms';  
import { Router } from '@angular/router';  
import _ from 'lodash';

@Component({  
  selector: 'app-sign-up-page',  
  templateUrl: './sign-up-page.component.html',  
  styleUrls: ['./sign-up-page.component.scss']  
})  
export class SignUpPageComponent implements OnInit {  
  signUpData: any = <any>{};

  constructor(  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() {  
  }

  signUp(signUpForm: NgForm) {  
    if (signUpForm.invalid) {  
      return;  
    }  
    this.userService.signUp(this.signUpData)  
      .subscribe(res => {  
        this.login();  
      }, err => {  
        console.log(err);  
        if (  
          _.has(err, 'error.error.errors') &&  
          Array.isArray(err.error.error.errors) &&  
          err.error.error.errors.length > 0  
        ) {  
          alert(err.error.error.errors[0].message);  
        }  
      })  
  }

  login() {  
    this.userService.login(this.signUpData)  
      .subscribe((res: any) => {  
        localStorage.setItem('token', res.token);  
        this.router.navigate(['/tweets']);  
      })  
  }  
}

The two pieces of code get the sign-up data and send it to the back end which will save the file if they are all valid.

Similarly, in the settings-page.component.html:

<div class="center">  
    <h1>Settings</h1>  
</div>  
<br>  
<div>  
    <h2>Update User Info</h2>  
</div>  
<br>  
<form #updateUserForm='ngForm' (ngSubmit)='updateUser(updateUserForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Username" required #userName='ngModel' name='userName'  
            [(ngModel)]='updateUserData.userName'>  
        <mat-error *ngIf="userName.invalid && (userName.dirty || userName.touched)">  
            <div *ngIf="userName.errors.required">  
                Username is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <mat-form-field>  
        <input pattern="\S+@\S+\.\S+" matInput placeholder="Email" required #email='ngModel' name='email'  
            [(ngModel)]='updateUserData.email'>  
        <mat-error *ngIf="email.invalid && (email.dirty || email.touched)">  
            <div *ngIf="email.errors.required">  
                Email is required.  
            </div>  
            <div *ngIf="email.invalid">  
                Email is invalid.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update User Info</button>  
</form>  
<br><div>  
    <h2>Update Password</h2>  
</div>  
<br>  
<form #updatePasswordForm='ngForm' (ngSubmit)='updatePassword(updatePasswordForm)'>  
    <mat-form-field>  
        <input matInput placeholder="Password" type='password' required #password='ngModel' name='password'  
            [(ngModel)]='updatePasswordData.password'>  
        <mat-error *ngIf="password.invalid && (password.dirty || password.touched)">  
            <div *ngIf="password.errors.required">  
                Password is required.  
            </div>  
        </mat-error>  
    </mat-form-field>  
    <br>  
    <button mat-raised-button type='submit'>Update Password</button>  
</form>  
<br>

<div *ngIf='currentTwitterUser.id' class="title">  
    <h2>Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Different Twitter Account</button>  
    </div>  
</div>  
<div *ngIf='!currentTwitterUser.id' class="title">  
    <h2>Not Connected to Twitter Account</h2>  
    <div>  
        <button mat-raised-button (click)='redirectToTwitter()'>Connect to Twitter Account</button>  
    </div>  
</div>

In settings-page.component.html, we put:

import { Component, OnInit } from '@angular/core';  
import { ActivatedRoute, Router } from '@angular/router';  
import { SessionService } from '../session.service';  
import { NgForm } from '@angular/forms';  
import { UserService } from '../user.service';
@Component({  
  selector: 'app-settings-page',  
  templateUrl: './settings-page.component.html',  
  styleUrls: ['./settings-page.component.scss']  
})  
export class SettingsPageComponent implements OnInit {  
  currentTwitterUser: any = <any>{};  
  elements: any[] = [];  
  displayedColumns: string[] = ['key', 'value'];  
  updateUserData: any = <any>{};  
  updatePasswordData: any = <any>{};

  constructor(  
    private sessionService: SessionService,  
    private userService: UserService,  
    private router: Router  
  ) { }

  ngOnInit() { }

  updateUser(updateUserForm: NgForm) {  
    if (updateUserForm.invalid) {  
      return;  
    }  
    this.userService.updateUser(this.updateUserData)  
      .subscribe(res => {  
        alert('Updated user info successful.');  
      }, err => {  
        alert('Updated user info failed.');  
      })  
  }

  updatePassword(updatePasswordForm: NgForm) {  
    if (updatePasswordForm.invalid) {  
      return;  
    }  
    this.userService.updatePassword(this.updatePasswordData)  
      .subscribe(res => {  
        alert('Updated password successful.');  
      }, err => {  
        alert('Updated password failed.');  
      })  
  }  
}

Similar to other pages, this sends a request payload for changing user data and passwords to our back end.

Finally, to make our top bar, we put the following in top-bar.component.html:

<mat-toolbar>  
    <a (click)='toggleMenu()' class="menu-button">  
        <i class="material-icons">  
            menu  
        </i>  
    </a>  
    Twitter Automator  
</mat-toolbar>

And in top-bar.component.ts:

import { Component, OnInit } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from '../reducers/menu-reducer';
@Component({  
  selector: 'app-top-bar',  
  templateUrl: './top-bar.component.html',  
  styleUrls: ['./top-bar.component.scss']  
})  
export class TopBarComponent implements OnInit {  
  menuOpen: boolean;

  constructor(  
    private store: Store<any>  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  }

  ngOnInit() {  
  }

  toggleMenu() {  
    this.store.dispatch({ type: TOGGLE_MENU, payload: !this.menuOpen });  
  }  
}

In app.component.ts, we put:

import { Component, HostListener } from '@angular/core';  
import { Store, select } from '@ngrx/store';  
import { TOGGLE_MENU } from './reducers/menu-reducer';  
import { UserService } from './user.service';
@Component({  
  selector: 'app-root',  
  templateUrl: './app.component.html',  
  styleUrls: ['./app.component.scss']  
})  
export class AppComponent {  
  menuOpen: boolean;

  constructor(  
    private store: Store<any>,  
    private userService: UserService  
  ) {  
    store.pipe(select('menu'))  
      .subscribe(menuOpen => {  
        this.menuOpen = menuOpen;  
      })  
  }

  isAuthenticated() {  
    return this.userService.isAuthenticated();  
  }

  @HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

  logOut() {  
    this.userService.logOut();  
  }  
}

And in app.component.html, we have:

<mat-sidenav-container class="example-container">  
    <mat-sidenav mode="side" [opened]='menuOpen'>  
        <ul>  
            <li>  
                <b>  
                    Twitter Automator  
                </b>  
            </li>  
            <li>  
                <a routerLink='/login' *ngIf='!isAuthenticated()'>Log In</a>  
            </li>  
            <li>  
                <a routerLink='/signup' *ngIf='!isAuthenticated()'>Sign Up</a>  
            </li>  
            <li>  
                <a href='#' (click)='logOut()' *ngIf='isAuthenticated()'>Log Out</a>  
            </li>  
            <li>  
                <a routerLink='/tweets' *ngIf='isAuthenticated()'>Tweets</a>  
            </li>  
            <li>  
                <a routerLink='/settings' *ngIf='isAuthenticated()'>Settings</a>  
            </li>  
        </ul></mat-sidenav>  
    <mat-sidenav-content>  
        <app-top-bar></app-top-bar>  
        <div id='content'>  
            <router-outlet></router-outlet>  
        </div>  
    </mat-sidenav-content>  
</mat-sidenav-container>

This allows us to toggle our side nav menu. Note that we have:

@HostListener('document:click', ['$event'])  
  public onClick(event) {  
    const isOutside = !event.target.className.includes("menu-button") &&  
      !event.target.className.includes("material-icons") &&  
      !event.target.className.includes("mat-drawer-inner-container")  
    if (isOutside) {  
      this.menuOpen = false;  
      this.store.dispatch({ type: TOGGLE_MENU, payload: this.menuOpen });  
    }  
  }

To detect clicks outside the side nav. If we click outside, i.e. we’re not clicking on any element with those classes, then we close the menu.

this.store.dispatch propagates the closed state to all components.

Categories
Express JavaScript

Guide to the Express Application Object — Routing

The core part of an Express app is the Application object. It’s the application itself.

In this piece, we’ll look at the methods of the app object and what we can do with it.


app.METHOD(path, callback [, callback …])

This method routes an HTTP request. METHOD is a placeholder for various routing methods that Express supports.

It takes the following arguments:

  • path — It can be a string or regex representing paths or patterns of paths. The default is /.
  • callback — a function to handle requests. It can be a middleware function, a series of them, array of them, or a combination of all of the above.

The following routing methods are supported by Express:

  • checkout
  • copy
  • delete
  • get
  • head
  • lock
  • merge
  • mkactivity
  • mkcol
  • move
  • m-search
  • notify
  • options
  • patch
  • post
  • purge
  • put
  • report
  • search
  • subscribe
  • trace
  • unlock
  • unsubscribe

All methods take the same arguments and work exactly the same way. So app.get and app.unlock are the same.


app.param([name], callback)

app.param adds callback triggers for route parameters. name is the parameter or an array of them. callback is a callback function.

The parameters of the callback are the request object, response object, next middleware function, the value of the parameter, and the name of the parameter in the given order.

For example, we can use it as follows:

The code above will get the id URL parameter when it exists with the app.param callback.

Then in the callback, we get id to req.id and call next to call our route handler in app.get.

Then we call res.send(req.id); to send the response with req.id, which we set earlier.

Note that id in app.param has to match :id with the route handlers.

We can pass in an array of parameters that we want to watch for:

Then if we make a request for /1/foo, we’ll get 1 and foo one at a time as the value of the value parameter.


app.path()

app.path returns the path of mounted apps as a string.

For example, we can use it as follows:

Then we get '' for app.path(), '/foo' for foo.path(), and '/foo/bar' for bar.path() since we mounted foo to app and bar to foo.

<img class="s t u hh ai" src="https://miro.medium.com/max/6438/0*hC4qa3eKTWD_g_hs" width="3219" height="4836" srcSet="https://miro.medium.com/max/552/0*hC4qa3eKTWD_g_hs 276w, https://miro.medium.com/max/1104/0*hC4qa3eKTWD_g_hs 552w, https://miro.medium.com/max/1280/0*hC4qa3eKTWD_g_hs 640w, https://miro.medium.com/max/1400/0*hC4qa3eKTWD_g_hs 700w" sizes="700px" role="presentation"/>

Photo by Bundo Kim on Unsplash.


app.post(path, callback [, callback …])

We can use the app.post method to handle POST requests with the given path by passing in a callback route handler.

It takes the following arguments:

  • path — It can be a string or regex representing paths or patterns of paths. The default is /.
  • callback — a function to handle requests. It can be a middleware function, a series of them, array of them, or a combination of all of the above.

For example, we can use it as follows:

Then when we make a POST request with a client like Postman, we should see “POST request made.”


app.put(path, callback [, callback …])

We can use the app.put method to handle PUT requests with the given path by passing in a callback route handler.

It takes the following arguments:

  • path — It can be a string or regex representing paths or patterns of paths. The default is /.
  • callback — a function to handle requests. It can be a middleware function, a series of them, array of them, or a combination of all of the above.

For example, we can use it as follows:

Then when we make a PUT request with a client like Postman, we should see “PUT request made.”


Conclusion

We can intercept the parameters sent from requests with the app.params method.

To listen to POST requests, we can use the app.post method. To listen to PUT requests, we can use the app.put method.

app.path lets us get the path of mounted apps.

app also has a long list of methods for listening to all kinds of requests.