Skip to content

Commit

Permalink
feat(search-client): Add support for Universal Search Clients (#2894)
Browse files Browse the repository at this point in the history
* feat(search-client): Add support and checks for `searchClient`
* test(search-client): Add tests for `searchClient`
* refactor(search-client): Refactor code
* feat: Throw if `searchClient` doesn't implement `search()`
* test: Test `searchClient` throws error if no `search()` method
* feat: Deprecate `createAlgoliaClient`
* test: Test error message thrown
* test: Snapshot test the search client method input
* fix: Deep copy of the search client
* ci(lint): Rename FIXME to THROWAWAY for eslint to not fail
* refactor(searchClient): Clone `searchClient` with `Object.create()`
* docs(searchClient): Add docs for `searchClient`
* docs: Replace `br` with new lines
* docs: Document why we need `Object.create()`
* docs(guide): Add guide "Prepare for v3" (#2905)
* docs(guide): Add "Prepare for v3" guide
* feat(guide): Lower nav weight for routing guide
This makes the "Prepare for v3" guide last in the nav list.

* feat(menu): Add "Prepare for v3" guide in the menu
* docs: Add intro and change titles
* docs: Add "Deprecations"
* docs: Rephrase introduction
* docs: Move second part of intro in first section
* docs: Link guide to `searchClient`
* refactor: Update client assignation
* test: Update `client.search()`
* chore: Add lock file
* revert: Add lock file
This reverts commit 2b4d47b.

* feat: Change usage message for search clients
* test: Update usage
* feat: Update warning for `createAlgoliaClient`
  • Loading branch information
francoischalifour authored and bobylito committed Apr 26, 2018
1 parent 925789a commit 5df3c74
Show file tree
Hide file tree
Showing 8 changed files with 328 additions and 24 deletions.
8 changes: 8 additions & 0 deletions docgen/src/data/communityHeader.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,10 @@
{
"name": "Integrate with Angular (2+)",
"url": "guides/angular-integration.html"
},
{
"name": "Prepare for v3",
"url": "guides/prepare-for-v3.html"
}
]
},
Expand Down Expand Up @@ -144,6 +148,10 @@
"name": "Migrate from V1",
"url": "guides/migration.html"
},
{
"name": "Prepare for v3",
"url": "guides/prepare-for-v3.html"
},
{
"name": "instantsearch()",
"url": "instantsearch.html"
Expand Down
50 changes: 50 additions & 0 deletions docgen/src/guides/prepare-for-v3.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
---
title: Prepare for v3
mainTitle: Guides
layout: main.pug
category: guides
withHeadings: true
navWeight: 0
editable: true
githubSource: docgen/src/guides/prepare-for-v3.md
---

Starting with 2.8.0, we are introducing changes and deprecations to prepare for the upcoming release of InstantSearch.js 3.

## Initializing InstantSearch

InstantSearch has always worked with the [Algolia search client](https://github.com/algolia/algoliasearch-client-javascript) behind the scenes. This will no longer be the default for InstantSearch.js 3. To prepare this future migration, you can start using the new options now.

### Current usage

1. [Import `InstantSearch.js`](https://community.algolia.com/instantsearch.js/v2/getting-started.html#install-instantsearchjs)
2. Initialize InstantSearch

```javascript
const search = instantsearch({
appId: 'appId',
apiKey: 'apiKey',
indexName: 'indexName',
});

search.start();
```

### New usage

1. [Import `algoliasearch`](https://github.com/algolia/algoliasearch-client-javascript)
2. [Import `InstantSearch.js`](https://community.algolia.com/instantsearch.js/v2/getting-started.html#install-instantsearchjs)
3. Initialize InstantSearch with the `searchClient` option

```javascript
const search = instantsearch({
indexName: 'indexName',
searchClient: algoliasearch('appId', 'apiKey'),
});

search.start();
```

## Deprecations

* [`createAlgoliaClient`](https://community.algolia.com/instantsearch.js/v2/instantsearch.html#struct-InstantSearchOptions-createAlgoliaClient) becomes deprecated in favor of [`searchClient`](https://community.algolia.com/instantsearch.js/v2/instantsearch.html#struct-InstantSearchOptions-searchClient)
1 change: 1 addition & 0 deletions docgen/src/guides/routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ mainTitle: Guides
layout: main.pug
category: guides
name: routing
navWeight: 5
withHeadings: true
editable: true
githubSource: docgen/src/guides/routing.md
Expand Down
92 changes: 71 additions & 21 deletions src/lib/InstantSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,39 @@ function defaultCreateURL() {
const defaultCreateAlgoliaClient = (defaultAlgoliasearch, appId, apiKey) =>
defaultAlgoliasearch(appId, apiKey);

const checkOptions = ({
appId,
apiKey,
indexName,
createAlgoliaClient,
searchClient,
}) => {
if (!searchClient) {
if (appId === null || apiKey === null || indexName === null) {
const usage = `
Usage: instantsearch({
appId: 'my_application_id',
apiKey: 'my_search_api_key',
indexName: 'my_index_name'
});`;
throw new Error(usage);
}
} else if (
searchClient &&
(indexName === null ||
appId !== null ||
apiKey !== null ||
createAlgoliaClient !== defaultCreateAlgoliaClient)
) {
const usage = `
Usage: instantsearch({
indexName: 'my_index_name',
searchClient: algoliasearch('appId', 'apiKey')
});`;
throw new Error(usage);
}
};

/**
* Widgets are the building blocks of InstantSearch.js. Any
* valid widget must have at least a `render` or a `init` function.
Expand All @@ -41,30 +74,39 @@ const defaultCreateAlgoliaClient = (defaultAlgoliasearch, appId, apiKey) =>
* @fires Instantsearch#render This event is triggered each time a render is done
*/
class InstantSearch extends EventEmitter {
constructor({
appId = null,
apiKey = null,
indexName = null,
numberLocale,
searchParameters = {},
urlSync = null,
routing = null,
searchFunction,
createAlgoliaClient = defaultCreateAlgoliaClient,
stalledSearchDelay = 200,
}) {
constructor(options) {
super();
if (appId === null || apiKey === null || indexName === null) {
const usage = `
Usage: instantsearch({
appId: 'my_application_id',
apiKey: 'my_search_api_key',
indexName: 'my_index_name'
});`;
throw new Error(usage);

const {
appId = null,
apiKey = null,
indexName = null,
numberLocale,
searchParameters = {},
urlSync = null,
routing = null,
searchFunction,
createAlgoliaClient = defaultCreateAlgoliaClient,
stalledSearchDelay = 200,
searchClient = null,
} = options;

checkOptions({
appId,
apiKey,
indexName,
createAlgoliaClient,
searchClient,
});

if (searchClient && typeof searchClient.search !== 'function') {
throw new Error(
'InstantSearch configuration error: `searchClient` must implement a `search(requests)` method.'
);
}

const client = createAlgoliaClient(algoliasearch, appId, apiKey);
const client =
searchClient || createAlgoliaClient(algoliasearch, appId, apiKey);

if (typeof client.addAlgoliaAgent === 'function') {
client.addAlgoliaAgent(`instantsearch.js ${version}`);
Expand Down Expand Up @@ -115,6 +157,14 @@ Usage: instantsearch({
...ROUTING_DEFAULT_OPTIONS,
...routing,
};

if (options.createAlgoliaClient) {
// eslint-disable-next-line no-console
console.warn(`
InstantSearch.js: \`createAlgoliaClient\` option is deprecated and will be removed in the next major version.
Please use \`searchClient\` instead: https://community.algolia.com/instantsearch.js/v2/instantsearch.html#struct-InstantSearchOptions-searchClient.
To help you migrate, please refer to the migration guide: https://community.algolia.com/instantsearch.js/v2/guides/prepare-for-v3.html`);
}
}

/**
Expand Down
31 changes: 31 additions & 0 deletions src/lib/__tests__/__snapshots__/search-client-test.js.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`InstantSearch Search Client Lifecycle calls the provided searchFunction when used 1`] = `
Array [
Object {
"indexName": "",
"params": Object {
"facets": Array [],
"page": 0,
"query": "test",
"tagFilters": "",
},
},
]
`;

exports[`InstantSearch Search Client Lifecycle gets called on search 1`] = `
Array [
Object {
"indexName": "",
"params": Object {
"facets": Array [],
"page": 0,
"query": "",
"tagFilters": "",
},
},
]
`;

exports[`InstantSearch Search Client Properties throws if no \`search()\` method 1`] = `"InstantSearch configuration error: \`searchClient\` must implement a \`search(requests)\` method."`;
62 changes: 62 additions & 0 deletions src/lib/__tests__/api-collision.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
/* eslint no-new: off */
import InstantSearch from '../InstantSearch';

const usage = `
Usage: instantsearch({
indexName: 'my_index_name',
searchClient: algoliasearch('appId', 'apiKey')
});`;

// THROWAWAY: Test suite to remove once the next major version is released
describe('InstantSearch API collision', () => {
describe('with search client', () => {
const appId = 'appId';
const apiKey = 'apiKey';
const indexName = 'indexName';
const searchClient = { search() {} };

it('and indexName', () => {
expect(() => {
new InstantSearch({
indexName,
searchClient,
});
}).not.toThrow();
});

it('and nothing else', () => {
expect(() => {
new InstantSearch({
searchClient,
});
}).toThrow(usage);
});

it('and appId', () => {
expect(() => {
new InstantSearch({
appId,
searchClient,
});
}).toThrow(usage);
});

it('and apiKey', () => {
expect(() => {
new InstantSearch({
apiKey,
searchClient,
});
}).toThrow(usage);
});

it('and createAlgoliaClient', () => {
expect(() => {
new InstantSearch({
createAlgoliaClient: () => {},
searchClient,
});
}).toThrow(usage);
});
});
});
62 changes: 62 additions & 0 deletions src/lib/__tests__/search-client-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
import InstantSearch from '../InstantSearch';

describe('InstantSearch Search Client', () => {
describe('Properties', () => {
it('throws if no `search()` method', () => {
expect(() => {
// eslint-disable-next-line no-new
new InstantSearch({
indexName: '',
searchClient: {},
});
}).toThrowErrorMatchingSnapshot();
});
});

describe('Lifecycle', () => {
it('gets called on search', () => {
const searchClientSpy = {
search: jest.fn(() => Promise.resolve({ results: [{}] })),
};

const search = new InstantSearch({
indexName: '',
searchClient: searchClientSpy,
});

expect(searchClientSpy.search).not.toHaveBeenCalled();

search.start();

expect(search.helper.state.query).toBe('');
expect(searchClientSpy.search).toHaveBeenCalledTimes(1);
expect(searchClientSpy.search.mock.calls[0][0]).toMatchSnapshot();
});

it('calls the provided searchFunction when used', () => {
const searchFunctionSpy = jest.fn(h => {
h.setQuery('test').search();
});

const searchClientSpy = {
search: jest.fn(() => Promise.resolve({ results: [{}] })),
};

const search = new InstantSearch({
indexName: '',
searchFunction: searchFunctionSpy,
searchClient: searchClientSpy,
});

expect(searchFunctionSpy).not.toHaveBeenCalled();
expect(searchClientSpy.search).not.toHaveBeenCalled();

search.start();

expect(searchFunctionSpy).toHaveBeenCalledTimes(1);
expect(search.helper.state.query).toBe('test');
expect(searchClientSpy.search).toHaveBeenCalledTimes(1);
expect(searchClientSpy.search.mock.calls[0][0]).toMatchSnapshot();
});
});
});
Loading

0 comments on commit 5df3c74

Please sign in to comment.