ps73/feathers-prisma

View on GitHub
src/utils.ts

Summary

Maintainability
B
4 hrs
Test Coverage
import { NotFound } from '@feathersjs/errors';
import { NullableId } from '@feathersjs/feathers';
import { OPERATORS_MAP } from './constants';
import { EagerQuery, IdField, QueryParam, QueryParamRecordFilters } from './types';

export const castToNumberBooleanStringOrNull = (value: string | boolean | number) => {
  const isNumber = typeof value === 'number';
  const isBoolean = value === 'true' || value === 'false';
  if (isBoolean || typeof value === 'boolean') {
    return typeof value === 'string' ? value === 'true' : value;
  }
  if (isNumber) {
    return value;
  }
  if (value === 'null') {
    return null;
  }
  return value;
};

export const castFeathersQueryToPrismaFilters = (p: QueryParamRecordFilters, whitelist: string[]) => {
  const filters: Record<string, any> = {};
  Object.keys(p).forEach((k: string) => {
    const key = (k as keyof typeof OPERATORS_MAP);
    const prismaKey = OPERATORS_MAP[key];
    if (prismaKey && key !== '$eager' && whitelist.includes(key)) {
      const value = p[key];
      if (Array.isArray(value)) {
        filters[prismaKey] = value.map((v) => castToNumberBooleanStringOrNull(v));
      } else if (key === '$rawWhere' && typeof value === 'object' || key === '$prisma' && typeof value === 'object') {
        Object.keys(value).forEach((rawKey) => {
          filters[rawKey] = value[rawKey];
        });
      } else if (value !== undefined && typeof value !== 'object') {
        filters[prismaKey] = castToNumberBooleanStringOrNull(value);
      }
    }
  });
  return filters;
};

export const castEagerQueryToPrismaInclude = (value: EagerQuery, whitelist: string[], idField: string) => {
  const include: Record<string, any> = {};
  if (Array.isArray(value)) {
    value.forEach((v) => {
      if (Array.isArray(v) && typeof v[0] === 'string' && v.length > 1) {
        const [key, ...includes] = v;
        const subinclude = castEagerQueryToPrismaInclude(includes, whitelist, idField);
        include[key] = {
          include: subinclude,
        };
      } else if (Array.isArray(v) && typeof v[0] === 'string' && v.length === 1) {
        const [key] = v;
        include[key] = true;
      } else if (typeof v[0] !== 'string') {
        throw {
          code: 'FP1001',
          message: 'First Array Item in a sub-array must be a string!',
        };
      } else if (typeof v === 'string') {
        include[v] = true;
      }
    });
  } else {
    Object.keys(value).forEach((key) => {
      const val = value[key];
      if (typeof val === 'boolean') {
        include[key] = val;
      } else if (Array.isArray(val)) {
        include[key] = {
          select: {
            [idField]: true,
            ...buildSelect(val),
          },
        };
      }
    });
  }

  return include;
};

export const mergeFiltersWithSameKey = (
  where: Record<string, any>,
  key: string,
  filter: Record<string, any> | string | number | boolean | null,
): Record<string, any> | string | number | boolean => {
  const current = where[key];
  if (typeof filter === 'object') {
    const currentIsObj = typeof current === 'object';
    return {
      ...(currentIsObj ? current : {}),
      ...filter,
      ...(!currentIsObj && current ? { equals: current } : {})
    };
  }
  return filter;
};

export const buildWhereAndInclude = (query: QueryParam, whitelist: string[], idField: string) => {
  const where: Record<string, any> = {};
  let include: Record<string, any> = {};
  Object.keys(query).forEach((k: string | '$or' | '$and') => {
    const value = query[k];
    if (k === '$or' && Array.isArray(value)) {
      where.OR = value.map((v) => buildWhereAndInclude(v, whitelist, idField).where);
    } else if (k === '$and' && Array.isArray(value)) {
      value.forEach((v) => {
        const whereValue = buildWhereAndInclude(v, whitelist, idField).where;
        Object.keys(whereValue).map((subKey) => {
          where[subKey] = mergeFiltersWithSameKey(where, subKey, whereValue[subKey]);
        });
      });
    } else if (k !== '$eager' && typeof value === 'object' && !Array.isArray(value)) {
      where[k] = mergeFiltersWithSameKey(where, k, castFeathersQueryToPrismaFilters(value, whitelist));
    } else if (k !== '$eager' && typeof value !== 'object' && !Array.isArray(value)) {
      where[k] = castToNumberBooleanStringOrNull(value);
    } else if (k === '$eager' && whitelist.includes(k)) {
      const eager = value as EagerQuery;
      include = castEagerQueryToPrismaInclude(eager, whitelist, idField);
    }
  });
  return { where, include };
};

export const buildSelect = ($select: string[]) => {
  const select: Record<string, boolean> = {};
  $select.forEach((f: string) => { select[f] = true; });
  return select;
};

export const buildOrderBy = ($sort: Record<string, any>) => {
  return Object.keys($sort).map((k) => ({ [k]: $sort[k] === 1 ? 'asc' : 'desc' }));
};

export const buildPagination = ($skip: number, $limit: number) => {
  return {
    skip: $skip || 0,
    take: $limit,
  };
};

export const buildPrismaQueryParams = (
  { id, query, filters, whitelist }: {
    id?: IdField,
    query: Record<string, any>,
    filters: Record<string, any>,
    whitelist: string[],
  },
  idField: string,
) => {
  let select = buildSelect(filters.$select || []);
  const selectExists = Object.keys(select).length > 0;
  const { where, include } = buildWhereAndInclude(id ? { [idField]: id, ...query } : query, whitelist, idField);
  const includeExists = Object.keys(include).length > 0;
  const orderBy = buildOrderBy(filters.$sort || {});
  const { skip, take } = buildPagination(filters.$skip, filters.$limit);
  const queryWhereExists = Object.keys(where).filter((k) => k !== idField).length > 0;
  const idQueryIsObject = typeof where.id === 'object';

  if (selectExists) {
    select = {
      [idField]: true,
      ...select,
      ...include,
    };

    return {
      skip,
      take,
      orderBy,
      where,
      select,
      _helper: {
        queryWhereExists,
        idQueryIsObject
      },
    };
  }

  if (!selectExists && includeExists) {
    return {
      skip,
      take,
      orderBy,
      where,
      include,
      _helper: {
        queryWhereExists,
        idQueryIsObject
      },
    };
  }

  return {
    skip,
    take,
    orderBy,
    where,
    _helper: {
      queryWhereExists,
      idQueryIsObject
    },
  };
};

export const buildSelectOrInclude = (
  { select, include }: { select?: Record<string, boolean>; include?: Record<string, any> },
) => {
  return select ? { select } : include ? { include } : {};
};

export const checkIdInQuery = (
  {
    id,
    query,
    idField,
    allowOneOf,
  }: {
    id: IdField | null;
    query: Record<string, any>;
    idField: string;
    allowOneOf?: boolean;
  }
) => {
  if ((allowOneOf && id && Object.keys(query).length > 0) || (id && query[idField] && id !== query[idField])) {
    throw new NotFound(`No record found for ${idField} '${id}' and query.${idField} '${id}'`);
  }
};

export const buildWhereWithOptionalIdObject = (id: NullableId, where: Record<string, any>, idField: string) => {
  if (typeof where.id === 'object') {
    return {
      ...where,
      [idField]: {
        ...where[idField],
        equals: id,
      },
    };
  }
  return where;
};