Woodshop for old agent generation
Try the new agent generation
  • What is woodshop
  • How to's
    • Smart Relationship
      • GetIdsFromRequest
    • Smart views
      • Display a calendar view
      • Create a custom tinder-like validation view
      • Create a custom moderation view
      • Create a dynamic calendar view for an event-booking use case
    • Configure environment variables
      • NodeJS/Express projects
    • Elasticsearch Integration
      • Interact with your Elasticsearch data
      • Elasticsearch service/utils
      • Another example
    • Zendesk Integration
      • Authentication, Filtering & Sorting
      • Display Zendesk tickets
      • Display Zendesk users
      • View tickets related to a user
      • Bonus: Direct link to Zendesk + change priority of a ticket
    • Dwolla integration
      • Display Dwolla customers
      • Display Dwolla funding sources
      • Display Dwolla transfers
      • Link users and Dwolla customers
      • Dwolla service
    • Make filters case insensitive
    • Use Azure Table Storage
    • Create multiple line charts
    • Create Charts with AWS Redshift
    • View soft-deleted records
    • Send Smart Action notifications to Slack
    • Authenticate a Forest Admin API against an OAuth protected API Backend
    • Translate your project into TypeScript
      • V8
        • Migrate Mongoose files
        • Migrate Sequelize files
      • v7
        • Migrate Mongoose files
        • Migrate Sequelize files
      • v6
    • Geocode an address with Algolia
    • Display/edit a nested document
    • Send an SMS with Zapier
    • Hash a password with bcrypt
    • Display a customized response
    • Search on a smart field with two joints
    • Override the count route
    • Make a field readOnly with Sequelize
    • Hubspot integration
      • Create a Hubspot company
      • Display Hubspot companies
    • Impersonate a user
    • Import data from a CSV file
    • Import data from a JSON file
    • Load smart fields using hook
    • Pre-fill a form with data from a relationship
    • Re-use a smart field logic
    • Link to record info in a smart view
    • Display data in html format
    • Upload files to AWS S3
    • Display AWS S3 files from signed URLs
    • Prevent record update
    • Display, search and update attributes from a JSON field
    • Add many existing records at the same time (hasMany-belongsTo relationship)
    • Track users’ logs with morgan
    • Search on relationship fields by default
    • Export related data as CSV
    • Run automated tests
  • Forest Admin Documentation
Powered by GitBook
On this page
  • Creating the Smart Collection with related data
  • Implementing the GET (all records with a filter on related data)
  • Implementing the GET (all records with the search)

Was this helpful?

  1. How to's
  2. Elasticsearch Integration

Another example

For the purpose of this example let's say we have an activity-logs index in Elasticsearch with the following mapping.

{
  "mappings": {
    "_doc": {
      "dynamic": "strict",
      "properties": {
        "action": {
          "type": "keyword"
        },
        "label": {
          "type": "text",
          "index_options": "docs",
          "norms": false
        },
        "userId": {
          "type": "keyword"
        },
        "collectionId": {
          "type": "keyword"
        },
        "createdAt": {
          "type": "date"
        }
      }
    }
  }
}

Creating the Smart Collection with related data

First, we declare the activity-logs collection in the forest/ directory.

In this Smart Collection, we want to display for each activity log its action, the label (in a field description), the related user that made the activity, the collectionId on which the activity was made and the date the activity was made by the user.

/forest/activity-logs.js
const { collection } = require('forest-express-sequelize');
const models = require('../models');

collection('activity-logs', {
  isSearchable: false,
  fields: [{
    field: 'id',
    type: 'string',
  }, {
    field: 'action',
    type: 'Enum',
    isFilterable: true,
    enums: [
      'create',
      'read',
      'update',
      'delete',
      'action',
      'search',
      'filter'],
  }, {
    field: 'label',
    type: 'String',
    isFilterable: true,
  }, {
    field: 'collectionId',
    type: 'String',
    isFilterable: true,
  }, {
    field: 'userId',
    type: 'String',
    isFilterable: true,
  }, {
    field: 'createdAt',
    type: 'Date',
    isFilterable: true,
  }, {
    field: 'user',
    type: 'Number',
    reference: 'users.id',
    get: async (activityLog) => {
      // For search queries, the user is already loaded for performance reasons
      if (activityLog.user) { return activityLog.user; }
      if (!activityLog.userId) { return null; }

      return models.users.findOne({
        attributes: ['id', 'firstName', 'lastName', 'email'],
        paranoid: false,
        where: {
          id: activityLog.userId,
        },
      });
    },
  }, {
    field: 'user_email',
    type: 'String',
    isFilterable: true,
    get: (activityLog) => {
      // The field is declared after, when processed, the user has already been retrieved
      return activityLog.user.email;
    },
  }
  ],
});

Implementing the GET (all records with a filter on related data)

routes/activity-logs.js
const express = require('express');
const router = express.Router();

const models = require('../models');

// We need parseFilter utils to create the where clause for sequelize
const { RecordSerializer, Schemas, parseFilter } = require('forest-express-sequelize');

const ElasticsearchHelper = require('../service/elasticsearch-helper');
const { FIELD_DEFINITIONS } = require('../utils/filter-translator');

const serializer = new RecordSerializer({ name: 'es-activity-logs' });

// Custom mapping function
function mapActivityLog(id, source) {
  const {
    createdAt,
    ...simpleProperties
  } = source;

  return {
    id,
    ...simpleProperties,
    createdAt: source.createdAt ? new Date(source.createdAt) : null,
  };
}

const configuration = {
  index: 'activity-logs-*',
  filterDefinition: {
    action: FIELD_DEFINITIONS.keyword,
    label: FIELD_DEFINITIONS.text,
    collectionId: FIELD_DEFINITIONS.keyword,
    userId: FIELD_DEFINITIONS.keyword,
    createdAt: FIELD_DEFINITIONS.date,
  },
  mappingFunction: mapActivityLog,
  sort: [
    { createdAt: { order: 'desc' } },
  ],
}

const elasticsearchHelper = new ElasticsearchHelper(configuration);

// Specific implementation to handle related data
async function computeUserFilter(models, filter, options) {
  const where = await parseFilter({
    ...filter,
    field: filter.field.replace('user_', ''),
  }, Schemas.schemas.users, options.timezone);

  const users = await models.users.findAll({
    where,
    attributes: ['id'],
    paranoid: false,
  });

  return { operator: 'equal', field: 'userId', value: users.map((user) => user.id) };
}

async function computeFilterOnRelatedEntity(models, options, filter) {
  if (filter.field === 'user_email') {
    return computeUserFilter(models, filter, options);
  }

  return filter;
}

async function computeFiltersOnRelatedEntities(models, filters, options) {
  if (!filters) {
    return undefined;
  }

  if (!filters.aggregator) {
    return computeFilterOnRelatedEntity(models, options, filters);
  }

  return {
    ...filters,
    conditions: await Promise.all(filters.conditions.map(
      computeFilterOnRelatedEntity.bind(undefined, models, options),
    )),
  };
}

router.get('/es-activity-logs', async (request, response, next) => {
  try {
    const pageSize = Number(request?.query?.page?.size) || 20;
    const page = Number(request?.query?.page?.number) || 1;
    const options = { timezone: request.query?.timezone };

    let filters;
    try {
      filters = request.query?.filters && JSON.parse(request.query.filters);
    } catch (e) {
      filters = undefined;
    }

    const filtersWithRelatedEntities = await computeFiltersOnRelatedEntities(models, filters, options);

    const result = await elasticsearchHelper.functionSearch({
      page,
      pageSize,
      filters: filtersWithRelatedEntities || undefined,
      options,
    });

    response.send({
      ...await serializer.serialize(result.results),
      meta: {
        count: result.count,
      }
    });
  } catch (e) {
    next(e);
  }
});

module.exports = router;

Implementing the GET (all records with the search)

Another way to search through related data is to implement your own search logic.

routes/activity-logs.js
const express = require('express');
const router = express.Router();

const models = require('../models');
const Sequelize = require('sequelize');

const { RecordSerializer } = require('forest-express-sequelize');

const ElasticsearchHelper = require('../service/elasticsearch-helper');
const { FIELD_DEFINITIONS } = require('../utils/filter-translator');

const serializer = new RecordSerializer({ name: 'es-activity-logs' });

// Custom mapping function
function mapActivityLog(id, source) {
  const {
    createdAt,
    ...simpleProperties
  } = source;

  return {
    id,
    ...simpleProperties,
    createdAt: source.createdAt ? new Date(source.createdAt) : null,
  };
}

const configuration = {
  index: 'activity-logs-*',
  filterDefinition: {
    action: FIELD_DEFINITIONS.keyword,
    label: FIELD_DEFINITIONS.text,
    collectionId: FIELD_DEFINITIONS.keyword,
    userId: FIELD_DEFINITIONS.keyword,
    createdAt: FIELD_DEFINITIONS.date,
  },
  mappingFunction: mapActivityLog,
  sort: [
    { createdAt: { order: 'desc' } },
  ],
}

const elasticsearchHelper = new ElasticsearchHelper(configuration);

router.get('/es-activity-logs', async (request, response, next) => {
  try {
    const pageSize = Number(request?.query?.page?.size) || 20;
    const page = Number(request?.query?.page?.number) || 1;
    const search = request.query?.search;

    // NOTICE: search all user ids whom firstName or lastName or email match %search%
    const { Op } = Sequelize;

    const where = {};
    const searchCondition = { [Op.iLike]: `%${search}%` };
    where[Op.or] = [
      { firstName: searchCondition },
      { lastName: searchCondition },
      { email: searchCondition },
    ];
    
    const userIdsFromSearch = await models.users.findAll({
      where,
      attributes: ['id'],
      paranoid: false,
    });

    // NOTICE: Create a custom boolean query for Elasticsearch
    const booleanQuery = {
      should: [{
        terms: {
          userId: userIdsFromSearch.map((user) => user.id),
        },
      }],
      minimum_should_match: 1,
    };
    
    // NOTICE: Use the elasticsearchHelper to query Elasticsearch
    const [results, count] = await Promise.all([
      elasticsearchHelper.esSearch(
        { page, pageSize },
        booleanQuery,
      ),
      elasticsearchHelper.esCount(booleanQuery),
    ]);
    
    response.send({
      ...await serializer.serialize(results),
      meta: {
        count: count,
      }
    });
  } catch (e) {
    next(e);
  }
});

module.exports = router;
PreviousElasticsearch service/utilsNextZendesk Integration

Last updated 4 years ago

Was this helpful?

You can check out the list of if you need them for your own case.

This is a complex use case: How to handle filters on related data. We want to be able to filter using the user.mail field. To accommodate you we already provide you a simple service that handles all the logic to connect with your Elasticseearch data.

available field options
ElasticsearchHelper
Example of filter on related data
Example of custom search