Asymmetrik/node-rest-starter

View on GitHub
src/app/common/util.service.ts

Summary

Maintainability
A
3 hrs
Test Coverage
import _ from 'lodash';
import { SortOrder, Types } from 'mongoose';
import platform from 'platform';

import { IdOrObject } from './typescript-util';

export const validateNonEmpty = function (property) {
    return null != property && property.length > 0;
};

export const toLowerCase = function (v: string | null) {
    return null != v ? v.toLowerCase() : undefined;
};

/**
 * Parse an input as a date. Handles various types
 * of inputs, such as Strings, Date objects, and Numbers.
 *
 * @param date The input representing a date / timestamp
 * @returns The timestamp in milliseconds since the Unix epoch
 */
export const dateParse = function (
    // eslint-disable-next-line @typescript-eslint/ban-types
    date: string | number | Date | Array<unknown> | Function | Object
) {
    // Handle nil values, arrays, and functions by simply returning null
    if (_.isNil(date) || _.isArray(date) || _.isFunction(date)) {
        return null;
    }

    // Date object should return its time in milliseconds
    if (_.isDate(date)) {
        return date.getTime();
    }

    // A number that exists will be interpreted as millisecond
    if (_.isFinite(date)) {
        return date;
    }

    // Handle number string
    if (!isNaN(date as number)) {
        return +date;
    }

    // Handle String, Object, etc.
    const parsed = Date.parse(date as string);

    // A string that cannot be parsed returns NaN
    if (isNaN(parsed)) {
        return null;
    }

    return parsed;
};

/**
 * Get the limit provided by the user, if there is one.
 * Limit has to be at least 1 and no more than 100, with
 * a default value of 20.
 *
 * @param queryParams
 * @param maxSize (optional) default: 100
 * @returns {number}
 */
export const getLimit = function (queryParams, maxSize = 100) {
    const limit = queryParams?.size ?? 20;
    return isNaN(limit) ? 20 : Math.max(1, Math.min(maxSize, Math.floor(limit)));
};

/**
 * Page needs to be positive and has no upper bound
 * @param queryParams
 * @returns {number}
 */
export const getPage = function (queryParams) {
    const page = queryParams?.page ?? 0;
    return isNaN(page) ? 0 : Math.max(0, page);
};

/**
 * Get the sort provided by the user, if there is one.
 * Limit has to be at least 1 and no more than 100, with
 * a default value of 20.
 *
 * @param queryParams
 * @param defaultDir (optional) default: ASC
 * @param defaultSort (optional)
 * @returns {Array}
 */
export const getSort = function (
    queryParams,
    defaultDir = 'ASC',
    defaultSort = undefined
) {
    const sort = queryParams?.sort ?? defaultSort;
    const dir = queryParams?.dir ?? defaultDir;
    if (!sort) {
        return null;
    }
    return [{ property: sort, direction: dir }];
};

/**
 * Get the sort provided by the user, if there is one.
 */
export const getSortObj = function (
    queryParams: { sort?: string; dir?: string | 1 | -1 },
    defaultDir: 'ASC' | 'DESC' = 'ASC',
    defaultSort?: string
): { [key: string]: SortOrder } | null {
    const sort = queryParams?.sort ?? defaultSort;
    const dir = queryParams?.dir ?? defaultDir;
    if (!sort) {
        return null;
    }

    return { [sort]: dir === 'ASC' ? 1 : -1 };
};

/**
 * Extract given field from request header
 */
export const getHeaderField = function (header, fieldName) {
    return header?.[fieldName] ?? null;
};

/**
 * Parses user agent information from request header
 */
export const getUserAgentFromHeader = function (header) {
    const userAgent = getHeaderField(header, 'user-agent');

    let data = {};
    if (null != userAgent) {
        const info = platform.parse(userAgent);
        data = {
            browser: `${info.name} ${info.version}`,
            os: info.os.toString()
        };
    }

    return data;
};

const isMongooseDateValue = (obj: unknown): obj is { $date: string } => {
    return typeof obj === 'object' && '$date' in obj;
};

const isMongooseObjValue = (obj: unknown): obj is { $obj: string } => {
    return typeof obj === 'object' && '$obj' in obj;
};

function propToMongoose(
    prop: unknown,
    nonMongoFunction: (prop: unknown) => unknown
) {
    if (isMongooseDateValue(prop)) {
        return new Date(prop.$date);
    }
    if (isMongooseObjValue(prop)) {
        return new Types.ObjectId(prop.$obj);
    }
    return nonMongoFunction(prop);
}

export const toMongoose = (obj: unknown) => {
    if (obj && typeof obj === 'object') {
        if (Array.isArray(obj)) {
            return obj.map((value) => propToMongoose(value, toMongoose));
        }
        return Object.keys(obj).reduce((newObj, key) => {
            newObj[key] = propToMongoose(obj[key], toMongoose);
            return newObj;
        }, {});
    }
    return obj;
};

/**
 * Determine if an array contains a given element by doing a deep comparison.
 * @param arr
 * @param element
 * @returns {boolean} True if the array contains the given element, false otherwise.
 */
export const contains = function (arr, element) {
    for (let i = 0; i < arr.length; i++) {
        if (_.isEqual(element, arr[i])) {
            return true;
        }
    }
    return false;
};

export const toProvenance = function (user) {
    const now = new Date();
    return {
        username: user.username,
        org: user.organization,
        created: now.getTime(),
        updated: now.getTime()
    };
};

export const emailMatcher = /.+@.+\..+/;

/**
 * @deprecated
 */
export const getPagingResults = (
    pageSize = 20,
    pageNumber = 0,
    totalSize = 0,
    elements = []
) => {
    if (totalSize === 0) {
        pageNumber = 0;
    }
    return {
        pageSize,
        pageNumber,
        totalSize,
        totalPages: Math.ceil(totalSize / pageSize),
        elements
    };
};

/**
 * Given an array of values, remove the values ending with a wildcard character (*)
 * @param stringArray - an array of string values
 * @return an array of the strings removed from the input list because they end with a '*' character
 */
export const removeStringsEndingWithWildcard = (stringArray: string[]) => {
    return _.remove(stringArray, (value) => {
        return _.endsWith(value, '*');
    });
};

/**
 * Escapes regex-specific characters in a given string
 */
export const escapeRegex = (str: string) => {
    return `${str}`.replace(/[.?*+^$[\]\\(){}|-]/g, '\\$&');
};

export const getId = <T>(obj: IdOrObject<T>) => {
    if (typeof obj === 'object' && '_id' in obj) {
        return obj._id;
    }
    return obj;
};