huridocs/uwazi

View on GitHub
app/api/relationships.v2/services/DenormalizationService.ts

Summary

Maintainability
A
2 hrs
Test Coverage
A
100%
import { TransactionManager } from 'api/common.v2/contracts/TransactionManager';
import { EntitiesDataSource } from 'api/entities.v2/contracts/EntitiesDataSource';
import { SettingsDataSource } from 'api/settings.v2/contracts/SettingsDataSource';
import { TemplatesDataSource } from 'api/templates.v2/contracts/TemplatesDataSource';
import { RelationshipProperty } from 'api/templates.v2/model/RelationshipProperty';
import { Entity } from 'api/entities.v2/model/Entity';
import { RelationshipsDataSource } from '../contracts/RelationshipsDataSource';
import { MatchQueryNode } from '../model/MatchQueryNode';
import { RelationshipPropertyUpdateStrategy } from './propertyUpdateStrategies/RelationshipPropertyUpdateStrategy';

interface IndexEntitiesCallback {
  (sharedIds: string[]): Promise<void>;
}

export class DenormalizationService {
  private relationshipsDS: RelationshipsDataSource;

  private entitiesDS: EntitiesDataSource;

  private templatesDS: TemplatesDataSource;

  private settingsDS: SettingsDataSource;

  private transactionManager: TransactionManager;

  private indexEntities: IndexEntitiesCallback;

  private updateStrategy: RelationshipPropertyUpdateStrategy;

  constructor(
    relationshipsDS: RelationshipsDataSource,
    entitiesDS: EntitiesDataSource,
    templatesDS: TemplatesDataSource,
    settingsDS: SettingsDataSource,
    transactionManager: TransactionManager,
    indexEntitiesCallback: IndexEntitiesCallback,
    updateStrategy: RelationshipPropertyUpdateStrategy
  ) {
    this.relationshipsDS = relationshipsDS;
    this.entitiesDS = entitiesDS;
    this.templatesDS = templatesDS;
    this.settingsDS = settingsDS;
    this.transactionManager = transactionManager;
    this.indexEntities = indexEntitiesCallback;
    this.updateStrategy = updateStrategy;
  }

  private async getCandidateEntities(
    invertQueryCallback: (property: RelationshipProperty) => MatchQueryNode[],
    language: string,
    relatedEntities: Entity[] = []
  ) {
    const properties = await this.templatesDS.getAllRelationshipProperties().all();
    const entities: { sharedId: string; property: string }[] = [];
    await Promise.all(
      properties.map(async property => {
        relatedEntities.forEach(re => {
          if (property.template === re.template) {
            entities.push({
              sharedId: re.sharedId,
              property: property.name,
            });
          }
        });

        return Promise.all(
          invertQueryCallback(property).map(async query => {
            await this.relationshipsDS.getByQuery(query, language).forEach(async entity => {
              entities.push({
                sharedId: entity.sharedId,
                property: property.name,
              });
            });
          })
        );
      })
    );

    return entities;
  }

  private async getCandidateEntitiesForRelationship(_id: string, language: string) {
    const relationship = await this.relationshipsDS.getById([_id]).first();

    if (!relationship) throw new Error('missing relationship');

    const relatedEntities = await this.entitiesDS
      .getByIds([relationship.from.entity, relationship.to.entity])
      .all();

    return this.getCandidateEntities(
      property => property.buildQueryInvertedFromRelationship(relationship, relatedEntities),
      language,
      relatedEntities
    );
  }

  private async getCandidateEntitiesForEntity(sharedId: string, language: string) {
    const entity = await this.entitiesDS.getByIds([sharedId]).first();
    if (!entity) throw new Error('missing entity');
    return this.getCandidateEntities(
      property => property.buildQueryInvertedFromEntity(entity),
      language
    );
  }

  private async getCandidateEntitiesForTemplate(templateId: string, propertyNames: string[]) {
    const entities = await this.entitiesDS.getIdsByTemplate(templateId).all();
    return entities.map(entity => ({ sharedId: entity, properties: propertyNames }));
  }

  private async invalidateMetadataCacheForTemplate(templateIds: string[], propertyNames: string[]) {
    return this.entitiesDS.markMetadataAsChanged(
      templateIds.map(templateId => ({ template: templateId, properties: propertyNames }))
    );
  }

  private async runQueriesAndInvalidateMetadataCaches<Id>(
    ids: Id[],
    findCandidatesCallback: (id: Id) => Promise<
      (
        | {
            sharedId: string;
            property: string;
          }
        | {
            sharedId: string;
            properties: string[];
          }
      )[]
    >,
    invalidateMetadataCacheCallback?: (id: Id[]) => Promise<void>,
    updatePropertiesCallback?: (id: Id[]) => Promise<void>
  ) {
    const candidates = (await Promise.all(ids.map(async id => findCandidatesCallback(id)))).flat();

    if (invalidateMetadataCacheCallback) {
      await invalidateMetadataCacheCallback(ids);
    } else {
      await this.entitiesDS.markMetadataAsChanged(candidates);
    }

    this.transactionManager.onCommitted(async () => {
      if (updatePropertiesCallback) {
        await updatePropertiesCallback(ids);
      } else {
        await this.updateStrategy.update(candidates.map(c => c.sharedId));
      }
    });
  }

  private async updateDenormalizedMetadataDirectly(changedEntityIds: string[], language: string) {
    const relationshipProperties = await this.templatesDS.getAllRelationshipProperties().all();
    const relationshipPropertyNames = relationshipProperties.map(property => property.name);

    await this.entitiesDS.getByIds(changedEntityIds, language).forEach(async entity => {
      const newValuesForProperties = relationshipProperties.map(property => ({
        propertyName: property.name,
        ...(property.denormalizedProperty
          ? { value: entity.metadata[property.denormalizedProperty] }
          : {}),
      }));

      await this.entitiesDS.updateDenormalizedMetadataValues(
        entity.sharedId,
        language,
        entity.title,
        newValuesForProperties
      );
    });

    const affectedEntities = await this.entitiesDS
      .getByDenormalizedId(relationshipPropertyNames, changedEntityIds)
      .all();

    this.transactionManager.onCommitted(async () => {
      await this.indexEntities(affectedEntities);
    });
  }

  async denormalizeAfterCreatingRelationships(relationshipIds: string[]) {
    const defaultLanguage = await this.settingsDS.getDefaultLanguageKey();
    return this.runQueriesAndInvalidateMetadataCaches(relationshipIds, async id =>
      this.getCandidateEntitiesForRelationship(id, defaultLanguage)
    );
  }

  async denormalizeAfterUpdatingEntities(entityIds: string[], language: string) {
    return this.updateDenormalizedMetadataDirectly(entityIds, language);
  }

  async denormalizeBeforeDeletingFiles(fileIds: string[]) {
    const defaultLanguage = await this.settingsDS.getDefaultLanguageKey();
    const relationships = await this.relationshipsDS.getByFiles(fileIds).all();
    return this.runQueriesAndInvalidateMetadataCaches(
      relationships.map(r => r._id),
      async id => this.getCandidateEntitiesForRelationship(id, defaultLanguage)
    );
  }

  async denormalizeBeforeDeletingRelationships(relationshipIds: string[]) {
    const defaultLanguage = await this.settingsDS.getDefaultLanguageKey();
    return this.runQueriesAndInvalidateMetadataCaches(relationshipIds, async id =>
      this.getCandidateEntitiesForRelationship(id, defaultLanguage)
    );
  }

  async denormalizeAfterCreatingEntities(entityIds: string[], language: string) {
    return this.runQueriesAndInvalidateMetadataCaches(entityIds, async id =>
      this.getCandidateEntitiesForEntity(id, language)
    );
  }

  async denormalizeAfterCreatingOrUpdatingProperty(templateId: string, propertyNames: string[]) {
    return this.runQueriesAndInvalidateMetadataCaches(
      [templateId],
      async id => this.getCandidateEntitiesForTemplate(id, propertyNames),
      async () => this.invalidateMetadataCacheForTemplate([templateId], propertyNames),
      async () => this.updateStrategy.updateByTemplate(templateId)
    );
  }
}