Skip to content
This repository was archived by the owner on Dec 16, 2022. It is now read-only.

Commit 7df3225

Browse files
authored
feat(facets): use dynamicWidgets (#534)
* feat(facets): use dynamicWidgets * add configure * add condition * quotes * slightly safer + fix typos * make it a flag * refactor(search): call search only once * change text * refactor a little
1 parent fb4c2a8 commit 7df3225

File tree

15 files changed

+426
-111
lines changed

15 files changed

+426
-111
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,8 @@
3636
"node": ">= 10"
3737
},
3838
"dependencies": {
39-
"algoliasearch": "3.35.1",
39+
"@algolia/cache-in-memory": "4.11.0",
40+
"algoliasearch": "4.11.0",
4041
"chalk": "3.0.0",
4142
"commander": "4.0.1",
4243
"inquirer": "8.0.0",

src/cli/__tests__/getAttributesFromIndex.test.js

Lines changed: 18 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,23 @@
1-
const algoliasearch = require('algoliasearch');
21
const getAttributesFromIndex = require('../getAttributesFromIndex');
2+
const getInformationFromIndex = require('../getInformationFromIndex');
33

4-
jest.mock('algoliasearch');
4+
jest.mock('../getInformationFromIndex');
55

66
test('with search success should fetch attributes', async () => {
7-
algoliasearch.mockImplementationOnce(() => ({
8-
initIndex: () => ({
9-
search: () => ({
10-
hits: [
11-
{
12-
_highlightResult: {
13-
brand: 'brand',
14-
description: 'description',
15-
name: 'name',
16-
title: 'title',
17-
},
7+
getInformationFromIndex.mockImplementationOnce(() =>
8+
Promise.resolve({
9+
hits: [
10+
{
11+
_highlightResult: {
12+
brand: 'brand',
13+
description: 'description',
14+
name: 'name',
15+
title: 'title',
1816
},
19-
],
20-
}),
21-
}),
22-
}));
17+
},
18+
],
19+
})
20+
);
2321

2422
const attributes = await getAttributesFromIndex({
2523
appId: 'appId',
@@ -31,13 +29,9 @@ test('with search success should fetch attributes', async () => {
3129
});
3230

3331
test('with search failure should return default attributes', async () => {
34-
algoliasearch.mockImplementationOnce(() => ({
35-
initIndex: () => ({
36-
search: () => {
37-
throw new Error();
38-
},
39-
}),
40-
}));
32+
getInformationFromIndex.mockImplementationOnce(() =>
33+
Promise.reject(new Error())
34+
);
4135

4236
const attributes = await getAttributesFromIndex({
4337
appId: 'appId',
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
const getFacetsFromIndex = require('../getFacetsFromIndex');
2+
const getInformationFromIndex = require('../getInformationFromIndex');
3+
4+
jest.mock('../getInformationFromIndex');
5+
6+
test('with search success should fetch attributes', async () => {
7+
getInformationFromIndex.mockImplementationOnce(() =>
8+
Promise.resolve({
9+
facets: {
10+
abc: {},
11+
def: {},
12+
something: {},
13+
'something.nested': {},
14+
},
15+
})
16+
);
17+
18+
const attributes = await getFacetsFromIndex({
19+
appId: 'appId',
20+
apiKey: 'apiKey',
21+
indexName: 'indexName',
22+
});
23+
24+
expect(attributes).toEqual(['abc', 'def', 'something', 'something.nested']);
25+
});
26+
27+
test('with search failure should return default attributes', async () => {
28+
getInformationFromIndex.mockImplementationOnce(() =>
29+
Promise.reject(new Error())
30+
);
31+
32+
const attributes = await getFacetsFromIndex({
33+
appId: 'appId',
34+
apiKey: 'apiKey',
35+
indexName: 'indexName',
36+
});
37+
38+
expect(attributes).toEqual([]);
39+
});
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
const algoliasearch = require('algoliasearch');
2+
const getInformationFromIndex = require('../getInformationFromIndex');
3+
4+
jest.mock('algoliasearch', () => {
5+
const _algoliasearch = jest.fn(() => ({ search: _algoliasearch.__search }));
6+
_algoliasearch.__search = jest.fn(() =>
7+
Promise.resolve({
8+
results: [
9+
{
10+
hits: [],
11+
facets: {},
12+
},
13+
],
14+
})
15+
);
16+
17+
return _algoliasearch;
18+
});
19+
20+
test('returns default information', async () => {
21+
const info = await getInformationFromIndex({
22+
appId: 'a',
23+
apiKey: 'a',
24+
indexName: 'a',
25+
});
26+
27+
expect(info).toEqual({ hits: [], facets: {} });
28+
});
29+
30+
test('returns {} on error', async () => {
31+
algoliasearch.__search.mockImplementationOnce(() =>
32+
Promise.reject(new Error())
33+
);
34+
35+
const info = await getInformationFromIndex({
36+
appId: 'a',
37+
apiKey: 'a',
38+
indexName: 'a',
39+
});
40+
41+
expect(info).toEqual({});
42+
});
43+
44+
test('creates client once per credentials', async () => {
45+
await getInformationFromIndex({
46+
appId: 'a',
47+
apiKey: 'a',
48+
indexName: 'a',
49+
});
50+
51+
expect(algoliasearch).toHaveBeenCalledTimes(1);
52+
53+
await getInformationFromIndex({
54+
appId: 'b',
55+
apiKey: 'b',
56+
indexName: 'b',
57+
});
58+
59+
expect(algoliasearch).toHaveBeenCalledTimes(2);
60+
61+
await getInformationFromIndex({
62+
appId: 'b',
63+
apiKey: 'b',
64+
indexName: 'c',
65+
});
66+
67+
expect(algoliasearch).toHaveBeenCalledTimes(2);
68+
});

src/cli/getAttributesFromIndex.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,19 @@
1-
const algoliasearch = require('algoliasearch');
1+
const getInformationFromIndex = require('./getInformationFromIndex');
22

33
module.exports = async function getAttributesFromIndex({
44
appId,
55
apiKey,
66
indexName,
77
} = {}) {
8-
const client = algoliasearch(appId, apiKey);
9-
const index = client.initIndex(indexName);
108
const defaultAttributes = ['title', 'name', 'description'];
119
let attributes = [];
1210

1311
try {
14-
const { hits } = await index.search({ hitsPerPage: 1 });
12+
const { hits } = await getInformationFromIndex({
13+
appId,
14+
apiKey,
15+
indexName,
16+
});
1517
const [firstHit] = hits;
1618
const highlightedAttributes = Object.keys(firstHit._highlightResult);
1719
attributes = [

src/cli/getFacetsFromIndex.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
const getInformationFromIndex = require('./getInformationFromIndex');
2+
3+
module.exports = async function getFacetsFromIndex({
4+
appId,
5+
apiKey,
6+
indexName,
7+
} = {}) {
8+
try {
9+
const { facets } = await getInformationFromIndex({
10+
appId,
11+
apiKey,
12+
indexName,
13+
});
14+
return Object.keys(facets);
15+
} catch (err) {
16+
return [];
17+
}
18+
};

src/cli/getInformationFromIndex.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
const algoliasearch = require('algoliasearch');
2+
const { createInMemoryCache } = require('@algolia/cache-in-memory');
3+
4+
const clients = new Map();
5+
function getClient(appId, apiKey) {
6+
const key = [appId, apiKey].join('__');
7+
let client = clients.get(key);
8+
if (!client) {
9+
client = algoliasearch(appId, apiKey, {
10+
responsesCache: createInMemoryCache(),
11+
requestsCache: createInMemoryCache(),
12+
});
13+
14+
clients.set(key, client);
15+
}
16+
17+
return client;
18+
}
19+
20+
async function getInformationFromIndex({ appId, apiKey, indexName }) {
21+
try {
22+
const client = getClient(appId, apiKey);
23+
return await client
24+
.search([
25+
{
26+
indexName,
27+
params: {
28+
hitsPerPage: 1,
29+
facets: '*',
30+
maxValuesPerFacet: 1,
31+
},
32+
},
33+
])
34+
.then(({ results: [result] }) => result);
35+
} catch (err) {
36+
return {};
37+
}
38+
}
39+
40+
module.exports = getInformationFromIndex;

src/cli/index.js

Lines changed: 54 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
#!/usr/bin/env node
22
const path = require('path');
33
const process = require('process');
4+
const os = require('os');
45
const program = require('commander');
56
const inquirer = require('inquirer');
67
const chalk = require('chalk');
78
const latestSemver = require('latest-semver');
8-
const os = require('os');
9+
const semver = require('semver');
910

1011
const createInstantSearchApp = require('../api');
1112
const {
@@ -18,6 +19,7 @@ const {
1819
} = require('../utils');
1920
const getOptionsFromArguments = require('./getOptionsFromArguments');
2021
const getAttributesFromIndex = require('./getAttributesFromIndex');
22+
const getFacetsFromIndex = require('./getFacetsFromIndex');
2123
const getAnswersDefaultValues = require('./getAnswersDefaultValues');
2224
const isQuestionAsked = require('./isQuestionAsked');
2325
const {
@@ -61,6 +63,10 @@ const attributesToDisplay = (optionsFromArguments.attributesToDisplay || '')
6163
.split(',')
6264
.filter(Boolean)
6365
.map(x => x.trim());
66+
const attributesForFaceting = (optionsFromArguments.attributesForFaceting || '')
67+
.split(',')
68+
.filter(Boolean)
69+
.map(x => x.trim());
6470

6571
const getQuestions = ({ appName }) => ({
6672
application: [
@@ -156,6 +162,48 @@ const getQuestions = ({ appName }) => ({
156162
when: ({ appId, apiKey, indexName }) =>
157163
!attributesToDisplay.length > 0 && appId && apiKey && indexName,
158164
},
165+
{
166+
type: 'checkbox',
167+
name: 'attributesForFaceting',
168+
message: 'Attributes to display',
169+
suffix: `\n ${chalk.gray('Used to filter the search interface')}`,
170+
pageSize: 10,
171+
choices: async answers => {
172+
const templatePath = getTemplatePath(answers.template);
173+
const templateConfig = getAppTemplateConfig(templatePath);
174+
175+
const selectedLibraryVersion = answers.libraryVersion;
176+
const requiredLibraryVersion =
177+
templateConfig.flags && templateConfig.flags.dynamicWidgets;
178+
const supportsDynamicWidgets =
179+
selectedLibraryVersion &&
180+
requiredLibraryVersion &&
181+
semver.satisfies(selectedLibraryVersion, requiredLibraryVersion, {
182+
includePrerelease: true,
183+
});
184+
185+
const dynamicWidgets = supportsDynamicWidgets
186+
? [
187+
{
188+
name: 'Dynamic widgets',
189+
value: 'ais.dynamicWidgets',
190+
checked: true,
191+
},
192+
new inquirer.Separator(),
193+
]
194+
: [];
195+
196+
return [
197+
...dynamicWidgets,
198+
new inquirer.Separator('From your index'),
199+
...(await getFacetsFromIndex(answers)),
200+
new inquirer.Separator(),
201+
];
202+
},
203+
filter: attributes => attributes.filter(Boolean),
204+
when: ({ appId, apiKey, indexName }) =>
205+
!attributesForFaceting.length > 0 && appId && apiKey && indexName,
206+
},
159207
],
160208
widget: [
161209
{
@@ -301,6 +349,11 @@ async function run() {
301349
...configuration,
302350
...answers,
303351
...alternativeNames,
352+
flags: {
353+
dynamicWidgets:
354+
Array.isArray(answers.attributesForFaceting) &&
355+
answers.attributesForFaceting.includes('ais.dynamicWidgets'),
356+
},
304357
libraryVersion,
305358
template: templatePath,
306359
installation: program.installation,

src/templates/InstantSearch.js/.template.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,9 @@ module.exports = {
55
category: 'Web',
66
libraryName: 'instantsearch.js',
77
supportedVersion: '>= 3.0.0 < 5.0.0',
8+
flags: {
9+
dynamicWidgets: '>= 4.30',
10+
},
811
templateName: 'instantsearch.js',
912
appName: 'instantsearch.js-app',
1013
keywords: ['algolia', 'InstantSearch', 'Vanilla', 'instantsearch.js'],

0 commit comments

Comments
 (0)