huridocs/uwazi

View on GitHub
app/api/relationships.v2/model/TraversalQueryNode.ts

Summary

Maintainability
A
0 mins
Test Coverage
A
94%
import { Relationship } from 'api/relationships.v2/model/Relationship';
import _ from 'lodash';
import { MatchQueryNode, TemplateRecords } from './MatchQueryNode';
import { QueryNode } from './QueryNode';

interface TraversalFilters {
  _id?: string;
  types?: string[];
}

const inverseOfDirection = {
  in: 'out',
  out: 'in',
} as const;

// Temporal type definition
interface Entity {
  sharedId: string;
  template: string;
}

interface EntitiesMap {
  [sharedId: string]: Entity;
}

export class TraversalQueryNode extends QueryNode {
  private direction: 'in' | 'out';

  private filters: TraversalFilters;

  private parent?: MatchQueryNode;

  private matches: MatchQueryNode[] = [];

  constructor(direction: 'in' | 'out', filters?: TraversalFilters, matches?: MatchQueryNode[]) {
    super();
    this.direction = direction;
    this.filters = filters || {};
    matches?.forEach(match => this.addMatch(match));
  }

  protected getChildrenNodes(): QueryNode[] {
    return this.matches;
  }

  // eslint-disable-next-line class-methods-use-this
  getProjection() {
    return {
      type: 1,
    } as const;
  }

  getFilters() {
    return { ...this.filters };
  }

  getDirection() {
    return this.direction;
  }

  addMatch(match: MatchQueryNode) {
    this.matches.push(match);
    match.setParent(this);
  }

  getMatches() {
    return this.matches as readonly MatchQueryNode[];
  }

  setParent(parent: MatchQueryNode) {
    this.parent = parent;
  }

  getParent() {
    return this.parent;
  }

  getDepth(): number {
    if (!this.matches.length) {
      return 0;
    }

    return 1 + Math.max(...this.matches.map(match => match.getDepth()));
  }

  isSame(other: TraversalQueryNode): boolean {
    return (
      this.direction === other.direction &&
      _.isEqual(this.filters, other.filters) &&
      this.matches.length === other.matches.length &&
      this.matches.every((match, index) => match.isSame(other.matches[index]))
    );
  }

  chainsDecomposition(): TraversalQueryNode[] {
    if (!this.matches.length) {
      return [this.shallowClone()];
    }

    const decomposition: TraversalQueryNode[] = [];
    const childrenDecompositions = this.matches.map(match => match.chainsDecomposition());
    childrenDecompositions.forEach(childDecompositions => {
      childDecompositions.forEach(childDecomposition => {
        decomposition.push(this.shallowClone([childDecomposition]));
      });
    });
    return decomposition;
  }

  wouldTraverse(fromEntity: string, relationship: Relationship, toEntity: string) {
    let traverseDirection: 'in' | 'out';
    if (relationship.from.entity === fromEntity && relationship.to.entity === toEntity) {
      traverseDirection = 'out';
    } else if (relationship.to.entity === fromEntity && relationship.from.entity === toEntity) {
      traverseDirection = 'in';
    } else {
      return false;
    }
    return (
      (this.filters.types
        ? this.filters.types.includes(relationship.type)
        : true && this.filters.types) && this.direction === traverseDirection
    );
  }

  inverse(next: MatchQueryNode) {
    this.validateIsChain();
    const inversed = new TraversalQueryNode(
      inverseOfDirection[this.direction],
      {
        ...this.filters,
      },
      [next]
    );
    return this.matches[0].inverse(inversed);
  }

  private sortEntitiesInTraversalOrder(entityData: EntitiesMap, relationship: Relationship) {
    const relSideGivenDirection = {
      out: {
        first: 'from',
        second: 'to',
      } as const,
      in: {
        first: 'to',
        second: 'from',
      } as const,
    } as const;

    const first = entityData[relationship[relSideGivenDirection[this.direction].first].entity];
    const second = entityData[relationship[relSideGivenDirection[this.direction].second].entity];

    return [first, second];
  }

  reachesRelationship(
    relationship: Relationship,
    entityData: EntitiesMap
  ): MatchQueryNode | undefined {
    this.validateIsChain();

    const nextReaches = this.matches[0].reachesRelationship(relationship, entityData);
    if (nextReaches) {
      return this.parent!.shallowClone([this.shallowClone([nextReaches])]);
    }

    const [toMatchBeforeTraverse, toMatchAfterTraverse] = this.sortEntitiesInTraversalOrder(
      entityData,
      relationship
    );

    const matchesRelationship =
      this.parent!.wouldMatch(toMatchBeforeTraverse) &&
      this.wouldTraverse(
        toMatchBeforeTraverse.sharedId,
        relationship,
        toMatchAfterTraverse.sharedId
      ) &&
      this.matches[0].wouldMatch(toMatchAfterTraverse);

    if (matchesRelationship) {
      return MatchQueryNode.forEntity(toMatchBeforeTraverse.sharedId, [
        TraversalQueryNode.forRelationship(relationship._id, this.direction, [
          MatchQueryNode.forEntity(toMatchAfterTraverse.sharedId),
        ]),
      ]);
    }

    return undefined;
  }

  reachesEntity(entity: Entity) {
    this.validateIsChain();

    const nextReaches = this.matches[0].reachesEntity(entity);
    if (nextReaches) {
      return this.shallowClone([nextReaches]);
    }

    if (this.matches[0].wouldMatch(entity)) {
      return this.shallowClone([MatchQueryNode.forEntity(entity.sharedId)]);
    }

    return undefined;
  }

  shallowClone(matches?: MatchQueryNode[]) {
    return new TraversalQueryNode(this.direction, { ...this.filters }, matches ?? []);
  }

  getTemplates(
    path: number[] = [],
    _records: TemplateRecords | undefined = undefined
  ): TemplateRecords {
    const records = _records || [];
    this.matches.forEach((m, index) => m.getTemplates([...path, index], records));
    return records;
  }

  usesTemplate(templateId: string): boolean {
    return this.matches.some(match => match.usesTemplate(templateId));
  }

  usesType(typeId: string): boolean {
    return (
      this.filters.types?.includes(typeId) || this.matches.some(match => match.usesType(typeId))
    );
  }

  getRelationTypes(
    path: number[] = [],
    _records: TemplateRecords | undefined = undefined
  ): TemplateRecords {
    const records = _records || [];
    records.push({
      path,
      templates: this.filters.types || [],
    });
    this.matches.forEach((m, index) => m.getRelationTypes([...path, index], records));
    return records;
  }

  getTemplatesInLeaves(path: number[] = []): { path: number[]; templates: (string | 'ALL')[] }[] {
    return this.matches.map((m, index) => m.getTemplatesInLeaves([...path, index])).flat();
  }

  static forRelationship(
    relationship: Relationship['_id'],
    direction: 'in' | 'out',
    matches?: MatchQueryNode[]
  ) {
    return new TraversalQueryNode(direction, { _id: relationship }, matches);
  }
}