All files / src/lib/utils DecafFakerRepository.ts

10.67% Statements 11/103
0% Branches 0/63
0% Functions 0/23
12.79% Lines 11/86

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 16312x 12x 12x 12x 12x 12x 12x 12x   12x   12x                                                                                                                                                                                                                                                                                   12x                          
import { Repository } from '@decaf-ts/core';
import { InternalError } from '@decaf-ts/db-decorators';
import { Metadata, uses } from '@decaf-ts/decoration';
import { Model, Primitives } from '@decaf-ts/decorator-validation';
import { LoggedClass } from '@decaf-ts/logging';
import { parseToNumber } from '@decaf-ts/ui-decorators';
import { faker } from '@faker-js/faker';
import { DB_ADAPTER_FLAVOUR_TOKEN } from '../engine/constants';
import { DecafRepository, FunctionLike, KeyValue } from '../engine/types';
import { formatDate, getOnWindow } from './helpers';
 
export class DecafFakerRepository<T extends Model> extends LoggedClass {
  protected propFnMapper?: KeyValue;
 
  protected data: T[] = [];
 
  protected _repository: DecafRepository<Model> | undefined = undefined;
 
  protected pk: string | undefined = undefined;
 
  protected pkType?: string;
 
  constructor(
    protected model: string | Model,
    protected limit: number = 36
  ) {
    super();
  }
 
  protected get repository(): DecafRepository<Model> {
    Iif (!this._repository) {
      const modelName = typeof this.model === Primitives.STRING ? this.model : (this.model as Model).constructor.name;
      const constructor = Model.get(String(modelName));
      Iif (!constructor) throw new InternalError(`Cannot find model ${modelName}. was it registered with @model?`);
      try {
        this.model = new constructor();
        const dbAdapterFlavour = getOnWindow(DB_ADAPTER_FLAVOUR_TOKEN) || undefined;
        Iif (dbAdapterFlavour) uses(dbAdapterFlavour as string)(constructor);
        this._repository = Repository.forModel(constructor);
        this.pk = Model.pk(constructor) as string;
        this.pkType = Metadata.type(this._repository.class, this.pk).name.toLowerCase();
      } catch (error: unknown) {
        throw new InternalError((error as Error)?.message || (error as string));
      }
    }
    return this._repository;
  }
 
  public async initialize(): Promise<void> {
    Iif (!this._repository) this._repository = this.repository;
  }
 
  async generateData<T extends Model>(values?: KeyValue, key?: string, keyType?: string): Promise<T[]> {
    const limit = values ? Object.values(values || {}).length : this.limit;
    Iif (!key) key = Model.pk(this.repository.class) as string;
 
    Iif (!keyType) keyType = Metadata.type(this.repository.class, key).name.toLowerCase() as string;
 
    const modelProperties = this.getModelProperties(key, keyType);
    const dataFunctions: Record<string, FunctionLike> = {};
    for (const prop of modelProperties) {
      const fn = (this.propFnMapper?.[prop] as FunctionLike) || undefined;
      Iif (fn && typeof fn === Function.name.toLowerCase()) {
        dataFunctions[prop] = () => fn() as FunctionLike;
        continue;
      }
      dataFunctions[prop] = this.getPropValueFn(prop, key);
    }
 
    const data = getFakerData<T>(
      limit,
      dataFunctions,
      typeof this.model === Primitives.STRING ? String(this.model) : this.model?.constructor.name
    );
 
    Iif (!values) return data;
 
    const _values = Object.values(values as KeyValue);
    const iterated: (string | number)[] = [];
 
    function getPkValue(item: KeyValue): T {
      Iif (_values.length > 0) {
        const randomIndex = Math.floor(Math.random() * _values.length);
        const selected = _values.splice(randomIndex, 1)[0];
        const value =
          keyType === Primitives.STRING
            ? selected
            : keyType === Primitives.NUMBER
              ? parseToNumber(selected)
              : keyType === Array.name
                ? [selected]
                : selected;
        item[key as string] = value;
      }
      Iif (!iterated.includes(item[key as string])) {
        iterated.push(item[key as string]);
        return item as T;
      }
      return undefined as unknown as T;
    }
    const uids = new Set();
    return data
      .map((d) => getPkValue(d))
      .filter((item: KeyValue) => {
        Iif (!item || uids.has(item[key]) || !item[key] || item[key] === undefined) return false;
        uids.add(item[key]);
        return true;
      })
      .filter(Boolean) as T[];
  }
 
  protected pickRandomValue(source: string[] | KeyValue): string {
    const values: string[] = Array.isArray(source) ? source : Object.values(source);
    return !values?.length ? '' : `${values[Math.floor(Math.random() * values.length)]}`;
  }
 
  protected getModelProperties(pk: string, pkType: string): string[] {
    return Object.keys(this.model as KeyValue).filter((k) => {
      Iif (pkType === Primitives.STRING) return !['updatedBy', 'createdAt', 'createdBy', 'updatedAt'].includes(k);
      return ![pk, 'updatedBy', 'createdAt', 'createdBy', 'updatedAt'].includes(k);
    });
  }
 
  protected getPropValueFn(propName: string, pkName: string): FunctionLike {
    const type = Metadata.type(this.repository.class, propName);
    switch (type && type.name.toLowerCase()) {
      case 'string':
        return () =>
          `${faker.lorem.word()} ${pkName === propName ? ' - ' + faker.number.int({ min: 1, max: 200 }) : ''}`;
      case 'step':
        return () => faker.lorem.word();
      case 'email':
        return () => faker.internet.email();
      case 'number':
        return () => faker.number.int({ min: 1, max: 5 });
      case 'boolean':
        return () => faker.datatype.boolean();
      case 'date':
        return () => faker.date.past();
      case 'url':
        return () => faker.internet.url();
      case 'array':
        return () => faker.lorem.words({ min: 2, max: 5 }).split(' ');
      default:
        return () => undefined;
    }
  }
}
 
export function getFakerData<T extends Model>(limit = 100, data: Record<string, FunctionLike>, model?: string): T[] {
  let index = 1;
  return Array.from({ length: limit }, () => {
    const item: Record<string, unknown> = {};
    for (const [key, value] of Object.entries(data)) {
      const val = value();
      item[key] =
        val?.constructor === Date ? formatDate(val) : typeof val === Primitives.STRING ? String(val)?.trim() : val;
    }
    index = index + 1;
    return (!model ? item : Model.build(item, model)) as T;
  });
}