huridocs/uwazi

View on GitHub
app/api/templates.v2/services/CreateTemplateService.ts

Summary

Maintainability
B
5 hrs
Test Coverage
A
100%
import { ValidationError } from 'api/common.v2/validation/ValidationError';
import { EntitiesDataSource } from 'api/entities.v2/contracts/EntitiesDataSource';
import { propertyTypes } from 'shared/propertyTypes';
import { MatchQueryNode, TemplateRecordElement } from 'api/relationships.v2/model/MatchQueryNode';
import { RelationshipTypesDataSource } from 'api/relationshiptypes.v2/contracts/RelationshipTypesDataSource';
import { RelationshipPropertyData } from 'shared/types/api.v2/templates.createTemplateRequest';
import { createError } from 'api/utils';
import { DenormalizationService } from 'api/relationships.v2/services/DenormalizationService';
import { TransactionManager } from 'api/common.v2/contracts/TransactionManager';
import { TemplatesDataSource } from '../contracts/TemplatesDataSource';
import { QueryMapper } from '../database/QueryMapper';
import { TemplateInput, TemplateInputMappers } from './TemplateInputMappers';
import { RelationshipProperty } from '../model/RelationshipProperty';

interface MatchQuery {
  templates: string[];
  traverse?: TraverseQuery[];
}

interface TraverseQuery {
  direction: 'in' | 'out';
  types: string[];
  match: MatchQuery[];
}

const expandAllTemplates = (allTemplatesIds: string[]) => (record: TemplateRecordElement) =>
  record.templates.length ? record : { ...record, templates: allTemplatesIds };

const validateTemplateExistence = (
  query: MatchQueryNode,
  errors: ValidationError['errors'],
  allTemplatesIds: string[]
) => {
  const templateIdSet = new Set(allTemplatesIds);

  const allQueryTemplates = query.getTemplates().map(expandAllTemplates(allTemplatesIds));
  allQueryTemplates.forEach(record => {
    const nonExisting = record.templates.filter(template => !templateIdSet.has(template));
    if (nonExisting.length) {
      errors.push({
        path: `/query/${record.path.join('/')}/templates`,
        message: `Templates ${nonExisting.join(', ')} do not exist.`,
      });
    }
  });
};

const validateRelTypeExistence = async (
  query: MatchQueryNode,
  errors: ValidationError['errors'],
  relTypeDS: RelationshipTypesDataSource
) => {
  const relTypeIds = await relTypeDS.getRelationshipTypeIds();
  const relTypeIdSet = new Set(relTypeIds);
  const allQueryRelationTypes = query.getRelationTypes().map(expandAllTemplates(relTypeIds));
  allQueryRelationTypes.forEach(record => {
    const nonExisting = record.templates.filter(template => !relTypeIdSet.has(template));
    if (nonExisting.length) {
      errors.push({
        path: `/query/${record.path.join('/')}/types`,
        message: `Relation types ${nonExisting.join(', ')} do not exist.`,
      });
    }
  });
};

const validateTemplatesInLeaves = async (
  query: MatchQueryNode,
  errors: ValidationError['errors'],
  denormalizedProperty: string,
  allTemplatesIds: string[],
  templatesDS: TemplatesDataSource
) => {
  const templatesInLeaves = query.getTemplatesInLeaves().map(expandAllTemplates(allTemplatesIds));

  const templatesHavingProperty = new Set(
    await templatesDS.getTemplatesIdsHavingProperty(denormalizedProperty).all()
  );

  templatesInLeaves.forEach(record => {
    if (!record.templates.every(template => templatesHavingProperty.has(template))) {
      errors.push({
        path: `/query/${record.path.join('/')}/templates`,
        message: 'template does not have the required property',
      });
    }
  });
};

const flushErrors = (errors: ValidationError['errors']) => {
  if (errors.length) {
    throw new ValidationError(errors);
  }
};

export class CreateTemplateService {
  private templatesDataSource: TemplatesDataSource;

  private relTypesDataSource: RelationshipTypesDataSource;

  private entitiesDataSource: EntitiesDataSource;

  private denormalizationService: DenormalizationService;

  private transactionManager: TransactionManager;

  constructor(
    templatesDataSource: TemplatesDataSource,
    relTypesDataSource: RelationshipTypesDataSource,
    entitiesDataSource: EntitiesDataSource,
    denormalizationService: DenormalizationService,
    transactionManager: TransactionManager
  ) {
    this.templatesDataSource = templatesDataSource;
    this.relTypesDataSource = relTypesDataSource;
    this.entitiesDataSource = entitiesDataSource;
    this.denormalizationService = denormalizationService;
    this.transactionManager = transactionManager;
  }

  private async validateQuery(query: MatchQueryNode, denormalizedProperty?: string) {
    const errors: ValidationError['errors'] = [];

    const allTemplatesIds = await this.templatesDataSource.getAllTemplatesIds().all();
    validateTemplateExistence(query, errors, allTemplatesIds);
    await validateRelTypeExistence(query, errors, this.relTypesDataSource);
    flushErrors(errors);

    if (!denormalizedProperty) {
      return;
    }

    await validateTemplatesInLeaves(
      query,
      errors,
      denormalizedProperty,
      allTemplatesIds,
      this.templatesDataSource
    );

    flushErrors(errors);
  }

  async createRelationshipProperty(property: RelationshipPropertyData) {
    const query = TemplateInputMappers.queryToApp(property.query);
    await this.validateQuery(query, property.denormalizedProperty);
    return {
      ...property,
      query: QueryMapper.toDBO(query.getTraversals()),
      targetTemplates: query.determinesRelationships(),
    };
  }

  async validateUpdateActions(_oldTemplate: TemplateInput, _newTemplate: TemplateInput) {
    const oldTemplate = TemplateInputMappers.toApp(_oldTemplate);
    const newTemplate = TemplateInputMappers.toApp(_newTemplate);

    const updatedProperties = oldTemplate
      .selectUpdatedProperties(newTemplate)
      .filter(info => info.oldProperty.type === propertyTypes.newRelationship);

    const updatesDenormalizedProperty = updatedProperties.filter(info =>
      info.updatedAttributes.includes('denormalizedProperty')
    );

    if (updatesDenormalizedProperty.length) {
      throw createError(
        `Cannot update denormalized property of a relationship property. The following properties try to do so: ${updatesDenormalizedProperty
          .map(info => info.newProperty.name)
          .join(', ')}`,
        400
      );
    }
  }

  async handleRelationshipPropertyUpdates(
    _oldTemplate: TemplateInput,
    _newTemplate: TemplateInput
  ) {
    await this.transactionManager.run(async () => {
      const oldTemplate = TemplateInputMappers.toApp(_oldTemplate);
      const newTemplate = TemplateInputMappers.toApp(_newTemplate);

      const newRelationshipNames = oldTemplate
        .selectNewProperties(newTemplate)
        .filter(p => p instanceof RelationshipProperty)
        .map(p => p.name);
      const updatedProperties = oldTemplate
        .selectUpdatedProperties(newTemplate)
        .filter(info => info.oldProperty.type === propertyTypes.newRelationship);

      const updatesQuery = updatedProperties
        .filter(info => info.updatedAttributes.includes('query'))
        .map(info => info.newProperty.name);

      const uniquePropertyNames = Array.from(new Set([...newRelationshipNames, ...updatesQuery]));

      await this.denormalizationService.denormalizeAfterCreatingOrUpdatingProperty(
        newTemplate.id,
        uniquePropertyNames
      );
    });
  }

  async templateIsUsedInQueries(templateId: string) {
    const relProps = this.templatesDataSource.getAllRelationshipProperties();
    return relProps.some(property => property.queryUsesTemplate(templateId));
  }

  async relationTypeIsUsedInQueries(typeId: string) {
    const relProps = this.templatesDataSource.getAllRelationshipProperties();
    return relProps.some(property => property.queryUsesRelationType(typeId));
  }
}