huridocs/uwazi

View on GitHub
app/api/search/metadataAggregations.js

Summary

Maintainability
A
0 mins
Test Coverage
A
97%
import { preloadOptionsSearch } from 'shared/config';
import { permissionsContext } from 'api/permissions/permissionsContext';
import commonProperties from 'shared/commonProperties';

const aggregation = (key, should, filters, nestedAggregationName, nestedAggregation) => {
  const agg = {
    terms: {
      field: key,
      missing: 'missing',
      size: preloadOptionsSearch(),
    },
    aggregations: {
      filtered: {
        filter: {
          bool: {
            should,
            filter: filters,
          },
        },
      },
    },
  };

  if (nestedAggregationName && nestedAggregation) {
    agg.aggregations[nestedAggregationName] = nestedAggregation;
  }

  return agg;
};

const nestedMatcherIsAggregationProperty = (nestedMatcher, nestedPropPath) =>
  !nestedMatcher.nested ||
  !nestedMatcher.nested.query.bool.must ||
  !nestedMatcher.nested.query.bool.must[0].terms ||
  !nestedMatcher.nested.query.bool.must[0].terms[nestedPropPath] ||
  !nestedMatcher.nested.query.bool.must_not ||
  !nestedMatcher.nested.query.bool.must_not[0].exists ||
  !nestedMatcher.nested.query.bool.must[0].exists.field[nestedPropPath];

const nestedAggregation = (property, should, readOnlyFilters, path, missing = false) => {
  const nestedPath = path || `metadata.${property.name}`;
  const agg = {
    nested: {
      path: nestedPath,
    },
    aggregations: {},
  };
  let nestedFilters = readOnlyFilters
    .filter(match => match.nested && match.nested.path === nestedPath)
    .map(nestedFilter => nestedFilter.nested.query.bool.must)
    .reduce((result, propFilters) => result.concat(propFilters), []);

  property.nestedProperties.forEach(prop => {
    const nestedPropPath = path
      ? `${path}.metadata.${prop}.raw`
      : `metadata.${property.name}.${prop}.raw`;
    const filters = readOnlyFilters
      .map(match => {
        if (match.bool && match.bool.must && match.bool.must[0] && match.bool.must[0].nested) {
          match.bool.must = match.bool.must.filter(nestedMatcher =>
            nestedMatcherIsAggregationProperty(nestedMatcher, nestedPropPath)
          );

          if (!match.bool.must.length) {
            return;
          }
        }
        if (match.nested) {
          return;
        }
        return match;
      })
      .filter(f => f);

    nestedFilters = nestedFilters.filter(filter => !filter.terms || !filter.terms[nestedPropPath]);

    agg.aggregations[prop] = {
      terms: {
        field: nestedPropPath,
        missing: missing ? 'missing' : undefined,
        size: preloadOptionsSearch(),
      },
      aggregations: {
        filtered: {
          filter: {
            bool: {
              must: nestedFilters,
            },
          },
          aggregations: {
            total: {
              reverse_nested: {},
              aggregations: {
                filtered: {
                  filter: {
                    bool: {
                      should,
                      must: filters,
                    },
                  },
                },
              },
            },
          },
        },
      },
    };
  });

  return agg;
};

const extractFilters = (baseQuery, path) => {
  let filters = baseQuery.query.bool.filter.filter(
    match =>
      match &&
      (!match.terms || (match.terms && !match.terms[path])) &&
      (!match.bool ||
        !match.bool.should ||
        !match.bool.should[1] ||
        !match.bool.should[1].terms ||
        !match.bool.should[1].terms[path])
  );
  filters = filters.concat(baseQuery.query.bool.must);
  return filters;
};

const getpath = (property, suggested) =>
  suggested ? `suggestedMetadata.${property.name}` : `metadata.${property.name}`;

const getSelectParentPath = path => {
  const parentPathSplit = path.split('.');
  parentPathSplit[parentPathSplit.length - 1] = 'parent';
  parentPathSplit.push('value');
  const parentPath = parentPathSplit.join('.');
  return parentPath;
};

const selectAggregation = (path, should, filters) => {
  const parentPath = getSelectParentPath(path);
  return {
    filter: { match_all: {} },
    aggregations: {
      self: aggregation(path, should, filters),
      parent: aggregation(parentPath, should, filters),
    },
  };
};

export const propertyToAggregation = (property, baseQuery, suggested = false) => {
  const path = getpath(property, suggested);
  const filters = extractFilters(baseQuery, path);
  const { should } = baseQuery.query.bool;

  if (property.type === 'nested') {
    return nestedAggregation(property, should, filters);
  }

  if (commonProperties.isOrInheritsSelect(property)) {
    return selectAggregation(path, should, filters);
  }

  return aggregation(path, should, filters);
};

export const generatedTocAggregations = baseQuery => {
  const path = 'generatedToc';
  const filters = extractFilters(baseQuery, path);
  const { should } = baseQuery.query.bool;
  return aggregation(path, should, filters);
};

const permissionsAggregations = (baseQuery, path, terms) => {
  const filters = extractFilters(baseQuery, path);
  const { should } = baseQuery.query.bool;

  const baseFilters = filters.filter(
    f =>
      !(
        (f.nested && f.nested.path === 'permissions') ||
        f?.bool?.should?.find(i => i?.nested?.path === 'permissions')
      )
  );

  return {
    filter: {
      bool: {
        should,
        filter: baseFilters,
      },
    },
    aggregations: {
      nestedPermissions: {
        nested: { path: 'permissions' },
        aggregations: {
          filtered: {
            terms: {
              field: path,
              size: preloadOptionsSearch(),
            },
            aggregations: {
              filteredByUser: {
                filter: {
                  bool: {
                    filter: [
                      {
                        terms,
                      },
                    ],
                  },
                },
                aggregations: {
                  uniqueEntities: {
                    reverse_nested: {},
                  },
                },
              },
            },
          },
        },
      },
    },
  };
};

export const permissionsLevelAgreggations = baseQuery =>
  permissionsAggregations(baseQuery, 'permissions.level', {
    'permissions.refId': permissionsContext.permissionsRefIds(),
  });

export const permissionsUsersAgreggations = (baseQuery, level) =>
  permissionsAggregations(baseQuery, 'permissions.refId', {
    'permissions.level': [level],
  });

export const publishingStatusAgreggations = baseQuery => {
  const path = 'published';
  const filters = extractFilters(baseQuery, path);
  const { should } = baseQuery.query.bool;
  const user = permissionsContext.getUserInContext();
  const needsPermissions = user && !['admin', 'editor'].includes(user.role);

  const baseFilters = filters.filter(
    f => !((f?.bool?.must || f?.bool?.should)?.[0]?.term?.published !== undefined)
  );

  if (needsPermissions) {
    baseFilters.push({
      bool: {
        should: [
          {
            term: {
              published: true,
            },
          },
          {
            bool: {
              must: [
                {
                  term: {
                    published: false,
                  },
                },
                {
                  nested: {
                    path: 'permissions',
                    query: {
                      bool: {
                        must: [
                          {
                            terms: {
                              'permissions.refId': permissionsContext.permissionsRefIds(),
                            },
                          },
                        ],
                      },
                    },
                  },
                },
              ],
            },
          },
        ],
      },
    });
  }

  return {
    filter: {
      bool: {
        should,
        filter: baseFilters,
      },
    },
    aggregations: {
      filtered: {
        terms: {
          field: path,
          missing: 'false',
          size: preloadOptionsSearch(),
        },
      },
    },
  };
};