diff --git a/community-website/src/community-project-boilerplate-docgen/src/widgets/instantsearch.md b/community-website/src/community-project-boilerplate-docgen/src/widgets/instantsearch.md index 5afddace8..26fdd22d4 100644 --- a/community-website/src/community-project-boilerplate-docgen/src/widgets/instantsearch.md +++ b/community-website/src/community-project-boilerplate-docgen/src/widgets/instantsearch.md @@ -60,10 +60,14 @@ export class AppComponent {} > 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. `createAlgoliaClient?: (algoliasearch: Function, appId: string, apiKey: string) => CustomClient` +> _Deprecated in favor of `searchClient`._ > Allows you to provide your own algolia client instead of the one instantiated internally. > Useful in situations where you need to setup complex mechanism on the client or if you need to share it easily. > We forward `algoliasearch` which is the original algoliasearch module imported inside angular-instantsearch. +`searchClient?: {}` +> The search client to plug to InstantSearch.js. + `searchParameters?: {}` > Additional parameters to pass to the Algolia API. diff --git a/examples/dev-novel/main.ts b/examples/dev-novel/main.ts index 80a006c6c..a385fe06e 100644 --- a/examples/dev-novel/main.ts +++ b/examples/dev-novel/main.ts @@ -1,5 +1,6 @@ import { enableProdMode } from "@angular/core"; import { start, storiesOf } from "dev-novel"; +import * as algoliasearch from "algoliasearch"; import { wrapWithHits } from "./wrap-with-hits"; import { MenuSelect } from "./custom-widgets"; @@ -33,6 +34,61 @@ storiesOf("InstantSearch").add( }) ); +storiesOf("InstantSearch").add( + "with algoliasearch search client", + wrapWithHits({ + template: "", + searchClient: algoliasearch("latency", "6be0576ff61c053d5f9a3225e2a90f76") + }) +); + +storiesOf("InstantSearch").add( + "with custom search client", + wrapWithHits({ + template: "", + searchClient: { + search(requests) { + return Promise.resolve({ + results: [ + { + hits: [ + { + objectID: "1", + image: + "https://cdn-demo.algolia.com/bestbuy-0118/5477500_sb.jpg", + price: "99.99", + rating: 4, + description: + "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Proin nunc lacus, vestibulum non rutrum a, dapibus interdum magna. Quisque semper orci erat, id placerat nunc convallis at. Praesent commodo, elit non fermentum blandit, augue dolor cursus metus, eu auctor leo erat sit amet ante. Interdum et malesuada fames ac ante ipsum primis in faucibus.", + _highlightResult: { + name: { + value: "Fake Result 1" + } + } + }, + { + objectID: "2", + image: + "https://cdn-demo.algolia.com/bestbuy-0118/4397400_sb.jpg", + price: "39.99", + rating: 3, + description: + "Morbi pretium urna et massa maximus maximus. Nunc risus lectus, mattis non malesuada quis, pretium eget ligula. Sed vulputate mauris congue, tempor velit et, pretium felis. Ut ullamcorper et ligula et congue. Nunc consequat massa massa. Etiam eu purus lorem. Ut bibendum nisi nec sapien imperdiet, vel laoreet velit porttitor.", + _highlightResult: { + name: { + value: "Fake Result 2" + } + } + } + ] + } + ] + }); + } + } + }) +); + storiesOf("Breadcrumb").add( "default", wrapWithHits({ diff --git a/examples/dev-novel/wrap-with-hits.ts b/examples/dev-novel/wrap-with-hits.ts index cd6b0bb46..ed7fc6e2f 100644 --- a/examples/dev-novel/wrap-with-hits.ts +++ b/examples/dev-novel/wrap-with-hits.ts @@ -15,6 +15,7 @@ export function wrapWithHits({ searchParameters = {}, methods = {}, searchFunction, + searchClient, appDeclarations = [] }: { template: string; @@ -22,6 +23,7 @@ export function wrapWithHits({ searchParameters?: {}; methods?: {}; searchFunction?: (helper: Helper) => void; + searchClient?: {}; appDeclarations?: any[]; }) { return (container: Element) => { @@ -117,9 +119,12 @@ export function wrapWithHits({ }) class AppComponent { config = { + ...(!searchClient && { + appId: "latency", + apiKey: "6be0576ff61c053d5f9a3225e2a90f76" + }), searchFunction, - apiKey: "6be0576ff61c053d5f9a3225e2a90f76", - appId: "latency", + searchClient, indexName: "instant_search", searchParameters: { hitsPerPage: 3, diff --git a/package.json b/package.json index ec2cbfff9..73cb5b076 100644 --- a/package.json +++ b/package.json @@ -51,10 +51,10 @@ "dependencies": { "@angular/common": "^4.4.5", "@angular/core": "^4.4.5", - "algoliasearch": "^3.24.7", - "algoliasearch-helper": "^2.23.0", + "algoliasearch": "^3.27.0", + "algoliasearch-helper": "^2.26.0", "instantsearch.css": "^7.0.0", - "instantsearch.js": "^2.7.0", + "instantsearch.js": "^2.8.0-beta.1", "lodash-es": "^4.17.4", "nouislider": "^10.0.0", "querystring-es3": "^0.2.1" diff --git a/src/create-ssr-algolia-client.ts b/src/create-ssr-algolia-client.ts index 1c4943c67..5d4864bf8 100644 --- a/src/create-ssr-algolia-client.ts +++ b/src/create-ssr-algolia-client.ts @@ -13,64 +13,85 @@ export function createSSRAlgoliaClient({ httpClient, HttpHeaders, transferState, - makeStateKey + makeStateKey, }) { - return (_, appId, apiKey) => { - const client = algoliasearch(appId, apiKey, {}); - client.addAlgoliaAgent(`angular-instantsearch ${VERSION}`); + console.warn( + '`createSSRAlgoliaClient` is deprecated in favor of `createSSRSearchClient` to be plugged to `searchClient`.' + ); - client._request = (rawUrl, opts) => { - let headers = new HttpHeaders(); + return (_, appId, apiKey) => + createSSRSearchClient({ + appId, + apiKey, + httpClient, + HttpHeaders, + transferState, + makeStateKey, + }); +} - headers = headers.set( - "content-type", - opts.method === "POST" - ? "application/x-www-form-urlencoded" - : "application/json" - ); +export function createSSRSearchClient({ + appId, + apiKey, + httpClient, + HttpHeaders, + transferState, + makeStateKey +}) { + const client = algoliasearch(appId, apiKey, {}); + client.addAlgoliaAgent(`angular-instantsearch ${VERSION}`); - headers = headers.set("accept", "application/json"); + client._request = (rawUrl, opts) => { + let headers = new HttpHeaders(); - const url = - rawUrl + (rawUrl.includes("?") ? "&" : "?") + encode(opts.headers); + headers = headers.set( + "content-type", + opts.method === "POST" + ? "application/x-www-form-urlencoded" + : "application/json" + ); - const transferStateKey = makeStateKey(`ngais(${opts.body})`); + headers = headers.set("accept", "application/json"); - if (transferState.hasKey(transferStateKey)) { - const resp = JSON.parse(transferState.get(transferStateKey, {})); - return Promise.resolve({ - statusCode: resp.status, - body: resp.body, - headers: resp.headers - }); - } + const url = + rawUrl + (rawUrl.includes("?") ? "&" : "?") + encode(opts.headers); - return new Promise((resolve, reject) => { - httpClient - .request(opts.method, url, { - headers, - body: opts.body, - observe: "response" - }) - .subscribe( - resp => { - transferState.set(transferStateKey, JSON.stringify(resp)); - resolve({ - statusCode: resp.status, - body: resp.body, - headers: resp.headers - }); - }, - resp => - reject({ - statusCode: resp.status, - body: resp.body, - headers: resp.headers - }) - ); + const transferStateKey = makeStateKey(`ngais(${opts.body})`); + + if (transferState.hasKey(transferStateKey)) { + const resp = JSON.parse(transferState.get(transferStateKey, {})); + return Promise.resolve({ + statusCode: resp.status, + body: resp.body, + headers: resp.headers }); - }; + } - return client; + return new Promise((resolve, reject) => { + httpClient + .request(opts.method, url, { + headers, + body: opts.body, + observe: "response" + }) + .subscribe( + resp => { + transferState.set(transferStateKey, JSON.stringify(resp)); + resolve({ + statusCode: resp.status, + body: resp.body, + headers: resp.headers + }); + }, + resp => + reject({ + statusCode: resp.status, + body: resp.body, + headers: resp.headers + }) + ); + }); }; + + return client; } diff --git a/src/index.ts b/src/index.ts index be7a02396..b591ff94d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -26,8 +26,11 @@ import { NgAisPanelModule } from "./panel/panel.module"; import { NgAisConfigureModule } from "./configure/configure.module"; // Custom SSR algoliasearchClient -import { createSSRAlgoliaClient } from "./create-ssr-algolia-client"; -export { createSSRAlgoliaClient }; +import { + createSSRAlgoliaClient, + createSSRSearchClient, +} from "./create-ssr-algolia-client"; +export { createSSRAlgoliaClient, createSSRSearchClient }; import { parseServerRequest } from "./parse-server-request"; export { parseServerRequest }; diff --git a/src/instantsearch/instantsearch.ts b/src/instantsearch/instantsearch.ts index 96eb9c5de..f6efe81d3 100644 --- a/src/instantsearch/instantsearch.ts +++ b/src/instantsearch/instantsearch.ts @@ -11,14 +11,149 @@ import { } from "@angular/core"; import { isPlatformBrowser } from "@angular/common"; +import * as algoliasearchProxy from "algoliasearch"; import instantsearch from "instantsearch.js/es"; import { Widget } from "../base-widget"; import { VERSION } from "../version"; +const algoliasearch = algoliasearchProxy.default || algoliasearchProxy; + +export type SearchRequest = { + indexName: string; + params: SearchRequestParameters; +}; + +export type SearchForFacetValuesRequest = { + indexName: string; + params: SearchForFacetValuesRequestParameters; +}; + +// Documentation: https://www.algolia.com/doc/api-reference/search-api-parameters/ +export type SearchParameters = { + // Attributes + attributesToRetrieve?: string[]; + restrictSearchableAttributes?: string[]; + + // Filtering + filters?: string; + facetFilters?: string[]; + optionalFilters?: string[]; + numericFilters?: string[]; + sumOrFiltersScores?: boolean; + + // Faceting + facets?: string[]; + maxValuesPerFacet?: number; + facetingAfterDistinct?: boolean; + sortFacetValuesBy?: string; + + // Highlighting / Snippeting + attributesToHighlight?: string[]; + attributesToSnippet?: string[]; + highlightPreTag?: string; + highlightPostTag?: string; + snippetEllipsisText?: string; + restrictHighlightAndSnippetArrays?: boolean; + + // Pagination + page?: number; + hitsPerPage?: number; + offset?: number; + length?: number; + + // Typos + minWordSizefor1Typo?: number; + minWordSizefor2Typos?: number; + typoTolerance?: string | boolean; + allowTyposOnNumericTokens?: boolean; + ignorePlurals?: boolean | string[]; + disableTypoToleranceOnAttributes?: string[]; + + // Geo-Search + aroundLatLng?: string; + aroundLatLngViaIP?: boolean; + aroundRadius?: number | "all"; + aroundPrecision?: number; + minimumAroundRadius?: number; + insideBoundingBox?: GeoRectangle | GeoRectangle[]; + insidePolygon?: GeoPolygon | GeoPolygon[]; + + // Query Strategy + queryType?: string; + removeWordsIfNoResults?: string; + advancedSyntax?: boolean; + optionalWords?: string | string[]; + removeStopWords?: boolean | string[]; + disableExactOnAttributes?: string[]; + exactOnSingleWordQuery?: string; + alternativesAsExact?: string[]; + + // Query Rules + enableRules?: boolean; + ruleContexts?: string[]; + + // Advanced + minProximity?: number; + responseFields?: string[]; + maxFacetHits?: number; + percentileComputation?: boolean; + distinct?: number | boolean; + getRankingInfo?: boolean; + clickAnalytics?: boolean; + analytics?: boolean; + analyticsTags?: string[]; + synonyms?: boolean; + replaceSynonymsInHighlight?: boolean; +}; + +export interface SearchRequestParameters extends SearchParameters { + query: string; +} + +export interface SearchForFacetValuesRequestParameters + extends SearchParameters { + facetQuery: string; + facetName: string; +} + +export type GeoRectangle = [number, number, number, number]; +export type GeoPolygon = [number, number, number, number, number, number]; + +// Documentation: https://www.algolia.com/doc/rest-api/search/?language=javascript#search-multiple-indexes +export type SearchResponse = { + hits: Hit[]; + page?: number; + nbHits?: number; + nbPages?: number; + hitsPerPage?: number; + processingTimeMS?: number; + query?: string; + params?: string; + index?: string; +}; + +export type Hit = { + _highlightResult?: object; +}; + +// Documentation: https://www.algolia.com/doc/rest-api/search/?language=javascript#search-for-facet-values +export type SearchForFacetValuesResponse = { + value: string; + highlighted?: string; + count?: number; +}; + +export type SearchClient = { + search: (requests: SearchRequest[]) => Promise<{ results: SearchResponse[] }>; + searchForFacetValues?: ( + requests: SearchForFacetValuesRequest[] + ) => Promise<{ facetHits: SearchForFacetValuesResponse[] }>; +}; + export type InstantSearchConfig = { - appId: string; - apiKey: string; + appId?: string; + apiKey?: string; indexName: string; numberLocale?: string; @@ -28,7 +163,8 @@ export type InstantSearchConfig = { appId: string, apiKey: string ) => object; - searchParameters?: object | void; + searchClient?: SearchClient; + searchParameters?: SearchParameters | void; urlSync?: | boolean | { @@ -106,12 +242,13 @@ export class NgAisInstantSearch implements AfterViewInit, OnInit, OnDestroy { } // custom algolia client agent - if (!config.createAlgoliaClient) { - config.createAlgoliaClient = (algoliasearch, appId, apiKey) => { - const client = algoliasearch(appId, apiKey); - client.addAlgoliaAgent(`angular-instantsearch ${VERSION}`); - return client; - }; + if (!config.searchClient && !config.createAlgoliaClient) { + const client = algoliasearch(config.appId, config.apiKey); + client.addAlgoliaAgent(`angular-instantsearch ${VERSION}`); + + config.searchClient = client; + config.appId = undefined; + config.apiKey = undefined; } this.instantSearchInstance = instantsearch(config); diff --git a/yarn.lock b/yarn.lock index 600bc75c0..ee5aec869 100644 --- a/yarn.lock +++ b/yarn.lock @@ -272,38 +272,18 @@ ajv@^6.1.0: json-schema-traverse "^0.3.0" uri-js "^3.0.2" -algoliasearch-helper@2.24.0, algoliasearch-helper@^2.23.0: - version "2.24.0" - resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-2.24.0.tgz#6ebf91683a82799bc4019ee1ad92d0ad0f41167b" +algoliasearch-helper@2.26.0, algoliasearch-helper@^2.26.0: + version "2.26.0" + resolved "https://registry.yarnpkg.com/algoliasearch-helper/-/algoliasearch-helper-2.26.0.tgz#cb784b692a5aacf17062493cb0b94f6d60d30d0f" dependencies: - events "^1.1.0" - lodash "^4.13.1" - qs "^6.2.1" + events "^1.1.1" + lodash "^4.17.5" + qs "^6.5.1" util "^0.10.3" -algoliasearch@3.25.1: - version "3.25.1" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.25.1.tgz#e543108b528e5c89338834473cb8fb082d13d11f" - dependencies: - agentkeepalive "^2.2.0" - debug "^2.6.8" - envify "^4.0.0" - es6-promise "^4.1.0" - events "^1.1.0" - foreach "^2.0.5" - global "^4.3.2" - inherits "^2.0.1" - isarray "^2.0.1" - load-script "^1.0.0" - object-keys "^1.0.11" - querystring-es3 "^0.2.1" - reduce "^1.0.1" - semver "^5.1.0" - tunnel-agent "^0.6.0" - -algoliasearch@^3.24.7: - version "3.26.0" - resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.26.0.tgz#5059cfe4b049ae1a1b9b7c25f5dbd3e75db6126a" +algoliasearch@3.27.0, algoliasearch@^3.27.0: + version "3.27.0" + resolved "https://registry.yarnpkg.com/algoliasearch/-/algoliasearch-3.27.0.tgz#675b7f2d186e5785a1553369b15d47b53d4efb31" dependencies: agentkeepalive "^2.2.0" debug "^2.6.8" @@ -3000,7 +2980,7 @@ events@1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/events/-/events-1.1.0.tgz#4b389fc200f910742ebff3abb2efe33690f45429" -events@^1.0.0, events@^1.1.0: +events@^1.0.0, events@^1.1.0, events@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/events/-/events-1.1.1.tgz#9ebdb7635ad099c70dcc4c2a1f5004288e8bd924" @@ -3971,12 +3951,12 @@ instantsearch.css@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/instantsearch.css/-/instantsearch.css-7.0.0.tgz#74fb9aa25ce64c80effc663fe92bc9ec785cc5e3" -instantsearch.js@^2.7.0: - version "2.7.0" - resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-2.7.0.tgz#7b94980f96e48dab67316ace8d4bdd29aee2ec7c" +instantsearch.js@^2.8.0-beta.1: + version "2.8.0-beta.1" + resolved "https://registry.yarnpkg.com/instantsearch.js/-/instantsearch.js-2.8.0-beta.1.tgz#f51cd7a506500cfaef119c7b8dcb1b69b2148c5d" dependencies: - algoliasearch "3.25.1" - algoliasearch-helper "2.24.0" + algoliasearch "3.27.0" + algoliasearch-helper "2.26.0" classnames "2.2.5" events "1.1.0" hogan.js "3.0.2" @@ -4948,6 +4928,10 @@ lodash@4.17.5, lodash@^4.13.1, lodash@^4.14.0, lodash@^4.17.2, lodash@^4.17.3, l version "4.17.5" resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.5.tgz#99a92d65c0272debe8c96b6057bc8fbfa3bed511" +lodash@^4.17.5: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.10.tgz#1b7793cf7259ea38fb3661d4d38b3260af8ae4e7" + loglevel@^1.4.1: version "1.6.1" resolved "https://registry.yarnpkg.com/loglevel/-/loglevel-1.6.1.tgz#e0fc95133b6ef276cdc8887cdaf24aa6f156f8fa" @@ -6187,7 +6171,7 @@ q@^1.1.2, q@^1.4.1, q@^1.5.1: version "1.5.1" resolved "https://registry.yarnpkg.com/q/-/q-1.5.1.tgz#7e32f75b41381291d04611f1bf14109ac00651d7" -qs@6.5.1, qs@^6.2.1, qs@^6.4.0, qs@~6.5.1: +qs@6.5.1, qs@^6.4.0, qs@^6.5.1, qs@~6.5.1: version "6.5.1" resolved "https://registry.yarnpkg.com/qs/-/qs-6.5.1.tgz#349cdf6eef89ec45c12d7d5eb3fc0c870343a6d8"