Notes on quick 1st round of remaining part of John Smilga Node Express tutorial (after Tasks project)

Last updated on 4 Oct. 2024
2 & 3 Oct. 2024:
Continuing from after Tasks project in John Smilga Express tutorial...Node.js Projects,
https://youtu.be/rltfdjcXjmk?t=11235 (from around 3 hrs, 7 mins in the video).

The next project taken up is the Store project.


Most of the source code of the Store project is straight-forward and somewhat similar to Tasks project. However, controllers\products.js has fair deal of complexity as it uses the various query features of Mongoose find() with Query String params being the mechanism for specifying the query to the get route ('routed' to getAllProducts() method in controllers\products.js file).

The code for string data query is simple. Numeric data query code is complex which is covered below in some detail.

Chaining the find method conditionally with sort and select methods, and unconditionally with skip and limit methods is new in the tutorial. The await keyword is not used with find but used with a 'result' object that holds return value of the earlier chained methods.

https://mongoosejs.com/docs/api/model.html#Model.find() examples do not cover such chaining. It instead shows three parameters for find() - filter (having the search conditions part), projection (having the select part - https://mongoosejs.com/docs/api/query.html#Query.prototype.select() ) and options (having the limit, skip and sort (and more options) - https://mongoosejs.com/docs/api/query.html#Query.prototype.setOptions() ). IIRC, the tutorial video does cover such find() usage too but then says that dynamic addition of sort and select could be done using the approach it takes (and, IIRC, cannot be done using only parameters without chaining).

How find() Works in Mongoose, https://thecodebarbarian.com/how-find-works-in-mongoose, Feb. 2019 - covers find() with parameters and with chaining. Article is very helpful to get better understanding of these topics.
...

In controllers\products.js (in final version) of Store project:
      if (options.includes(field)) {
        queryObject[field] = { [operator]: Number(value) };
      }
----
As I understand it, if only operator is used then the property name would be operator. We need property name to be the value of the operator variable and so use [operator]. See references below:

"The object initializer syntax also supports computed property names. That allows you to put an expression in square brackets [], that will be computed and used as the property name."

What do square brackets around a property name in an object literal mean?, https://stackoverflow.com/questions/34831262/what-do-square-brackets-around-a-property-name-in-an-object-literal-mean has an interesting answer about using "key"*5 as property name. Without square brackets it would result in syntax error, it says. ["key"*5] works!

===============
In controllers\products.js (in final version) of Store project:
  if (numericFilters) {
    const operatorMap = {
      '>': '$gt',
      '>=': '$gte',
      '=': '$eq',
      '<': '$lt',
      '<=': '$lte',
    };
    const regEx = /\b(<|>|>=|=|<|<=)\b/g;
    let filters = numericFilters.replace(
      regEx,
      (match) => `-${operatorMap[match]}-`
    );
    const options = ['price', 'rating'];
    filters = filters.split(',').forEach((item) => {
      const [field, operator, value] = item.split('-');
      if (options.includes(field)) {
        queryObject[field] = { [operator]: Number(value) };
      }
    });
  }
----------
https://hn.algolia.com/api has examples of conditional search which style seems to have been used by Smilga tutorial (with attribution).
"Stories between timestamp X and timestamp Y (in second)
...
Decoding regex: /\b(<|>|>=|=|<|<=)\b/g
\b word boundary  Ref: https://www.regular-expressions.info/wordboundaries.html - 'Simply put: \b allows you to perform a “whole words only” search using a regular expression in the form of \bword\b. A “word character” is a character that can be used to form words. All characters that are not “word characters” are “non-word characters”.'
Ref: https://www.regular-expressions.info/shorthand.html "\w stands for “word character”. It always matches the ASCII characters [A-Za-z0-9_]. Notice the inclusion of the underscore and digits."
My understanding is that < > =  are NOT word characters [confirmed by https://regex101.com/ test]

| is used for alternation (like OR) Ref: https://www.regular-expressions.info/alternation.html
( and ) are used for grouping  Ref: https://www.regular-expressions.info/brackets.html
g at end stands for global    From https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/RegExp/global : "The g flag indicates that the regular expression should be tested against all possible matches in a string."
...
So /\b(<|>|>=|=|<|<=)\b/g seems to mean:
find/get global matches for < or > or >= or = or < or <= which has word boundaries to its left and right.
...
Tested /\b(<|>|>=|=|<|<=)\b/g  in https://regex101.com/
price<2,rating>4.5 - has 2 matches < and >
price<<2,rating>4.5 - has only one match >
price<<2,rating>=4.5 - has only one match >=
-----------

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String/replace : "The replace() method of String values returns a new string with one, some, or all matches of a pattern replaced by a replacement. The pattern can be a string or a RegExp, and the replacement can be a string or a function called for each match. If pattern is a string, only the first occurrence will be replaced. The original string is left unchanged." ... "If (replacement is) a function, it will be invoked for every match and its return value is used as the replacement text."
----

// find all documents named john and at least 18
await MyModel.find({ name: 'john', age: { $gte: 18 } }).exec();
---

In above controllers\products.js code, my understanding of it is that if numericFilters is "price>20,rating<=4.5" then it will be processed as follows (see // (comment) lines in code below):
  if (numericFilters) {
    const operatorMap = {
      '>': '$gt',
      '>=': '$gte',
      '=': '$eq',
      '<': '$lt',
      '<=': '$lte',
    };
    const regEx = /\b(<|>|>=|=|<|<=)\b/g;
    let filters = numericFilters.replace(
      regEx,
      (match) => `-${operatorMap[match]}-`
    );
// Above code will have two matches and filters will become "price-$gt-20,rating-$lte-4.5"
    const options = ['price', 'rating'];
    filters = filters.split(',').forEach((item) => {
      const [field, operator, value] = item.split('-');
// field: price, operator: $gt, value: 20
// field: rating, operator: $lte, value: 4.5
      if (options.includes(field)) {
        queryObject[field] = { [operator]: Number(value) };
// Following properties and associated values will be added to queryObject:
// price: {$gt : 20}
// rating: {$lte : 4.5}
// The above is what is required for mongoose find().
      }
    });
  }
---------
=============================

4 Oct. 2024:

https://mongoosejs.com/docs/queries.html which is part of the Mongoose docs guide covers chaining and parameters equivalent example for find().

About JWT-Basics project in Express tutorial: https://youtu.be/rltfdjcXjmk?t=18330 (from around 5 hrs, 5 mins. in the video)
The code is quite straight-forward. Some notable points:

jwt.io - useful site.

The tutorial does not send back the jwt token as a cookie. Instead it sends it back as part of the json response for the login route. Also, the dashboard route expects the token to be specified as part of the Authorization header (Bearer token).

https://www.loginradius.com/blog/engineering/guest-post/nodejs-authentication-guide/ sends the token to the client as a cookie on successful login or registration. Protected routes then check for token in the cookies sent with the request.

Don't know which is the more appropriate approach for JWT. Here are some posts/articles on it got from Google search, but in my quick read, I did not get a clear answer. There is also some confusion about JWT being stored in cookie and used for authentication and using a cookie without JWT for authentication.
  1. Should JWT token be stored in a cookie, header or body, https://security.stackexchange.com/questions/130548/should-jwt-token-be-stored-in-a-cookie-header-or-body - Looks like this post has the best coverage among these posts/articles for the question I have raised.
  2. Section "Comparison of JWT and Cookies storage" in https://strapi.io/blog/introduction-to-jwt-and-cookie-storage
  3. JWT vs cookies for token-based authentication, https://stackoverflow.com/questions/37582444/jwt-vs-cookies-for-token-based-authentication

In middleware\auth.js :
const { UnauthenticatedError } = require('../errors')
----
errors is a folder which has an index.js file whose contents are:
const CustomAPIError = require('./custom-error')
const BadRequestError = require('./bad-request')
const UnauthenticatedError = require('./unauthenticated')

module.exports = {
  CustomAPIError,
  BadRequestError,
  UnauthenticatedError,
}
---
custom-error.js, bad-request.js and unauthenticated.js files are present in errors folder.

Node docs explains how it works. First https://nodejs.org/api/packages.html#modules-loaders states that CommonJS module loader "supports folders as modules" and links to https://nodejs.org/api/modules.html#folders-as-modules which states the way folders as modules works with one step being node looking for an index.js file in the folder and loading it if found.
======================================
 

Comments