Source: transformer.js

/*
 * Copyright 2018-2022 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
'use strict'

/**
 * Transformers:
 * this module exports some useful generic functions for the transformation of objects.
 */

/**
 * Get a reference to cloudevent Validator class.
 * See {@link Validator}.
 * @private
 */
const V = require('./validator') // get validator from here

/**
 * Get the host name where this code is runninng.
 * @private
 */
const hostname = require('node:os').hostname()

/**
 * Get the process id (pid) where this code is runninng.
 * @private
 */
const pid = require('node:process').pid

/**
 * Useful Transformations for CloudEvent objects.
 *
 * Note that all methods here are static, so no need to instance this class;
 * see it as an Utility/Companion class.
 */
class Transformer {
  /**
   * Create a new instance of a Transformer object.
   * Note that instancing is not allowed for this class because all its methods are static.
   *
   * @throws {Error} instancing not allowed for this class
   * @hideconstructor
   */
  constructor () {
    throw new Error('Instancing not allowed for this class')
  }

  /**
   * Utility function that return a dump of the given object.
   *
   * @static
   * @param {(?object|Map|Set)} obj the object to dump
   * @param {string} [name='noname'] the name to assign in the returned string, or 'noname' as default value
   * @return {string} the dump of the object or a message when obj is undefined/null/not an object
   */
  static dumpObject (obj, name = 'noname') {
    if (V.isUndefined(obj)) {
      return `${name}:undefined`
    } else if (V.isNull(obj)) {
      return `${name}:null`
    } else if (V.isObject(obj) || V.isKeyedCollection(obj)) {
      return `${name}:${JSON.stringify(obj)}`
    } else {
      return `${name}:'${obj.toString()}'`
    }
  }

  /**
   * Utility function that parse a string representation
   * (compatible with the CloudEvent standard) of the given timestamp (Date)
   * and returns it (if possible).
   *
   * Note that the value returned could be adjusted with the current timezone offset.
   *
   * @static
   * @param {!string} obj the timestamp/date to parse (as a string)
   * @param {number} [timezoneOffset=0] a timezone offset to add (in msec, default 0)
   * @return {object} the parsed version, as a timestamp (Date) object, if possible
   * @throws {TypeError} if obj is undefined or null, or is not a string
   */
  static timestampFromString (obj, timezoneOffset = 0) {
    if (!V.isStringNotEmpty(obj)) {
      throw new TypeError(`Missing or wrong timestamp: '${obj}' must be a string and not a: '${typeof obj}'.`)
    }
    const timestampMsec = Date.parse(obj)
    return new Date(timestampMsec + timezoneOffset)
  }

  /**
   * Utility function that return a string representation
   * (compatible with the CloudEvent standard)
   * of the given timestamp (Date), or the current one will be used.
   *
   * Note that the value returned is in the UTC format.
   *
   * @static
   * @param {?object} obj the timestamp/date to convert, or the current one if not defined or null
   * @return {string} the string representation of the object
   * @throws {TypeError} if obj is undefined or null, or is not a Date instance
   */
  static timestampToString (obj) {
    let timestamp = obj
    if (V.isUndefinedOrNull(timestamp)) {
      timestamp = new Date()
    }
    if (!V.isDateValid(timestamp)) {
      throw new TypeError(`Missing or wrong timestamp: '${timestamp}' must be a date and not a: '${typeof timestamp}'.`)
    }
    return timestamp.toISOString()
  }

  /**
   * Utility function that parse a number representation
   * of the given timestamp (Date)
   * and returns it (if possible).
   *
   * Note that the value returned could be adjusted with the current timezone offset.
   *
   * @static
   * @param {!number} obj the timestamp/date to parse (as a number)
   * @param {number} [timezoneOffset=0] a timezone offset to add (in msec, default 0)
   * @return {object} the parsed version, as a timestamp (Date) object, if possible
   * @throws {TypeError} if obj is undefined or null, or is not a number
   */
  static timestampFromNumber (obj, timezoneOffset = 0) {
    if (!V.isNumber(obj)) {
      throw new TypeError(`Missing or wrong timestamp: '${obj}' must be a number and not a: '${typeof obj}'.`)
    }
    return new Date(obj + timezoneOffset)
  }

  /**
   * Utility function that return a number representation
   * of the given timestamp (Date), or the current one will be used.
   *
   * Note that the value returned is in the UTC format.
   *
   * @static
   * @param {?object} obj the timestamp/date to convert, or the current one if not defined or null
   * @return {number} the number representation of the object
   * @throws {TypeError} if obj is not a Date instance
   */
  static timestampToNumber (obj) {
    let timestamp = obj
    if (V.isUndefinedOrNull(timestamp)) {
      timestamp = new Date()
    }
    if (!V.isDateValid(timestamp)) {
      throw new TypeError(`Wrong timestamp: '${timestamp}' must be a date and not a: '${typeof timestamp}'.`)
    }
    return timestamp.getTime()
  }

  /**
   * Utility function that encodes a string
   * in base64 (compatible with the CloudEvent standard).
   *
   * @static
   * @param {?string} obj the string to encode, or '' if not defined or null
   * @return {string} the string encoded in base64
   * @throws {TypeError} if str is not a string instance
   */
  static stringToBase64 (obj) {
    let str = obj
    if (V.isUndefinedOrNull(obj)) {
      str = ''
    }
    if (!V.isString(str)) {
      throw new TypeError(`Missing or wrong argument: '${str}' must be a string and not a: '${typeof str}'.`)
    }
    return Buffer.from(str).toString('base64')
  }

  /**
   * Utility function that decodes a base64 string
   * into a normal string using 'utf8' encoding
   * (compatible with the CloudEvent standard).
   *
   * @static
   * @param {?string} obj the string to decode, or '' if not defined or null
   * @return {string} the string decoded from base64
   * @throws {TypeError} if str is not a string instance
   */
  static stringFromBase64 (obj) {
    let str = obj
    if (V.isUndefinedOrNull(obj)) {
      str = ''
    }
    if (!V.isString(str)) {
      throw new TypeError(`Missing or wrong argument: '${str}' must be a string and not a: '${typeof str}'.`)
    }
    return Buffer.from(str, 'base64').toString('utf8')
  }

  /**
   * Utility function that map an Error into an object
   * (compatible with the CloudEvent standard), to fill its 'data' attribute.
   *
   * @static
   * @param {!Error} err the Error to transform
   * @param {object} [options={}] transformation options:
   *        - includeStackTrace flag (default false) to add the StackTrace into the object to return,
   *        - addStatus flag (default true) to add a 'status' attribute into the object to return,
   *        - addTimestamp flag (default false) to add the current 'timestamp' as attribute into the object to return,
   * @return {object} the object representation of the error
   * @throws {TypeError} if err is undefined or null, or is not an Error instance
   */
  static errorToData (err, {
    includeStackTrace = false,
    addStatus = true,
    addTimestamp = false
  } = {}
  ) {
    if (!V.isError(err)) {
      throw new TypeError(`Missing or wrong argument: '${err}' must be an Error and not a: '${typeof err}'.`)
    }
    const data = {
      name: err.name,
      message: err.message,
      stack: (includeStackTrace === true) ? err.stack : null
    }
    if (V.isDefinedAndNotNull(err.code)) {
      data.code = err.code
    }
    if (addStatus === true) {
      data.status = 'error'
    }
    if (addTimestamp === true) {
      data.timestamp = Date.now()
    }
    return data
  }

  /**
   * Utility function that get some process-related info and wrap into an object
   * (compatible with the CloudEvent standard), to fill its 'data' attribute.
   *
   * @static
   * @return {object} the object representation of process-related info data
   */
  static processInfoToData () {
    return {
      hostname,
      pid
    }
  }

  /**
   * Utility function that strip all arguments (if any) from the given URI/URL string
   * and returns the string without them.
   *
   * @static
   * @param {!string} url the URI/URL (as a string)
   * @param {object} [options={}] containing: strict (boolean, default false) to check it in a more strict way
   * @return {string} the parsed version, but without arguments (if any)
   * @throws {TypeError} if url is undefined or null, or is not a string
   */
  static uriStripArguments (url, { strict = false } = {}) {
    if (!V.isString(url)) {
      throw new TypeError(`Missing or wrong URL: '${url}' must be a string and not a: '${typeof url}'.`)
    }
    if (strict === true) {
      if (!V.isURI(url)) {
        throw new TypeError(`Missing or wrong URL: '${url}'`)
      }
    }
    return url.split('?')[0]
  }

  /**
   * Utility function that merge the given objects (at least one base and another)
   * and returns the new one (but with the prototype of the first).
   *
   * @static
   * @param {!object} base the first object to merge
   * @param {object} others all other(s) object to merge (at least one)
   * @return {object} the new object
   * @throws {TypeError} if base is undefined or null, or is not an object
   */
  static mergeObjects (base, ...others) {
    if (!V.isObject(base)) {
      throw new TypeError(`Missing or wrong argument: '${base}' must be an object and not a: '${typeof base}'.`)
    }
    const baseProto = Object.getPrototypeOf(base)
    return Object.assign(Object.create(baseProto), base, ...others) // set the prototype of the first argument in the clone
  }
}

/**
 * Utility variable that returns the timezone offset value, in msec.
 *
 * @readonly
 * @type {number}
 * @static
 */
Transformer.timezoneOffsetMsec = new Date().getTimezoneOffset() * 60 * 1000

module.exports = Transformer