Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(search-client): Add support for Custom Search Clients #2894

Merged
merged 24 commits into from
Apr 26, 2018
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
b085d77
feat(search-client): Add support and checks for `searchClient`
francoischalifour Apr 17, 2018
a700991
test(search-client): Add tests for `searchClient`
francoischalifour Apr 17, 2018
76c242e
refactor(search-client): Refactor code
francoischalifour Apr 18, 2018
113a56c
feat: Throw if `searchClient` doesn't implement `search()`
francoischalifour Apr 18, 2018
af50f97
test: Test `searchClient` throws error if no `search()` method
francoischalifour Apr 18, 2018
3c4da58
feat: Deprecate `createAlgoliaClient`
francoischalifour Apr 18, 2018
fd78e86
test: Test error message thrown
francoischalifour Apr 18, 2018
1b50bb4
test: Snapshot test the search client method input
francoischalifour Apr 18, 2018
dae8d23
fix: Deep copy of the search client
francoischalifour Apr 18, 2018
9fc0eb9
ci(lint): Rename FIXME to THROWAWAY for eslint to not fail
francoischalifour Apr 18, 2018
28c8f0e
refactor(searchClient): Clone `searchClient` with `Object.create()`
francoischalifour Apr 19, 2018
0bc3cf7
docs(searchClient): Add docs for `searchClient`
francoischalifour Apr 19, 2018
d6ce1c7
docs: Replace `br` with new lines
francoischalifour Apr 19, 2018
462b0c1
docs: Document why we need `Object.create()`
francoischalifour Apr 20, 2018
de182ab
docs(guide): Add guide "Prepare for v3" (#2905)
francoischalifour Apr 23, 2018
2de38bb
docs: Link guide to `searchClient`
francoischalifour Apr 25, 2018
35cba06
Merge branch 'feat/2.8' into feat/search-client-support
francoischalifour Apr 26, 2018
627d7f2
refactor: Update client assignation
francoischalifour Apr 26, 2018
517c109
test: Update `client.search()`
francoischalifour Apr 26, 2018
2b4d47b
chore: Add lock file
francoischalifour Apr 26, 2018
837c47e
revert: Add lock file
francoischalifour Apr 26, 2018
ae351c8
feat: Change usage message for search clients
francoischalifour Apr 26, 2018
dca3fab
test: Update usage
francoischalifour Apr 26, 2018
fa0ca7f
feat: Update warning for `createAlgoliaClient`
francoischalifour Apr 26, 2018
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
109 changes: 85 additions & 24 deletions src/lib/InstantSearch.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,42 @@ 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: {
search(requests) {},
searchForFacetValues(requests) {}
}
});`;
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,33 +77,50 @@ 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 client = createAlgoliaClient(algoliasearch, appId, apiKey);
client.addAlgoliaAgent(`instantsearch.js ${version}`);
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) {
if (typeof searchClient.search !== 'function') {
throw new Error(
'InstantSearch configuration error: `searchClient` must implement a `search(requests)` method.'
);
}

const client = Object.create(searchClient);
client.addAlgoliaAgent = client.addAlgoliaAgent || (() => {});
client.clearCache = client.clearCache || (() => {});

this.client = client;
} else {
const client = createAlgoliaClient(algoliasearch, appId, apiKey);
client.addAlgoliaAgent(`instantsearch.js ${version}`);

this.client = client;
}

this.client = client;
this.helper = null;
this.indexName = indexName;
this.searchParameters = { ...searchParameters, index: indexName };
Expand Down Expand Up @@ -112,6 +165,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.'
);
}
}

/**
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."`;
65 changes: 65 additions & 0 deletions src/lib/__tests__/api-collision.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
/* eslint no-new: off */
import InstantSearch from '../InstantSearch';

const usage = `
Usage: instantsearch({
indexName: 'my_index_name',
searchClient: {
search(requests) {},
searchForFacetValues(requests) {}
}
});`;

// 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);
});
});
});
72 changes: 72 additions & 0 deletions src/lib/__tests__/search-client-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
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();
});

it('should have default `addAlgoliaAgent()` and `clearCache()` methods', () => {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We'll get rid of this test once #2903 is merged.

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

expect(search.client.addAlgoliaAgent).toBeDefined();
expect(search.client.clearCache).toBeDefined();
});
});

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

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(),
};

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();
});
});
});
45 changes: 41 additions & 4 deletions src/lib/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,12 @@ import * as stateMappings from './stateMappings/index.js';
* @property {function} dispose cleans up any event listeners.
*/

/**
* @typedef {Object} SearchClient
* @property {function} search Performs the requests in the hits.
* @property {function} searchForFacetValues Performs the requests in the facet values.
*/

/**
* @typedef {Object} InstantSearchOptions
* @property {string} appId The Algolia application ID
Expand All @@ -89,10 +95,10 @@ import * as stateMappings from './stateMappings/index.js';
* @property {function} [searchFunction] A hook that will be called each time a search needs to be done, with the
* helper as a parameter. It's your responsibility to call helper.search(). This option allows you to avoid doing
* searches at page load for example.
* @property {function} [createAlgoliaClient] Allows you to provide your own algolia client instead of
* the one instantiated internally by instantsearch.js. Useful in situations where you need
* to setup complex mechanism on the client or if you need to share it easily.
* Usage:
* @property {function} [createAlgoliaClient] _Deprecated in favor of [`searchClient`](instantsearch.html#struct-InstantSearchOptions-searchClient)._<br>
* Allows you to provide your own algolia client instead of the one instantiated internally by instantsearch.js.
* Useful in situations where you need to setup complex mechanism on the client or if you need to share it easily.
* <br>Usage:
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can you also leave a newline instead of br?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, didn't realize we could.

* ```javascript
* instantsearch({
* // other parameters
Expand All @@ -110,6 +116,37 @@ import * as stateMappings from './stateMappings/index.js';
* @property {number} [stalledSearchDelay=200] Time before a search is considered stalled.
* @property {RoutingOptions} [routing] the router configuration used to save the UI State into the URL or
* any client side persistence.
* @property {SearchClient} [searchClient] The search client to plug to instantsearch.js. You should start updating with this
* syntax to ease the migration to InstantSearch 3.
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We could link to the "Preparing for v3" guide once it's published.

* <br>Usage:
* ```javascript
* // Using the default Algolia client (https://github.com/algolia/algoliasearch-client-javascript)
* // This is the default client used by InstantSearch. Equivalent to:
* // instantsearch({
* // appId: 'appId',
* // apiKey: 'apiKey',
* // indexName: 'indexName',
* // });
* instantsearch({
* indexName: 'indexName',
* searchClient: algoliasearch('appId', 'apiKey')
* });
*
* // Using a custom search client
* instantsearch({
* indexName: 'indexName',
* searchClient: {
* search(requests) {
* // fetch response based on requests
* return response;
* },
* searchForFacetValues(requests) {
* // fetch response based on requests
* return response;
* }
* }
* });
* ```
*/

/**
Expand Down