my-zxventures-utils

1.0.10 • Public • Published

@zxventures/utils (icp-pkg-utils)

Table of Contents

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:

//registry.npmjs.org/:_authToken=PUT-THE-AUTHENTICATION-TOKEN-HERE

NOTE: The authentication token should be requested from the infrastructure team.

Using NPM:

npm install @zxventures/utils

Usage

This package has several common utilities. These utilities are grouped into 4 main groups: constants, errors, helpers, 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:

// 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:

// 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:

[
  // ...
  {
    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, GraphqlError JoiValidationError and 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).

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).

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.

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.

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, json and text.

hash

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

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 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 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() 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() documentation.

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.

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() documentation.

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

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.

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.

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 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.

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, regex-patterns, joi-schema-validation and 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.

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.

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 validations: joiSchemaValidationSync and joiSchemaValidationAsync.

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.

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

ADVICE: It is recommended to review and run the unit tests to better understand each of the utilities provided by this package.

Run

npm test

To run code coverage use

npm run coverage

Package Dependencies

Contributors

Package Sidebar

Install

npm i my-zxventures-utils

Weekly Downloads

3

Version

1.0.10

License

ISC

Unpacked Size

101 kB

Total Files

38

Last publish

Collaborators

  • al3x-onetree