Woodshop
Search…
Another example
For the purpose of this example let's say we have an activity-logs index in Elasticsearch with the following mapping.
1
{
2
"mappings": {
3
"_doc": {
4
"dynamic": "strict",
5
"properties": {
6
"action": {
7
"type": "keyword"
8
},
9
"label": {
10
"type": "text",
11
"index_options": "docs",
12
"norms": false
13
},
14
"userId": {
15
"type": "keyword"
16
},
17
"collectionId": {
18
"type": "keyword"
19
},
20
"createdAt": {
21
"type": "date"
22
}
23
}
24
}
25
}
26
}
Copied!

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.
You can check out the list of available field options if you need them for your own case.
/forest/activity-logs.js
1
const { collection } = require('forest-express-sequelize');
2
const models = require('../models');
3
4
collection('activity-logs', {
5
isSearchable: false,
6
fields: [{
7
field: 'id',
8
type: 'string',
9
}, {
10
field: 'action',
11
type: 'Enum',
12
isFilterable: true,
13
enums: [
14
'create',
15
'read',
16
'update',
17
'delete',
18
'action',
19
'search',
20
'filter'],
21
}, {
22
field: 'label',
23
type: 'String',
24
isFilterable: true,
25
}, {
26
field: 'collectionId',
27
type: 'String',
28
isFilterable: true,
29
}, {
30
field: 'userId',
31
type: 'String',
32
isFilterable: true,
33
}, {
34
field: 'createdAt',
35
type: 'Date',
36
isFilterable: true,
37
}, {
38
field: 'user',
39
type: 'Number',
40
reference: 'users.id',
41
get: async (activityLog) => {
42
// For search queries, the user is already loaded for performance reasons
43
if (activityLog.user) { return activityLog.user; }
44
if (!activityLog.userId) { return null; }
45
46
return models.users.findOne({
47
attributes: ['id', 'firstName', 'lastName', 'email'],
48
paranoid: false,
49
where: {
50
id: activityLog.userId,
51
},
52
});
53
},
54
}, {
55
field: 'user_email',
56
type: 'String',
57
isFilterable: true,
58
get: (activityLog) => {
59
// The field is declared after, when processed, the user has already been retrieved
60
return activityLog.user.email;
61
},
62
}
63
],
64
});
65
Copied!

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

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 ElasticsearchHelper that handles all the logic to connect with your Elasticseearch data.
routes/activity-logs.js
1
const express = require('express');
2
const router = express.Router();
3
4
const models = require('../models');
5
6
// We need parseFilter utils to create the where clause for sequelize
7
const { RecordSerializer, Schemas, parseFilter } = require('forest-express-sequelize');
8
9
const ElasticsearchHelper = require('../service/elasticsearch-helper');
10
const { FIELD_DEFINITIONS } = require('../utils/filter-translator');
11
12
const serializer = new RecordSerializer({ name: 'es-activity-logs' });
13
14
// Custom mapping function
15
function mapActivityLog(id, source) {
16
const {
17
createdAt,
18
...simpleProperties
19
} = source;
20
21
return {
22
id,
23
...simpleProperties,
24
createdAt: source.createdAt ? new Date(source.createdAt) : null,
25
};
26
}
27
28
const configuration = {
29
index: 'activity-logs-*',
30
filterDefinition: {
31
action: FIELD_DEFINITIONS.keyword,
32
label: FIELD_DEFINITIONS.text,
33
collectionId: FIELD_DEFINITIONS.keyword,
34
userId: FIELD_DEFINITIONS.keyword,
35
createdAt: FIELD_DEFINITIONS.date,
36
},
37
mappingFunction: mapActivityLog,
38
sort: [
39
{ createdAt: { order: 'desc' } },
40
],
41
}
42
43
const elasticsearchHelper = new ElasticsearchHelper(configuration);
44
45
// Specific implementation to handle related data
46
async function computeUserFilter(models, filter, options) {
47
const where = await parseFilter({
48
...filter,
49
field: filter.field.replace('user_', ''),
50
}, Schemas.schemas.users, options.timezone);
51
52
const users = await models.users.findAll({
53
where,
54
attributes: ['id'],
55
paranoid: false,
56
});
57
58
return { operator: 'equal', field: 'userId', value: users.map((user) => user.id) };
59
}
60
61
async function computeFilterOnRelatedEntity(models, options, filter) {
62
if (filter.field === 'user_email') {
63
return computeUserFilter(models, filter, options);
64
}
65
66
return filter;
67
}
68
69
async function computeFiltersOnRelatedEntities(models, filters, options) {
70
if (!filters) {
71
return undefined;
72
}
73
74
if (!filters.aggregator) {
75
return computeFilterOnRelatedEntity(models, options, filters);
76
}
77
78
return {
79
...filters,
80
conditions: await Promise.all(filters.conditions.map(
81
computeFilterOnRelatedEntity.bind(undefined, models, options),
82
)),
83
};
84
}
85
86
router.get('/es-activity-logs', async (request, response, next) => {
87
try {
88
const pageSize = Number(request?.query?.page?.size) || 20;
89
const page = Number(request?.query?.page?.number) || 1;
90
const options = { timezone: request.query?.timezone };
91
92
let filters;
93
try {
94
filters = request.query?.filters && JSON.parse(request.query.filters);
95
} catch (e) {
96
filters = undefined;
97
}
98
99
const filtersWithRelatedEntities = await computeFiltersOnRelatedEntities(models, filters, options);
100
101
const result = await elasticsearchHelper.functionSearch({
102
page,
103
pageSize,
104
filters: filtersWithRelatedEntities || undefined,
105
options,
106
});
107
108
response.send({
109
...await serializer.serialize(result.results),
110
meta: {
111
count: result.count,
112
}
113
});
114
} catch (e) {
115
next(e);
116
}
117
});
118
119
module.exports = router;
Copied!
Example of filter on related data

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
1
const express = require('express');
2
const router = express.Router();
3
4
const models = require('../models');
5
const Sequelize = require('sequelize');
6
7
const { RecordSerializer } = require('forest-express-sequelize');
8
9
const ElasticsearchHelper = require('../service/elasticsearch-helper');
10
const { FIELD_DEFINITIONS } = require('../utils/filter-translator');
11
12
const serializer = new RecordSerializer({ name: 'es-activity-logs' });
13
14
// Custom mapping function
15
function mapActivityLog(id, source) {
16
const {
17
createdAt,
18
...simpleProperties
19
} = source;
20
21
return {
22
id,
23
...simpleProperties,
24
createdAt: source.createdAt ? new Date(source.createdAt) : null,
25
};
26
}
27
28
const configuration = {
29
index: 'activity-logs-*',
30
filterDefinition: {
31
action: FIELD_DEFINITIONS.keyword,
32
label: FIELD_DEFINITIONS.text,
33
collectionId: FIELD_DEFINITIONS.keyword,
34
userId: FIELD_DEFINITIONS.keyword,
35
createdAt: FIELD_DEFINITIONS.date,
36
},
37
mappingFunction: mapActivityLog,
38
sort: [
39
{ createdAt: { order: 'desc' } },
40
],
41
}
42
43
const elasticsearchHelper = new ElasticsearchHelper(configuration);
44
45
router.get('/es-activity-logs', async (request, response, next) => {
46
try {
47
const pageSize = Number(request?.query?.page?.size) || 20;
48
const page = Number(request?.query?.page?.number) || 1;
49
const search = request.query?.search;
50
51
// NOTICE: search all user ids whom firstName or lastName or email match %search%
52
const { Op } = Sequelize;
53
54
const where = {};
55
const searchCondition = { [Op.iLike]: `%${search}%` };
56
where[Op.or] = [
57
{ firstName: searchCondition },
58
{ lastName: searchCondition },
59
{ email: searchCondition },
60
];
61
62
const userIdsFromSearch = await models.users.findAll({
63
where,
64
attributes: ['id'],
65
paranoid: false,
66
});
67
68
// NOTICE: Create a custom boolean query for Elasticsearch
69
const booleanQuery = {
70
should: [{
71
terms: {
72
userId: userIdsFromSearch.map((user) => user.id),
73
},
74
}],
75
minimum_should_match: 1,
76
};
77
78
// NOTICE: Use the elasticsearchHelper to query Elasticsearch
79
const [results, count] = await Promise.all([
80
elasticsearchHelper.esSearch(
81
{ page, pageSize },
82
booleanQuery,
83
),
84
elasticsearchHelper.esCount(booleanQuery),
85
]);
86
87
response.send({
88
...await serializer.serialize(results),
89
meta: {
90
count: count,
91
}
92
});
93
} catch (e) {
94
next(e);
95
}
96
});
97
98
module.exports = router;
Copied!
Example of custom search