• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

GrottoCenter / grottocenter-api / 19538660432

20 Nov 2025 01:33PM UTC coverage: 45.853% (-0.5%) from 46.316%
19538660432

Pull #1407

github

vmarseguerra
feat(search): uses Typesense as a search database
Pull Request #1407: Search improvements

1040 of 2997 branches covered (34.7%)

Branch coverage included in aggregate %.

123 of 477 new or added lines in 48 files covered. (25.79%)

9 existing lines in 9 files now uncovered.

3084 of 5997 relevant lines covered (51.43%)

6.91 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

18.81
/api/services/SearchService.js
1
const typesense = require('../../config/typesense');
7✔
2

3
const organization = require('../dbSync/entites/organization');
7✔
4
const person = require('../dbSync/entites/person');
7✔
5
const massif = require('../dbSync/entites/massif');
7✔
6
const entrance = require('../dbSync/entites/entrance');
7✔
7
const cave = require('../dbSync/entites/cave');
7✔
8
const document = require('../dbSync/entites/document');
7✔
9

10
const allEntities = {
7✔
11
  [organization.search.schema.name]: organization.search,
12
  [person.search.schema.name]: person.search,
13
  [massif.search.schema.name]: massif.search,
14
  [cave.search.schema.name]: cave.search,
15
  [entrance.search.schema.name]: entrance.search,
16
  [document.search.schema.name]: document.search,
17
};
18
const allEntitiesKeys = Object.keys(allEntities);
7✔
19

20
function buildFilter(filter, isLogicalCompareAnd = true) {
×
NEW
21
  const out = Object.entries(filter)
×
NEW
22
    .filter(([, v]) => v)
×
23
    .map(([k, v]) => {
NEW
24
      let vFmt = v;
×
NEW
25
      let operator = ':'; // Partial equal
×
NEW
26
      if (Array.isArray(v))
×
NEW
27
        vFmt = `[${v.join('..')}]`; // Range
×
NEW
28
      else if (typeof v === 'boolean' || typeof v === 'number')
×
NEW
29
        operator = ':='; // Exact equal
×
NEW
30
      else vFmt = `\`${v}\``;
×
NEW
31
      return `${k}${operator}${vFmt}`;
×
32
    });
NEW
33
  return out.join(isLogicalCompareAnd ? ' && ' : ' || ');
×
34
}
35

36
async function isAlive() {
37
  return typesense.isAlive();
5✔
38
}
39

40
async function deleteDocument(entityName, documentId) {
41
  if (process.env.NODE_ENV === 'test') {
3!
42
    sails.log.info('SearchDb delete is disabled in during test');
3✔
43
    return;
3✔
44
  }
NEW
45
  if (!allEntitiesKeys.includes(entityName)) return;
×
NEW
46
  await typesense.deleteDocument(entityName, documentId);
×
47
}
48

49
async function updateDocument(entityName, doc) {
50
  if (process.env.NODE_ENV === 'test') {
20!
51
    sails.log.info('SearchDb upadet is disabled in during test');
20✔
52
    return;
20✔
53
  }
NEW
54
  if (!allEntitiesKeys.includes(entityName)) return;
×
NEW
55
  await typesense
×
56
    .importDocuments(entityName, [doc], allEntities[entityName].importFormater)
57
    .catch((err) => {
NEW
58
      sails.log.error(
×
59
        'Error in SearchService updateDocument',
60
        entityName,
61
        doc,
62
        err
63
      );
64
    });
65
}
66

67
async function multiCollectionsSearch({
×
68
  query,
69
  entities = [],
×
70
  filter = {},
×
71
} = {}) {
72
  // eslint-disable-next-line no-param-reassign
NEW
73
  entities = entities.filter((e) => allEntitiesKeys.includes(e));
×
NEW
74
  if (entities.length === 0) return null;
×
NEW
75
  const q = query || '*';
×
NEW
76
  const filterBy = buildFilter(filter);
×
77

NEW
78
  const collections = entities.map((e) => ({
×
79
    collection: e,
80
    query_by: allEntities[e].query.query_by,
81
    ...(filterBy && { filter_by: filterBy }),
×
82
  }));
83

NEW
84
  return typesense.multiSearch(collections, {
×
85
    per_page: 20,
86
    q,
87
  });
88
}
89

90
async function collectionSearch({
×
91
  query,
92
  entity = [],
×
93
  sort,
94
  filter = {},
×
95
  isLogicalCompareAnd = true,
×
96
  page,
97
  size,
98
  fields,
99
} = {}) {
NEW
100
  if (!allEntitiesKeys.includes(entity)) return null;
×
NEW
101
  const q = query || '*';
×
NEW
102
  const filterBy = buildFilter(filter, isLogicalCompareAnd);
×
103

NEW
104
  const params = {
×
105
    q,
106
    query_by: allEntities[entity].query.query_by,
107
    page, // Page starts at 1
108
    per_page: size,
109
    ...(sort && { sort_by: `${sort},_text_match:desc` }),
×
110
    ...(filterBy && { filter_by: filterBy }),
×
111
    ...(fields && { include_fields: fields.join(',') }),
×
112
  };
113

NEW
114
  return typesense.search(entity, params);
×
115
}
116

117
async function fieldSearch({
×
118
  entity,
119
  field,
120
  query,
121
  filter = {},
×
122
  size,
123
  isLogicalCompareAnd = true,
×
124
} = {}) {
NEW
125
  if (!allEntitiesKeys.includes(entity)) return null;
×
NEW
126
  if (!field) return null;
×
NEW
127
  const q = query || '*';
×
NEW
128
  const filterBy = buildFilter(filter, isLogicalCompareAnd);
×
129

NEW
130
  const params = {
×
131
    q,
132
    query_by: field,
133
    group_by: field,
134
    group_limit: 1,
135
    sort_by: '_group_found:desc',
136
    per_page: size,
137
    ...(filterBy && { filter_by: filterBy }),
×
138
  };
139

NEW
140
  return typesense.search(entity, params);
×
141
}
142

143
module.exports = {
7✔
144
  isAlive,
145

146
  updateDocument,
147
  deleteDocument,
148

149
  multiCollectionsSearch,
150
  collectionSearch,
151
  fieldSearch,
152
};
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc