# @zxventures/utils (`icp-pkg-utils`)

## Table of Contents <!-- omit in toc -->
- [@zxventures/utils (`icp-pkg-utils`)](#zxventuresutils-icp-pkg-utils)
  - [Why?](#why)
  - [Quickstart](#quickstart)
    - [Install](#install)
    - [Usage](#usage)
      - [1st way](#1st-way)
      - [2nd way](#2nd-way)
      - [✅ constants](#-constants)
        - [countryCodes](#-constants)
      - [✅ errors](#-errors)
        - [CustomBaseError](#custombaseerror)
        - [GraphqlError](#graphqlerror)
        - [JoiValidationError](#joivalidationerror)
        - [ProcessEnvValidationError](#processenvvalidationerror)
      - [✅ helpers](#-helpers)
        - [hash](#hash)
        - [json](#json)
        - [text](#text)
      - [✅ validations](#-validations)
        - [commons](#commons)
        - [regex-patterns](#regex-patterns)
        - [joi-schema-validation](#joi-schema-validation)
        - [env-variables-validation](#env-variables-validation)
  - [Run unit test and code coverage](#run-unit-test-and-code-coverage)
  - [Package Dependencies](#package-dependencies)
  - [Contributors](#contributors)


## Why?

Common utilities to use on `node.js` projects.

The main idea here is to condense all the code commonly used in the different projects/microservices within @zxventures.

With this approach, we can reduce the duplicated snippets between projects along with the error occurrences, and their unit tests saving time on each new project/microservice.

## Quickstart

### Install

> This package is scoped and is private by default.
> 
> Therefore, to use this package, it is necessary to include a file named `.npmrc` at the root of the project where this package will be used with the following content:
> 
> ```bash
> //registry.npmjs.org/:_authToken=PUT-THE-AUTHENTICATION-TOKEN-HERE
> ```
> 
> **NOTE:** The authentication token should be requested from the infrastructure team.
> 

Using NPM:

```bash
npm install @zxventures/utils
```


### Usage


This package has several common utilities.
These utilities are grouped into 4 main groups: [constants](#constants), [errors](#errors), [helpers](#helpers), [validations](#validations).

There are 2 ways to use the utilities in this package.

#### **1st way**

The first way is to use the root namespace of the package and require each of these groups:

```javascript
// package root namespace: '@zxventures/utils'
const { constants, errors, helpers, validations } = require('@zxventures/utils');

// to work with 'constants'
const { countryCodes } = constants;

// to work with 'errors'
const { CustomBaseError, GraphqlError, JoiValidationError, ProcessEnvValidationError } = errors;

// to work with 'helpers' (hash, json, text)
const { getHashFromObject } = helpers.hash;
const { parse, stringify, clone, prettyPrint, mergeDeep, getPropertyValueCaseInsensitive } = helpers.json;
const { capitalize } = helpers.text;

// to work with 'validations'
const { commons, joiSchemaValidation, regexPatterns, envVariablesValidation } = validations;
```

#### **2nd way**

The second way is to use the specific namespace:

```javascript
// to work with 'constants'
const { countryCodes } = require('@zxventures/utils/constants');

// to work with a specific 'constant' namespace
const countryCodes = require('@zxventures/utils/constants/country-codes');


// to work with 'errors'
const { CustomBaseError, GraphqlError, JoiValidationError, ProcessEnvValidationError } = require('@zxventures/utils/errors');

// to work with a specific 'error' namespace
const CustomBaseError = require('@zxventures/utils/errors/custom-base-error');
const GraphqlError = require('@zxventures/utils/errors/graphql-error');
const JoiValidationError = require('@zxventures/utils/errors/joi-validation-error');
const ProcessEnvValidationError = require('@zxventures/utils/errors/process-env-validation-error');


// to work with 'helpers'
const { hash, json, text } = require('@zxventures/utils/helpers');

// to work with a specific 'helper' namespace
const { getHashFromObject } = require('@zxventures/utils/helpers/hash');
const { parse, stringify, clone, prettyPrint, mergeDeep, getPropertyValueCaseInsensitive } = require('@zxventures/utils/helpers/json');
const { capitalize } = require('@zxventures/utils/helpers/text');


// to work with 'validations'
const { commons, joiSchemaValidation, regexPatterns, envVariablesValidation } = require('@zxventures/utils/validations');

// to work with a specific 'validation' namespace
const { isAsyncFunction, isPlainObject /*, and others */ } = require('@zxventures/utils/validations/commons');
const { joiSchemaValidationSync, joiSchemaValidationAsync } = require('@zxventures/utils/validations/joi-schema-validation');
const { validateProcessEnv } = require('@zxventures/utils/validations/env-variables-validation');
const { base64Pattern, isoDatePattern } = require('@zxventures/utils/validations/regex-patterns');
```

#### ✅ **constants**

In this group, at the moment the only constant is the information of the countries (`countryCodes`) and has **250 records**.

This information about countries has the following properties for each country:

```javascript
[
  // ...
  {
    name: "Mexico",   // full country name
    alpha2: "MX",     // country code using 2 chars and the value is in UPPERCASE
    alpha3: "MEX",    // country code using 3 chars and the value is in UPPERCASE
    dialCode: "+52"   // country dialing code
  },
  // ...
]
```

#### ✅ **errors**

In this group, there are 4 error classes: [CustomBaseError](#custombaseerror), [GraphqlError](#graphqlerror) [JoiValidationError](#joivalidationerror) and [ProcessEnvValidationError](#processenvvalidationerror).


##### CustomBaseError

The first one is `CustomBaseError` which inherits from `Error`.
The idea here is that all of our custom errors inherit from this `CustomBaseError` base class.
This base class (or `abstract class` in other programming languages) should only be used as a base class and not as a direct instance.
This base class makes sure to set the value of the property `name` of the error from the name of the constructor of the class that inherits from it.
This class has a constructor with a single parameter which is the `message` (error message).

```javascript
const CustomBaseError = require('@zxventures/utils/errors/custom-base-error');

class MyCustomError extends CustomBaseError {
  constructor(message, errorCode, originalError) {
    super(message);
    this.errorCode = errorCode;
    this.originalError = originalError;
  }
}

const originalError = new Error('This is the original error');
const myCustomError = new MyCustomError('This is the custom error message', 101, originalError);

console.log('is instance of Error:', myCustomError instanceof Error);
console.log('is instance of CustomBaseError:', myCustomError instanceof CustomBaseError);
console.log('error name:', myCustomError.name);

/*
 * console output:
 * > is instance of Error: true
 * > is instance of CustomBaseError: true
 * > error name: MyCustomError
 * 
 */
```

##### GraphqlError

This error is commonly used within microservices/projects that work with GraphQL to throw a custom error.

This class inherits from `CustomBaseError` and has a constructor with two parameters, the first one is the `message` (error message) and the second one is the `code` (error code).

```javascript
const GraphqlError = require('@zxventures/utils/errors/graphql-error');

// ...
throw new GraphqlError(
  'Error trying to get the minimum order value',
  'IOP-CHK-1001',
);
// ...
```

##### JoiValidationError

This error is used to classify the **Joi** errors and has the properties `message` and `originalError`.

> In this package, there are two useful methods to validate a Joi scheme (`joiSchemaValidationSync` and `joiSchemaValidationAsync` in the section `'/validations/joi-schema-validation'`) and throw a `JoiValidationError` if the schema is not valid.


```javascript
const Joi = require('joi');
const { JoiValidationError, CustomBaseError } = require('@zxventures/utils/errors');
const { joiSchemaValidationSync } = require('@zxventures/utils/validations/joi-schema-validation');

// joi schema
const joiSchemaSample = Joi.object({
  addressId: Joi.string().trim().required(),
  city: Joi.string().trim().required(),
  geoCoordinates: Joi.array().items(Joi.number()).length(2),
});

// any object to validate
const objectToValidate = {
  id: 1001,
};

try {
  // try to validate an invalid Joi schema
  const validatedObject = joiSchemaValidationSync({
    schema: joiSchemaSample, 
    inputToValidate: objectToValidate 
  });
  console.log('validation result: ', validatedObject);
} catch (error) {
  console.error('is instance of Error:', error instanceof Error);
  console.error('is instance of CustomBaseError:', error instanceof CustomBaseError);
  console.error('is instance of JoiValidationError:', error instanceof JoiValidationError);
  console.log('error name:', error.name);
}

/*
 * console output:
 * > is instance of Error: true
 * > is instance of CustomBaseError: true
 * > is instance of JoiValidationError: true
 * > error name: JoiValidationError
 * 
 */
```

##### ProcessEnvValidationError

This error is used to classify errors related to the validation of environment variables (`process.env`) and has the `message` and `variableName` properties.

> In this package, there is a useful method to validate the environment variables that are required. 
> This method can be found in section `'/validations/env-variables-validation'` under the name `validateProcessEnv`.


```javascript
const { ProcessEnvValidationError, CustomBaseError } = require('@zxventures/utils/errors');
const { validateProcessEnv } = require('@zxventures/utils/validations/env-variables-validation');

try {
  // try to validate any environment variable that does not exist.
  const envVariables = validateProcessEnv('ANY_VAR_NOT_PRESENT');
  console.log('validation result: ', envVariables);
} catch (error) {
  console.error('is instance of Error:', error instanceof Error);
  console.error('is instance of CustomBaseError:', error instanceof CustomBaseError);
  console.error('is instance of ProcessEnvValidationError:', error instanceof ProcessEnvValidationError);
  console.log('error name:', error.name);
}

/*
 * console output:
 * > is instance of Error: true
 * > is instance of CustomBaseError: true
 * > is instance of ProcessEnvValidationError: true
 * > error name: ProcessEnvValidationError
 * 
 */
```


#### ✅ **helpers**

In this group there are 3 main helpers: [hash](#hash), [json](#json) and [text](#text).


##### hash

This helper has a function that allows us to obtain a hash from an object.

```javascript
const { getHashFromObject } = require('@zxventures/utils/helpers/hash');

const someObject = {
  id: 1,
  name: 'some name'
};

const hash = getHashFromObject(someObject);
console.log('hash:', hash);

/*
 * console output:
 * > hash: 7f92b7545acc8f1fe3a48a8a794759cc2e92a05464de6c96722b4096e9ac7408
 * 
 */
```

##### json

This helper has several helper functions that allow us to do common operations with JSON such as: 

`stringify` and `parse`:  It's similar to `JSON.stringify` and `JSON.parse`. It serializes and deserializes otherwise valid JSON objects containing **circular references** into and from a specialized JSON format. Basically, it allows us to avoid a possible [cyclic object value](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Errors/Cyclic_object_value) exception. It's necessary that if you serialize with this `stringify` function you also deserialize with this `parse` function. It's based on [@ungap/structured-clone](https://www.npmjs.com/package/@ungap/structured-clone) npm package.

The `parse` helper function uses internally the native `JSON.parse()` function. Therefore, it receives the same two parameters as the native function. The 1st parameter is the `text` which is a string to parse as JSON and the 2nd parameter is an optional parameter called `reviver`. For more information see the [JSON.parse()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/parse) documentation.

Similarly, the `stringify` function uses internally the native `JSON.stringify()` function. Therefore, it receives the same three parameters as the native function. The 1st parameter is the `value` which is an *object* to be converted into a JSON string. The 2nd parameter is optional and is called `replacer`, and the 3rd parameter is also optional and is called `space`. For more information see the [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) documentation.

```javascript
const { parse, stringify } = require('@zxventures/utils/helpers/json');

// a simple object
const obj = {
  id: 1,
  name: 'some name'
};

// add a circular reference/structure
obj.myself = obj;

// serialize object with circular reference
const stringifyResult = stringify(obj);
console.log('stringify result:', stringifyResult);

// deserialize object with circular reference
const parseResult = parse(stringifyResult);
console.log('parse result:', parseResult);

/*
 * console output:
 * > stringify result: [[2,[[1,2],[3,4],[5,0]]],[0,"id"],[0,1],[0,"name"],[0,"some name"],[0,"myself"]]
 * > parse result: <ref *1> { id: 1, name: 'some name', myself: [Circular *1] }
 * 
 */
```

`clone`: It is a simple way to create a copy of an object.

```javascript
const { clone } = require('@zxventures/utils/helpers/json');

// a simple object
const obj = {
  id: 1,
  name: 'some name'
};

// could have a circular reference/structure
obj.myself = obj;

// create a clone
const cloned = clone(obj);

// change some value and it shouldn't affect the original object
cloned.id = 2; 

// trace: check values 
console.log(`obj { id: ${obj.id}, myselfId: ${obj.myself.id} }`);
console.log(`cloned { id: ${cloned.id}, myselfId: ${cloned.myself.id} }`);

/*
 * console output:
 * > obj { id: 1, myselfId: 1 }
 * > cloned { id: 2, myselfId: 2 }
 * 
 */
```


`prettyPrint`: This is a simple way to print a JSON in a pretty way. It simply encapsulates the native `JSON.stringify(object, null, 2)` (extra parameters) to get a pretty JSON string. This function receives the same three parameters as the native function: `object`, `replacer` and `space`. For more information see the [JSON.stringify()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify) documentation.

IMPORTANT: The object passed as a parameter **MUST NOT HAVE A CIRCULAR REFERENCE/STRUCTURE** because it uses the native function directly.

```javascript
const { prettyPrint } = require('@zxventures/utils/helpers/json');

// a simple object. 
// IMPORTANT: must not have a circular reference/structure
const obj = {
  id: 1,
  name: 'some name',
  data: {
    city: {
      id: 1001,
      iata: 'MEX',
    },
  },
};

// print the object in a simple way (not pretty)
console.log('simple way:', obj);

// get a pretty JSON string and print it
const pretty = prettyPrint(obj);
console.log('pretty-print:', pretty);

/*
 * console output:
 * > simple way: { id: 1, name: 'some name', data: { city: { id: 1001, iata: 'MEX' } } }
 * > pretty-print: {
 *     "id": 1,
 *     "name": "some name",
 *     "data": {
 *       "city": {
 *         "id": 1001,
 *         "iata": "MEX"
 *       }
 *     }
 *   }
 * 
 */
```

`mergeDeep`: It's a useful way to recursively merge values into a javascript object.

```javascript
const { mergeDeep, prettyPrint } = require('@zxventures/utils/helpers/json');

// a simple object base
const obj = {
  id: 1,
  name: 'some name',
  data: {
    city: {
      id: 1001,
      iata: 'MEX',
    },
  },
};

// some modification objects
const obj2 = {
  id: 2,
  name: 'other name',
  data: {
    city: {
      id: 1002,
    },
  },
  selectedIds: [],
};

const obj3 = {
  id: 3,
  data: {
    city: {
      iata: 'COL',
    },
  },
  selectedIds: [1, 3, 5],
};

// merge objects using native way. 
// NOTE: Take a look at how the object result lost the 'data.city.id' property.
const objectAssignResult = Object.assign({}, obj, obj2, obj3);

// merge objects using 'merge deep' helper 
// NOTE: Take a look at how the arrays are overwritten as the native way also does.
const mergeDeepResult = mergeDeep({}, obj, obj2, obj3);

// print the result objects
console.log('object-assign result:', prettyPrint(objectAssignResult));
console.log('merge-deep result:', prettyPrint(mergeDeepResult));

/*
 * console output:
 * > object-assign result: {
 *     "id": 3,
 *     "name": "other name",
 *     "data": {
 *       "city": {
 *         "iata": "COL"
 *       }
 *     },
 *     "selectedIds": [
 *       1,
 *       3,
 *       5
 *     ]
 *   }
 * > merge-deep result: {
 *     "id": 3,
 *     "name": "other name",
 *     "data": {
 *       "city": {
 *         "id": 1002,
 *         "iata": "COL"
 *       }
 *     },
 *     "selectedIds": [
 *       1,
 *       3,
 *       5
 *     ]
 *   }
 * 
 */
```

`getPropertyValueCaseInsensitive`: It is a useful way to get the value of some property in a case insensitive way.
It also allows us to get the value in a deep way.
The first parameter is the object and the second parameter is a string with the property name or the path to the deep property name.

```javascript
const { getPropertyValueCaseInsensitive } = require('@zxventures/utils/helpers/json');

// a object without `CamelCase` properties
const obj = {
  UniqueID: 1001,
  DatA: {
    User: {
      naME: 'user name',
      Email: 'username@email.com',
      LOCATION: {
        NAme: 'Bogota, Colombia',
        laTITUde: 4.606880,
        lonGItuDE: -74.071838,
      },
    },
  },
};

// e.g. get the value of a root property
const value1 = getPropertyValueCaseInsensitive(obj, "uniqueId");
console.log('value1:', value1);

// e.g. get the value of a deep property
const value2 = getPropertyValueCaseInsensitive(obj, "DATA.USER.NAME");
console.log('value2:', value2);

const value3 = getPropertyValueCaseInsensitive(obj, "data.user.location.name");
console.log('value3:', value3);

/*
 * console output:
 * value1: 1001
 * value2: user name
 * value3: Bogota, Colombia
 * 
 */
```

##### text

This helper has a function called `capitalize` that allows us to obtain an [capitalize](https://en.wikipedia.org/wiki/Capitalization) text. 
It receives as the first parameter a string and the second parameter is an optional parameter to indicate if only the first word is capitalized (it is the default value) or if all the words must be capitalized.

```javascript
const { capitalize } = require('@zxventures/utils/helpers/text');

const value1 = capitalize('this is an example');
console.log('value1:', value1);

const value2 = capitalize('this is an example', false);
console.log('value2:', value2);

const value3 = capitalize('THIS IS AN EXAMPLE', true);
console.log('value3:', value3);

/*
 * console output:
 * value1: This is an example
 * value2: This Is An Example
 * value3: This is an example
 * 
 */
```


#### ✅ **validations**

In this group there are 4 validation utilities: [commons](#commons), [regex-patterns](#regex-patterns), [joi-schema-validation](#joi-schema-validation) and [env-variables-validation](#env-variables-validation).

##### commons

In this section, there are a large number of validations that are commonly used in our projects. 
All of them return a boolean value and their names are clear and self-descriptive indicating their purpose.

```javascript
const {
  isUndefined,
  isNull,
  isNullOrUndefined,
  isString,
  isNumber,
  isArray,
  isFunction,
  isAsyncFunction,
  isPromise,
  isObject,
  isPlainObject,
  isDate,
  isIsoDate,
  isBase64,
  isURLSearchParams,
  isRegExp,

  stringHasValues,
  arrayHasValues,
} = require('@zxventures/utils/validations/commons');
```

##### regex-patterns

In this section, there are 2 regular expressions commonly used in our projects: `base64Pattern` and `isoDatePattern`.

```javascript
const { base64Pattern, isoDatePattern } = require('@zxventures/utils/validations/regex-patterns');

console.log('base64Pattern:', base64Pattern);
console.log('isoDatePattern:', isoDatePattern);

/*
 * console output:
 * > base64Pattern: /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/
 * > isoDatePattern: /^(?:[-+]\d{2})?(?:\d{4}(?!\d{2}\b))(?:(-?)(?:(?:0[1-9]|1[0-2])(?:\1(?:[12]\d|0[1-9]|3[01]))?|W(?:[0-4]\d|5[0-2])(?:-?[1-7])?|(?:00[1-9]|0[1-9]\d|[12]\d{2}|3(?:[0-5]\d|6[1-6])))(?![T]$|[T][\d]+Z$)(?:[T\s](?:(?:(?:[01]\d|2[0-3])(?:(:?)[0-5]\d)?|24:?00)(?:[.,]\d+(?!:))?)(?:\2[0-5]\d(?:[.,]\d+)?)?(?:[Z]|(?:[+-])(?:[01]\d|2[0-3])(?::?[0-5]\d)?)?)?)?$/
 * 
 */
```

##### joi-schema-validation

In this section, there are 2 functions to validate **Joi** schemas, one of them for synchronous validations and the other for [asynchronous](https://developer.mozilla.org/en-US/docs/Learn/JavaScript/Asynchronous) validations: `joiSchemaValidationSync` and `joiSchemaValidationAsync`.

```javascript
const Joi = require('joi');
const { prettyPrint } = require('@zxventures/utils/helpers/json');
const { joiSchemaValidationSync, joiSchemaValidationAsync } = require('@zxventures/utils/validations/joi-schema-validation');

// joi schema
const joiSchemaSample = Joi.object({
  addressId: Joi.string().trim().required(),
  city: Joi.string().trim().required(),
  geoCoordinates: Joi.array().items(Joi.number()).length(2),
});

// any object to validate
const objectToValidate = {
  id: 1001,
};

// synchronously validation of an invalid Joi scheme
const testSync = () => {
  try {
    const validatedObject = joiSchemaValidationSync({
      schema: joiSchemaSample, 
      inputToValidate: objectToValidate 
    });
    console.log('validation result: ', validatedObject);
  } catch (error) {
    const { message, originalError } = error;
    console.error('error.message: ', message);
    console.error('error.originalError: ', prettyPrint(originalError));
  }
};

// asynchronous validation of an invalid Joi scheme
const testAsync = async () => {
  try {
    const validatedObject = await joiSchemaValidationAsync({
      schema: joiSchemaSample, 
      inputToValidate: objectToValidate 
    });
    console.log('validation result: ', validatedObject);
  } catch (error) {
    const { message, originalError } = error;
    console.error('error.message: ', message);
    console.error('error.originalError: ', prettyPrint(originalError));
  }
};

/*
 * --- In both cases (`sync` and `async`) the output of the console is the same ---
 * 
 * console output:
 * > error.message: "addressId" is required
 * > error.originalError: {
 *     "_original": {
 *       "id": 1001
 *     },
 *     "details": [
 *       {
 *         "message": "\"addressId\" is required",
 *         "path": [
 *           "addressId"
 *         ],
 *         "type": "any.required",
 *         "context": {
 *           "label": "addressId",
 *           "key": "addressId"
 *         }
 *       }
 *     ]
 *   }
 * 
 */
```

##### env-variables-validation

In this section, there is a function to validate the environment variables that are required using the function `validateProcessEnv`.

This function has a single parameter which is a list of required environment variable name strings where at least one element must be present, and returns an object with each environment variable name as properties.


```javascript
const { ProcessEnvValidationError, CustomBaseError } = require('@zxventures/utils/errors');
const { validateProcessEnv } = require('@zxventures/utils/validations/env-variables-validation');

/*
 * Existing environment variables:
 *    process.env.VAR_A = 'A';
 *    process.env.VAR_B = 'B';
 *    process.env.VAR_C = 'C';
 *
 */

// validate and obtain some existing variables
const envVariables = validateProcessEnv('VAR_A', 'VAR_B', 'VAR_C');
console.log('envVariables:', envVariables);

// try to validate any environment variable that does not exist.
try {
  const envVariables = validateProcessEnv('ANY_VAR_NOT_PRESENT');
  console.log('validation result: ', envVariables);
} catch (error) {
  const { message, variableName, name } = error;
  console.error('error', { message, variableName });
  console.error('is instance of Error:', error instanceof Error);
  console.error('is instance of CustomBaseError:', error instanceof CustomBaseError);
  console.error('is instance of ProcessEnvValidationError:', error instanceof ProcessEnvValidationError);
  console.log('error name:', name);
}

/*
 * console output:
 * > envVariables: { VAR_A: 'A', VAR_B: 'B', VAR_C: 'C' }
 * > error {
 *    message: 'environment variable not found',
 *    variableName: 'ANY_VAR_NOT_PRESENT'
 *  }
 * > is instance of Error: true
 * > is instance of CustomBaseError: true
 * > is instance of ProcessEnvValidationError: true
 * > error name: ProcessEnvValidationError
 * 
 */
```



## Run unit test and code coverage

<span style="color: orange;">**ADVICE:**</span> 
It is recommended to review and run the unit tests to better understand each of the utilities provided by this package.

Run

```bash
npm test
```

To run code coverage use

```bash
npm run coverage
```


## Package Dependencies

- [@ungap/structured-clone](https://www.npmjs.com/package/@ungap/structured-clone)
- [joi](https://www.npmjs.com/package/joi)



## Contributors

- [@al3xjaur-onetree](https://github.com/al3xjaur-onetree)
