diff --git a/.eslintrc.js b/.eslintrc.js
index 65a2f36ef..6530cf849 100644
--- a/.eslintrc.js
+++ b/.eslintrc.js
@@ -61,10 +61,12 @@ module.exports = {
['@algolia/client-account', './packages/client-account/src'],
['@algolia/client-analytics', './packages/client-analytics/src'],
['@algolia/client-common', './packages/client-common/src'],
- ['@algolia/client-search', './packages/client-search/src'],
+ ['@algolia/client-personalization', './packages/client-personalization/src'],
['@algolia/client-recommendation', './packages/client-recommendation/src'],
+ ['@algolia/client-search', './packages/client-search/src'],
['@algolia/logger-common', './packages/logger-common/src'],
['@algolia/logger-console', './packages/logger-console/src'],
+ ['@algolia/recommend', './packages/recommend/src'],
['@algolia/requester-browser-xhr', './packages/requester-browser-xhr/src'],
['@algolia/requester-common', './packages/requester-common/src'],
['@algolia/requester-node-http', './packages/requester-node-http/src'],
diff --git a/README.md b/README.md
index c5d2a6047..8d23dc4d5 100644
--- a/README.md
+++ b/README.md
@@ -9,7 +9,7 @@
-
+
@@ -88,4 +88,4 @@ Encountering an issue? Before reaching out to support, we recommend heading to o
## 📄 License
-Algolia JavaScript API Client is an open-sourced software licensed under the [MIT license](LICENSE.txt).
+Algolia JavaScript API Client is an open-sourced software licensed under the [MIT license](LICENSE.md).
diff --git a/package.json b/package.json
index c3297dd33..fbd9d2796 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
+ "version": "4.9.3",
"private": true,
"license": "MIT",
- "version": "4.9.3",
"workspaces": [
"packages/*"
],
@@ -98,11 +98,15 @@
"bundlesize": [
{
"path": "packages/algoliasearch/dist/algoliasearch.umd.js",
- "maxSize": "7.85KB"
+ "maxSize": "7.95KB"
},
{
"path": "packages/algoliasearch/dist/algoliasearch-lite.umd.js",
"maxSize": "4.35KB"
+ },
+ {
+ "path": "packages/recommend/dist/recommend.umd.js",
+ "maxSize": "4.1KB"
}
]
}
diff --git a/packages/algoliasearch/README.md b/packages/algoliasearch/README.md
index 3a9d10485..9e227e857 100644
--- a/packages/algoliasearch/README.md
+++ b/packages/algoliasearch/README.md
@@ -9,7 +9,7 @@
-
+
@@ -79,4 +79,4 @@ For full documentation, visit the **[online documentation](https://www.algolia.c
## 📄 License
-Algolia JavaScript API Client is an open-sourced software licensed under the [MIT license](LICENSE.txt).
+Algolia JavaScript API Client is an open-sourced software licensed under the [MIT license](LICENSE.md).
diff --git a/packages/algoliasearch/package.json b/packages/algoliasearch/package.json
index d63721690..e9d6c354a 100644
--- a/packages/algoliasearch/package.json
+++ b/packages/algoliasearch/package.json
@@ -31,7 +31,7 @@
"@algolia/client-account": "4.9.3",
"@algolia/client-analytics": "4.9.3",
"@algolia/client-common": "4.9.3",
- "@algolia/client-recommendation": "4.9.3",
+ "@algolia/client-personalization": "4.9.3",
"@algolia/client-search": "4.9.3",
"@algolia/logger-common": "4.9.3",
"@algolia/logger-console": "4.9.3",
diff --git a/packages/algoliasearch/src/__tests__/default.test.ts b/packages/algoliasearch/src/__tests__/default.test.ts
index 3e37ab4b4..b86c866a0 100644
--- a/packages/algoliasearch/src/__tests__/default.test.ts
+++ b/packages/algoliasearch/src/__tests__/default.test.ts
@@ -83,7 +83,7 @@ describe('default preset', () => {
expect(client.transporter.requestsCache).not.toBe(cache);
expect(customClient.transporter.requestsCache).toBe(cache);
- expect(customClient.initRecommendation().transporter.requestsCache).not.toBe(cache);
+ expect(customClient.initPersonalization().transporter.requestsCache).not.toBe(cache);
expect(customClient.initAnalytics({ requestsCache: cache }).transporter.requestsCache).toBe(
cache
);
@@ -92,7 +92,7 @@ describe('default preset', () => {
read: 46,
write: 47,
});
- expect(customClient.initRecommendation().transporter.timeouts).toEqual({
+ expect(customClient.initPersonalization().transporter.timeouts).toEqual({
connect: testing.isBrowser() ? 1 : 2,
read: testing.isBrowser() ? 2 : 5,
write: 30,
@@ -110,7 +110,7 @@ describe('default preset', () => {
expect(customClient.transporter.hosts).toEqual([createStatelessHost({ url: 'foo.com' })]);
expect(customClient.initAnalytics().transporter.queryParameters).toEqual({});
- expect(customClient.initRecommendation().transporter.headers).toEqual({
+ expect(customClient.initPersonalization().transporter.headers).toEqual({
'content-type': 'application/json',
'x-algolia-application-id': 'appId',
'x-algolia-api-key': 'apiKey',
@@ -119,31 +119,31 @@ describe('default preset', () => {
createStatelessHost({ url: 'foo.com' }),
]);
- expect(customClient.initRecommendation().transporter.queryParameters).not.toEqual({
+ expect(customClient.initPersonalization().transporter.queryParameters).not.toEqual({
queryParameter: 'bar',
});
- expect(customClient.initRecommendation().transporter.headers).toEqual({
+ expect(customClient.initPersonalization().transporter.headers).toEqual({
'content-type': 'application/json',
'x-algolia-application-id': 'appId',
'x-algolia-api-key': 'apiKey',
});
- expect(customClient.initRecommendation().transporter.hosts).not.toEqual([
+ expect(customClient.initPersonalization().transporter.hosts).not.toEqual([
createStatelessHost({ url: 'foo.com' }),
]);
});
test('shared implementations between clients', () => {
const analytics = client.initAnalytics();
- const recommendation = client.initRecommendation();
+ const personalization = client.initPersonalization();
expect(analytics.transporter).not.toBe(client.transporter);
expect(analytics.transporter.hostsCache).toBe(client.transporter.hostsCache);
expect(analytics.transporter.userAgent).toBe(client.transporter.userAgent);
- expect(recommendation.transporter).not.toBe(client.transporter);
- expect(recommendation.transporter.hostsCache).toBe(client.transporter.hostsCache);
- expect(recommendation.transporter.userAgent).toBe(client.transporter.userAgent);
+ expect(personalization.transporter).not.toBe(client.transporter);
+ expect(personalization.transporter.hostsCache).toBe(client.transporter.hostsCache);
+ expect(personalization.transporter.userAgent).toBe(client.transporter.userAgent);
});
test('allows clients to override credentials', () => {
@@ -154,12 +154,12 @@ describe('default preset', () => {
const analytics = clientWithOptions.initAnalytics({
apiKey: 'analytics',
});
- const recommendation = clientWithOptions.initRecommendation({
- apiKey: 'recommendation',
+ const personalization = clientWithOptions.initPersonalization({
+ apiKey: 'personalization',
});
expect(analytics.transporter.headers['x-algolia-api-key']).toBe('analytics');
- expect(recommendation.transporter.headers['x-algolia-api-key']).toBe('recommendation');
+ expect(personalization.transporter.headers['x-algolia-api-key']).toBe('personalization');
});
test('allows clients to keep default credentials', () => {
@@ -168,10 +168,10 @@ describe('default preset', () => {
expect(clientWithOptions.transporter.headers['x-algolia-api-key']).toBe('apiKey');
const analytics = clientWithOptions.initAnalytics();
- const recommendation = clientWithOptions.initRecommendation();
+ const personalization = clientWithOptions.initPersonalization();
expect(analytics.transporter.headers['x-algolia-api-key']).toBe('apiKey');
- expect(recommendation.transporter.headers['x-algolia-api-key']).toBe('apiKey');
+ expect(personalization.transporter.headers['x-algolia-api-key']).toBe('apiKey');
});
it('can be destroyed', () => {
diff --git a/packages/algoliasearch/src/builds/browser.ts b/packages/algoliasearch/src/builds/browser.ts
index cb7bb484b..232fce240 100644
--- a/packages/algoliasearch/src/builds/browser.ts
+++ b/packages/algoliasearch/src/builds/browser.ts
@@ -19,14 +19,14 @@ import {
} from '@algolia/client-analytics';
import { version, WaitablePromise } from '@algolia/client-common';
import {
- createRecommendationClient,
+ createPersonalizationClient,
getPersonalizationStrategy,
GetPersonalizationStrategyResponse,
+ PersonalizationClient as BasePersonalizationClient,
PersonalizationStrategy,
- RecommendationClient as BaseRecommendationClient,
setPersonalizationStrategy,
SetPersonalizationStrategyResponse,
-} from '@algolia/client-recommendation';
+} from '@algolia/client-personalization';
import {
addApiKey,
AddApiKeyOptions,
@@ -192,7 +192,7 @@ import { createConsoleLogger } from '@algolia/logger-console';
import { createBrowserXhrRequester } from '@algolia/requester-browser-xhr';
import { createUserAgent, RequestOptions } from '@algolia/transporter';
-import { AlgoliaSearchOptions, InitAnalyticsOptions, InitRecommendationOptions } from '../types';
+import { AlgoliaSearchOptions, InitAnalyticsOptions, InitPersonalizationOptions } from '../types';
export default function algoliasearch(
appId: string,
@@ -219,10 +219,22 @@ export default function algoliasearch(
}),
userAgent: createUserAgent(version).add({ segment: 'Browser' }),
};
+ const searchClientOptions = { ...commonOptions, ...options };
+ const initPersonalization = () => (
+ clientOptions?: InitPersonalizationOptions
+ ): PersonalizationClient => {
+ return createPersonalizationClient({
+ ...commonOptions,
+ ...clientOptions,
+ methods: {
+ getPersonalizationStrategy,
+ setPersonalizationStrategy,
+ },
+ });
+ };
return createSearchClient({
- ...commonOptions,
- ...options,
+ ...searchClientOptions,
methods: {
search: multipleQueries,
searchForFacetValues: multipleSearchForFacetValues,
@@ -319,17 +331,15 @@ export default function algoliasearch(
},
});
},
+ initPersonalization,
initRecommendation: () => (
- clientOptions?: InitRecommendationOptions
- ): RecommendationClient => {
- return createRecommendationClient({
- ...commonOptions,
- ...clientOptions,
- methods: {
- getPersonalizationStrategy,
- setPersonalizationStrategy,
- },
- });
+ clientOptions?: InitPersonalizationOptions
+ ): PersonalizationClient => {
+ searchClientOptions.logger.info(
+ 'The `initRecommendation` method is deprecated. Use `initPersonalization` instead.'
+ );
+
+ return initPersonalization()(clientOptions);
},
},
});
@@ -338,7 +348,7 @@ export default function algoliasearch(
// eslint-disable-next-line functional/immutable-data
algoliasearch.version = version;
-export type RecommendationClient = BaseRecommendationClient & {
+export type PersonalizationClient = BasePersonalizationClient & {
readonly getPersonalizationStrategy: (
requestOptions?: RequestOptions
) => Readonly>;
@@ -348,6 +358,11 @@ export type RecommendationClient = BaseRecommendationClient & {
) => Readonly>;
};
+/**
+ * @deprecated Use `PersonalizationClient` instead.
+ */
+export type RecommendationClient = PersonalizationClient;
+
export type AnalyticsClient = BaseAnalyticsClient & {
readonly addABTest: (
abTest: ABTest,
@@ -663,7 +678,11 @@ export type SearchClient = BaseSearchClient & {
requestOptions?: RequestOptions
) => Readonly>;
readonly initAnalytics: (options?: InitAnalyticsOptions) => AnalyticsClient;
- readonly initRecommendation: (options?: InitRecommendationOptions) => RecommendationClient;
+ readonly initPersonalization: (options?: InitPersonalizationOptions) => PersonalizationClient;
+ /**
+ * @deprecated Use `initPersonalization` instead.
+ */
+ readonly initRecommendation: (options?: InitPersonalizationOptions) => PersonalizationClient;
};
export * from '../types';
diff --git a/packages/algoliasearch/src/builds/node.ts b/packages/algoliasearch/src/builds/node.ts
index 439605f04..4c4c37377 100644
--- a/packages/algoliasearch/src/builds/node.ts
+++ b/packages/algoliasearch/src/builds/node.ts
@@ -18,14 +18,14 @@ import {
} from '@algolia/client-analytics';
import { destroy, version, WaitablePromise } from '@algolia/client-common';
import {
- createRecommendationClient,
+ createPersonalizationClient,
getPersonalizationStrategy,
GetPersonalizationStrategyResponse,
+ PersonalizationClient as BasePersonalizationClient,
PersonalizationStrategy,
- RecommendationClient as BaseRecommendationClient,
setPersonalizationStrategy,
SetPersonalizationStrategyResponse,
-} from '@algolia/client-recommendation';
+} from '@algolia/client-personalization';
import {
addApiKey,
AddApiKeyOptions,
@@ -194,7 +194,7 @@ import { Destroyable } from '@algolia/requester-common';
import { createNodeHttpRequester } from '@algolia/requester-node-http';
import { createUserAgent, RequestOptions } from '@algolia/transporter';
-import { AlgoliaSearchOptions, InitAnalyticsOptions, InitRecommendationOptions } from '../types';
+import { AlgoliaSearchOptions, InitAnalyticsOptions, InitPersonalizationOptions } from '../types';
export default function algoliasearch(
appId: string,
@@ -219,10 +219,22 @@ export default function algoliasearch(
version: process.versions.node,
}),
};
+ const searchClientOptions = { ...commonOptions, ...options };
+ const initPersonalization = () => (
+ clientOptions?: InitPersonalizationOptions
+ ): PersonalizationClient => {
+ return createPersonalizationClient({
+ ...commonOptions,
+ ...clientOptions,
+ methods: {
+ getPersonalizationStrategy,
+ setPersonalizationStrategy,
+ },
+ });
+ };
return createSearchClient({
- ...commonOptions,
- ...options,
+ ...searchClientOptions,
methods: {
search: multipleQueries,
searchForFacetValues: multipleSearchForFacetValues,
@@ -322,17 +334,15 @@ export default function algoliasearch(
},
});
},
+ initPersonalization,
initRecommendation: () => (
- clientOptions?: InitRecommendationOptions
- ): RecommendationClient => {
- return createRecommendationClient({
- ...commonOptions,
- ...clientOptions,
- methods: {
- getPersonalizationStrategy,
- setPersonalizationStrategy,
- },
- });
+ clientOptions?: InitPersonalizationOptions
+ ): PersonalizationClient => {
+ searchClientOptions.logger.info(
+ 'The `initRecommendation` method is deprecated. Use `initPersonalization` instead.'
+ );
+
+ return initPersonalization()(clientOptions);
},
},
});
@@ -341,7 +351,7 @@ export default function algoliasearch(
// eslint-disable-next-line functional/immutable-data
algoliasearch.version = version;
-export type RecommendationClient = BaseRecommendationClient & {
+export type PersonalizationClient = BasePersonalizationClient & {
readonly getPersonalizationStrategy: (
requestOptions?: RequestOptions
) => Readonly>;
@@ -351,6 +361,11 @@ export type RecommendationClient = BaseRecommendationClient & {
) => Readonly>;
};
+/**
+ * @deprecated Use `PersonalizationClient` instead.
+ */
+export type RecommendationClient = PersonalizationClient;
+
export type AnalyticsClient = BaseAnalyticsClient & {
readonly addABTest: (
abTest: ABTest,
@@ -671,7 +686,11 @@ export type SearchClient = BaseSearchClient & {
requestOptions?: RequestOptions
) => Readonly>;
readonly initAnalytics: (options?: InitAnalyticsOptions) => AnalyticsClient;
- readonly initRecommendation: (options?: InitRecommendationOptions) => RecommendationClient;
+ readonly initPersonalization: (options?: InitPersonalizationOptions) => PersonalizationClient;
+ /**
+ * @deprecated Use `initPersonalization` instead.
+ */
+ readonly initRecommendation: (options?: InitPersonalizationOptions) => PersonalizationClient;
} & Destroyable;
export * from '../types';
diff --git a/packages/algoliasearch/src/types/AlgoliaSearchOptions.ts b/packages/algoliasearch/src/types/AlgoliaSearchOptions.ts
index 61e9cc81c..70ee0522a 100644
--- a/packages/algoliasearch/src/types/AlgoliaSearchOptions.ts
+++ b/packages/algoliasearch/src/types/AlgoliaSearchOptions.ts
@@ -1,6 +1,6 @@
import { AnalyticsClientOptions } from '@algolia/client-analytics';
import { ClientTransporterOptions } from '@algolia/client-common';
-import { RecommendationClientOptions } from '@algolia/client-recommendation';
+import { PersonalizationClientOptions } from '@algolia/client-personalization';
import { SearchClientOptions } from '@algolia/client-search';
type Credentials = { readonly appId: string; readonly apiKey: string };
@@ -20,5 +20,10 @@ export type AlgoliaSearchOptions = Partial &
export type InitAnalyticsOptions = Partial &
OptionalCredentials;
-export type InitRecommendationOptions = Partial &
- OptionalCredentials;
+export type InitPersonalizationOptions = Partial &
+ OptionalCredentials;
+
+/**
+ * @deprecated Use `InitPersonalizationOptions` instead.
+ */
+export type InitRecommendationOptions = InitPersonalizationOptions;
diff --git a/packages/client-common/src/__tests__/TestSuite.ts b/packages/client-common/src/__tests__/TestSuite.ts
index 8ddbae2c1..b33f3a79b 100644
--- a/packages/client-common/src/__tests__/TestSuite.ts
+++ b/packages/client-common/src/__tests__/TestSuite.ts
@@ -12,6 +12,8 @@ import { addMethods } from '..';
import algoliasearchForBrowser from '../../../algoliasearch/src/builds/browser';
import algoliasearchForBrowserLite from '../../../algoliasearch/src/builds/browserLite';
import algoliasearchForNode from '../../../algoliasearch/src/builds/node';
+import recommendForBrowser from '../../../recommend/src/builds/browser';
+import recommendForNode from '../../../recommend/src/builds/node';
/* eslint functional/no-class: 0 */
export class TestSuite {
@@ -29,6 +31,11 @@ export class TestSuite {
? algoliasearchForBrowser
: algoliasearchForNode;
+ // @ts-ignore `destroy` only exists on the Node build
+ public readonly recommend: typeof recommendForNode = this.isBrowser
+ ? recommendForBrowser
+ : recommendForNode;
+
public indicesCount = 0;
public constructor(testName?: string) {
@@ -68,13 +75,6 @@ export class TestSuite {
return client;
}
- public makeRecommendationClient(
- appIdEnv: string = 'ALGOLIA_APPLICATION_ID_1',
- apiKeyEnv: string = 'ALGOLIA_ADMIN_KEY_1'
- ) {
- return this.makeSearchClient(appIdEnv, apiKeyEnv).initRecommendation();
- }
-
public makeIndex(indexName?: string) {
const index = this.makeSearchClient().initIndex(indexName || this.makeIndexName());
diff --git a/packages/client-personalization/api-extractor.json b/packages/client-personalization/api-extractor.json
new file mode 100644
index 000000000..d182b70fb
--- /dev/null
+++ b/packages/client-personalization/api-extractor.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../api-extractor.json",
+ "mainEntryPointFilePath": "./dist/packages//src/index.d.ts",
+ "dtsRollup": {
+ "untrimmedFilePath": "./dist/.d.ts"
+ }
+}
diff --git a/packages/client-personalization/index.js b/packages/client-personalization/index.js
new file mode 100644
index 000000000..df07d60ae
--- /dev/null
+++ b/packages/client-personalization/index.js
@@ -0,0 +1,2 @@
+// eslint-disable-next-line functional/immutable-data, import/no-commonjs
+module.exports = require('./dist/client-personalization.cjs.js');
diff --git a/packages/client-personalization/package.json b/packages/client-personalization/package.json
new file mode 100644
index 000000000..f07b400b6
--- /dev/null
+++ b/packages/client-personalization/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@algolia/client-personalization",
+ "version": "4.9.3",
+ "private": false,
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/algolia/algoliasearch-client-javascript.git"
+ },
+ "license": "MIT",
+ "sideEffects": false,
+ "main": "index.js",
+ "module": "dist/client-personalization.esm.js",
+ "types": "dist/client-personalization.d.ts",
+ "files": [
+ "index.js",
+ "dist"
+ ],
+ "dependencies": {
+ "@algolia/client-common": "4.9.3",
+ "@algolia/requester-common": "4.9.3",
+ "@algolia/transporter": "4.9.3"
+ }
+}
diff --git a/packages/client-personalization/src/__tests__/features/personalization-strategy.test.ts b/packages/client-personalization/src/__tests__/features/personalization-strategy.test.ts
new file mode 100644
index 000000000..cbb3cefb1
--- /dev/null
+++ b/packages/client-personalization/src/__tests__/features/personalization-strategy.test.ts
@@ -0,0 +1,58 @@
+import { PersonalizationStrategy } from '../..';
+import { TestSuite } from '../../../../client-common/src/__tests__/TestSuite';
+import { SetPersonalizationStrategyResponse } from '../../types';
+
+const testSuite = new TestSuite('personalization_strategy');
+
+test(testSuite.testName, async () => {
+ // On the CI, we parallelize too many calls to this endpoint. Making
+ // the API slow to respond, and reaching a timeout. These specific
+ // timeouts should ensure the test suite works as expected.
+ const client = testSuite.makeSearchClient().initRecommendation({
+ timeouts: {
+ connect: 30,
+ read: 30,
+ write: 30,
+ },
+ });
+
+ const personalizationStrategy: PersonalizationStrategy = {
+ eventsScoring: [
+ {
+ eventName: 'Add to cart',
+ eventType: 'conversion',
+ score: 50,
+ },
+ {
+ eventName: 'Purchase',
+ eventType: 'conversion',
+ score: 100,
+ },
+ ],
+ facetsScoring: [
+ { facetName: 'brand', score: 100 },
+ { facetName: 'categories', score: 10 },
+ ],
+ personalizationImpact: 0,
+ };
+
+ const successResponse: SetPersonalizationStrategyResponse = {
+ status: 200,
+ message: 'Strategy was successfully updated',
+ };
+
+ const errorResponse: SetPersonalizationStrategyResponse = {
+ status: 429,
+ message: 'Number of strategy saves exceeded for the day',
+ };
+
+ try {
+ const response = await client.setPersonalizationStrategy(personalizationStrategy);
+ expect(response).toEqual(successResponse);
+ } catch (error) {
+ // eslint-disable-next-line jest/no-try-expect
+ expect(error).toEqual(expect.objectContaining({ name: 'ApiError', ...errorResponse }));
+ }
+
+ await expect(client.getPersonalizationStrategy()).resolves.toEqual(personalizationStrategy);
+});
diff --git a/packages/client-personalization/src/__tests__/unit/personalization-client.test.ts b/packages/client-personalization/src/__tests__/unit/personalization-client.test.ts
new file mode 100644
index 000000000..801c27930
--- /dev/null
+++ b/packages/client-personalization/src/__tests__/unit/personalization-client.test.ts
@@ -0,0 +1,84 @@
+import { MethodEnum } from '@algolia/requester-common';
+import { anything, deepEqual, spy, verify, when } from 'ts-mockito';
+
+import { TestSuite } from '../../../../client-common/src/__tests__/TestSuite';
+import { PersonalizationStrategy, SetPersonalizationStrategyResponse } from '../../types';
+
+const personalizationClient = new TestSuite()
+ .algoliasearch('appId', 'apiKey')
+ .initPersonalization();
+
+describe('personalization client', () => {
+ it('uses region to define the host', () => {
+ expect(personalizationClient.transporter.hosts[0].url).toBe('personalization.us.algolia.com');
+ });
+
+ it('sets default headers', () => {
+ expect(personalizationClient.transporter.headers).toEqual({
+ 'content-type': 'application/json',
+ 'x-algolia-application-id': 'appId',
+ 'x-algolia-api-key': 'apiKey',
+ });
+
+ expect(personalizationClient.transporter.queryParameters).toEqual({});
+ });
+});
+
+describe('personalization', () => {
+ const personalizationStrategy: PersonalizationStrategy = {
+ eventsScoring: [
+ { eventName: 'Add to cart', eventType: 'conversion', score: 50 },
+ { eventName: 'Purchase', eventType: 'conversion', score: 100 },
+ ],
+ facetsScoring: [
+ { facetName: 'brand', score: 100 },
+ { facetName: 'categories', score: 10 },
+ ],
+ personalizationImpact: 0,
+ };
+
+ it('set personalization strategy', async () => {
+ const transporterMock = spy(personalizationClient.transporter);
+ const response: SetPersonalizationStrategyResponse = {
+ status: 200,
+ message: 'Strategy was successfully updated',
+ };
+ when(transporterMock.write(anything(), anything())).thenResolve(response);
+
+ const setPersonalizationStrategyResponse = await personalizationClient.setPersonalizationStrategy(
+ personalizationStrategy,
+ { foo: 'bar' }
+ );
+ expect(setPersonalizationStrategyResponse).toEqual(response);
+
+ verify(
+ transporterMock.write(
+ deepEqual({
+ method: MethodEnum.Post,
+ path: '1/strategies/personalization',
+ data: personalizationStrategy,
+ }),
+ deepEqual({ foo: 'bar' })
+ )
+ ).once();
+ });
+
+ it('get personalization strategy', async () => {
+ const transporterMock = spy(personalizationClient.transporter);
+ when(transporterMock.read(anything(), anything())).thenResolve(personalizationStrategy);
+
+ const getPersonalizationStrategyResponse = await personalizationClient.getPersonalizationStrategy();
+
+ verify(
+ transporterMock.read(
+ deepEqual({
+ method: MethodEnum.Get,
+ path: '1/strategies/personalization',
+ }),
+ anything()
+ )
+ ).once();
+
+ expect(getPersonalizationStrategyResponse).toEqual(personalizationStrategy);
+ });
+});
diff --git a/packages/client-personalization/src/createPersonalizationClient.ts b/packages/client-personalization/src/createPersonalizationClient.ts
new file mode 100644
index 000000000..2fc37f4a3
--- /dev/null
+++ b/packages/client-personalization/src/createPersonalizationClient.ts
@@ -0,0 +1,35 @@
+import {
+ addMethods,
+ AuthMode,
+ ClientTransporterOptions,
+ createAuth,
+ CreateClient,
+} from '@algolia/client-common';
+import { createTransporter } from '@algolia/transporter';
+
+import { PersonalizationClient, PersonalizationClientOptions } from '.';
+
+export const createPersonalizationClient: CreateClient<
+ PersonalizationClient,
+ PersonalizationClientOptions & ClientTransporterOptions
+> = options => {
+ const region = options.region || 'us';
+ const auth = createAuth(AuthMode.WithinHeaders, options.appId, options.apiKey);
+
+ const transporter = createTransporter({
+ hosts: [{ url: `personalization.${region}.algolia.com` }],
+ ...options,
+ headers: {
+ ...auth.headers(),
+ ...{ 'content-type': 'application/json' },
+ ...options.headers,
+ },
+
+ queryParameters: {
+ ...auth.queryParameters(),
+ ...options.queryParameters,
+ },
+ });
+
+ return addMethods({ appId: options.appId, transporter }, options.methods);
+};
diff --git a/packages/client-personalization/src/index.ts b/packages/client-personalization/src/index.ts
new file mode 100644
index 000000000..18b3a1b76
--- /dev/null
+++ b/packages/client-personalization/src/index.ts
@@ -0,0 +1,7 @@
+/**
+ * @file Automatically generated by barrelsby.
+ */
+
+export * from './createPersonalizationClient';
+export * from './methods/index';
+export * from './types/index';
diff --git a/packages/client-personalization/src/methods/getPersonalizationStrategy.ts b/packages/client-personalization/src/methods/getPersonalizationStrategy.ts
new file mode 100644
index 000000000..8a96147d3
--- /dev/null
+++ b/packages/client-personalization/src/methods/getPersonalizationStrategy.ts
@@ -0,0 +1,18 @@
+import { MethodEnum } from '@algolia/requester-common';
+import { RequestOptions } from '@algolia/transporter';
+
+import { GetPersonalizationStrategyResponse, PersonalizationClient } from '..';
+
+export const getPersonalizationStrategy = (base: PersonalizationClient) => {
+ return (
+ requestOptions?: RequestOptions
+ ): Readonly> => {
+ return base.transporter.read(
+ {
+ method: MethodEnum.Get,
+ path: '1/strategies/personalization',
+ },
+ requestOptions
+ );
+ };
+};
diff --git a/packages/client-personalization/src/methods/index.ts b/packages/client-personalization/src/methods/index.ts
new file mode 100644
index 000000000..c64d44a9a
--- /dev/null
+++ b/packages/client-personalization/src/methods/index.ts
@@ -0,0 +1,6 @@
+/**
+ * @file Automatically generated by barrelsby.
+ */
+
+export * from './getPersonalizationStrategy';
+export * from './setPersonalizationStrategy';
diff --git a/packages/client-personalization/src/methods/setPersonalizationStrategy.ts b/packages/client-personalization/src/methods/setPersonalizationStrategy.ts
new file mode 100644
index 000000000..846d8c954
--- /dev/null
+++ b/packages/client-personalization/src/methods/setPersonalizationStrategy.ts
@@ -0,0 +1,24 @@
+import { MethodEnum } from '@algolia/requester-common';
+import { RequestOptions } from '@algolia/transporter';
+
+import {
+ PersonalizationClient,
+ PersonalizationStrategy,
+ SetPersonalizationStrategyResponse,
+} from '..';
+
+export const setPersonalizationStrategy = (base: PersonalizationClient) => {
+ return (
+ personalizationStrategy: PersonalizationStrategy,
+ requestOptions?: RequestOptions
+ ): Readonly> => {
+ return base.transporter.write(
+ {
+ method: MethodEnum.Post,
+ path: '1/strategies/personalization',
+ data: personalizationStrategy,
+ },
+ requestOptions
+ );
+ };
+};
diff --git a/packages/client-personalization/src/types/GetPersonalizationStrategyResponse.ts b/packages/client-personalization/src/types/GetPersonalizationStrategyResponse.ts
new file mode 100644
index 000000000..d0ae3b865
--- /dev/null
+++ b/packages/client-personalization/src/types/GetPersonalizationStrategyResponse.ts
@@ -0,0 +1,23 @@
+export type GetPersonalizationStrategyResponse = {
+ /**
+ * Events scoring
+ */
+ eventsScoring: Array<{
+ eventName: string;
+ eventType: string;
+ score: number;
+ }>;
+
+ /**
+ * Facets scoring
+ */
+ facetsScoring: Array<{
+ facetName: string;
+ score: number;
+ }>;
+
+ /**
+ * Personalization impact
+ */
+ personalizationImpact: number;
+};
diff --git a/packages/client-personalization/src/types/PersonalizationClient.ts b/packages/client-personalization/src/types/PersonalizationClient.ts
new file mode 100644
index 000000000..87235c140
--- /dev/null
+++ b/packages/client-personalization/src/types/PersonalizationClient.ts
@@ -0,0 +1,13 @@
+import { Transporter } from '@algolia/transporter';
+
+export type PersonalizationClient = {
+ /**
+ * The application id.
+ */
+ readonly appId: string;
+
+ /**
+ * The underlying transporter.
+ */
+ readonly transporter: Transporter;
+};
diff --git a/packages/client-personalization/src/types/PersonalizationClientOptions.ts b/packages/client-personalization/src/types/PersonalizationClientOptions.ts
new file mode 100644
index 000000000..6539401df
--- /dev/null
+++ b/packages/client-personalization/src/types/PersonalizationClientOptions.ts
@@ -0,0 +1,16 @@
+export type PersonalizationClientOptions = {
+ /**
+ * The application id.
+ */
+ readonly appId: string;
+
+ /**
+ * The api key.
+ */
+ readonly apiKey: string;
+
+ /**
+ * The prefered region.
+ */
+ readonly region?: string;
+};
diff --git a/packages/client-personalization/src/types/PersonalizationStrategy.ts b/packages/client-personalization/src/types/PersonalizationStrategy.ts
new file mode 100644
index 000000000..4af503cdc
--- /dev/null
+++ b/packages/client-personalization/src/types/PersonalizationStrategy.ts
@@ -0,0 +1,23 @@
+export type PersonalizationStrategy = {
+ /**
+ * Events scoring
+ */
+ readonly eventsScoring: ReadonlyArray<{
+ readonly eventName: string;
+ readonly eventType: string;
+ readonly score: number;
+ }>;
+
+ /**
+ * Facets scoring
+ */
+ readonly facetsScoring: ReadonlyArray<{
+ readonly facetName: string;
+ readonly score: number;
+ }>;
+
+ /**
+ * Personalization impact
+ */
+ readonly personalizationImpact: number;
+};
diff --git a/packages/client-personalization/src/types/SetPersonalizationStrategyResponse.ts b/packages/client-personalization/src/types/SetPersonalizationStrategyResponse.ts
new file mode 100644
index 000000000..f005b29c8
--- /dev/null
+++ b/packages/client-personalization/src/types/SetPersonalizationStrategyResponse.ts
@@ -0,0 +1,11 @@
+export type SetPersonalizationStrategyResponse = {
+ /**
+ * The status code.
+ */
+ status?: number;
+
+ /**
+ * The message.
+ */
+ message: string;
+};
diff --git a/packages/client-personalization/src/types/index.ts b/packages/client-personalization/src/types/index.ts
new file mode 100644
index 000000000..83c20c8c7
--- /dev/null
+++ b/packages/client-personalization/src/types/index.ts
@@ -0,0 +1,9 @@
+/**
+ * @file Automatically generated by barrelsby.
+ */
+
+export * from './GetPersonalizationStrategyResponse';
+export * from './PersonalizationStrategy';
+export * from './PersonalizationClient';
+export * from './PersonalizationClientOptions';
+export * from './SetPersonalizationStrategyResponse';
diff --git a/packages/client-recommendation/package.json b/packages/client-recommendation/package.json
index 8b8c82a6a..7cf9c280c 100644
--- a/packages/client-recommendation/package.json
+++ b/packages/client-recommendation/package.json
@@ -17,7 +17,7 @@
],
"dependencies": {
"@algolia/client-common": "4.9.3",
- "@algolia/requester-common": "4.9.3",
- "@algolia/transporter": "4.9.3"
+ "@algolia/client-personalization": "4.9.3",
+ "@algolia/requester-common": "4.9.3"
}
}
diff --git a/packages/client-recommendation/src/__tests__/unit/recommendation-client.test.ts b/packages/client-recommendation/src/__tests__/unit/recommendation-client.test.ts
index ef79956cc..1cf32617b 100644
--- a/packages/client-recommendation/src/__tests__/unit/recommendation-client.test.ts
+++ b/packages/client-recommendation/src/__tests__/unit/recommendation-client.test.ts
@@ -4,11 +4,25 @@ import { anything, deepEqual, spy, verify, when } from 'ts-mockito';
import { TestSuite } from '../../../../client-common/src/__tests__/TestSuite';
import { PersonalizationStrategy, SetPersonalizationStrategyResponse } from '../../types';
-const recommendationClient = new TestSuite().algoliasearch('appId', 'apiKey').initRecommendation();
+const searchClient = new TestSuite().algoliasearch('appId', 'apiKey', {
+ logger: {
+ debug: jest.fn(),
+ info: jest.fn(),
+ error: jest.fn(),
+ },
+});
+const recommendationClient = searchClient.initRecommendation();
describe('recommendation client', () => {
+ it('logs a deprecation message', () => {
+ expect(searchClient.transporter.logger.info).toHaveBeenCalledTimes(1);
+ expect(searchClient.transporter.logger.info).toHaveBeenCalledWith(
+ 'The `initRecommendation` method is deprecated. Use `initPersonalization` instead.'
+ );
+ });
+
it('uses region to define the host', () => {
- expect(recommendationClient.transporter.hosts[0].url).toBe('recommendation.us.algolia.com');
+ expect(recommendationClient.transporter.hosts[0].url).toBe('personalization.us.algolia.com');
});
it('sets default headers', () => {
diff --git a/packages/client-recommendation/src/createRecommendationClient.ts b/packages/client-recommendation/src/createRecommendationClient.ts
index cfd38ff9a..15bcb3e93 100644
--- a/packages/client-recommendation/src/createRecommendationClient.ts
+++ b/packages/client-recommendation/src/createRecommendationClient.ts
@@ -1,35 +1,21 @@
-import {
- addMethods,
- AuthMode,
- ClientTransporterOptions,
- createAuth,
- CreateClient,
-} from '@algolia/client-common';
-import { createTransporter } from '@algolia/transporter';
+import { ClientTransporterOptions, CreateClient } from '@algolia/client-common';
+import { createPersonalizationClient } from '@algolia/client-personalization';
import { RecommendationClient, RecommendationClientOptions } from '.';
+/**
+ * @deprecated The `@algolia/client-recommendation` package is deprecated and you should use `@algolia/client-personalization` instead. To migrate, install the new package and replace `createRecommendationClient` with `createPersonalizationClient`.
+ */
export const createRecommendationClient: CreateClient<
RecommendationClient,
RecommendationClientOptions & ClientTransporterOptions
> = options => {
- const region = options.region || 'us';
- const auth = createAuth(AuthMode.WithinHeaders, options.appId, options.apiKey);
+ /* eslint-disable max-len */
+ options.logger.info(
+ 'The `@algolia/client-recommendation` package is deprecated and you should use `@algolia/client-personalization` instead.\n' +
+ 'To migrate, install the new package and replace `createRecommendationClient` with `createPersonalizationClient`.'
+ );
+ /* eslint-enable max-len */
- const transporter = createTransporter({
- hosts: [{ url: `recommendation.${region}.algolia.com` }],
- ...options,
- headers: {
- ...auth.headers(),
- ...{ 'content-type': 'application/json' },
- ...options.headers,
- },
-
- queryParameters: {
- ...auth.queryParameters(),
- ...options.queryParameters,
- },
- });
-
- return addMethods({ appId: options.appId, transporter }, options.methods);
+ return createPersonalizationClient(options);
};
diff --git a/packages/client-search/src/types/ApiKeyType.ts b/packages/client-search/src/types/ApiKeyType.ts
index 155ace2d3..093e12e6c 100644
--- a/packages/client-search/src/types/ApiKeyType.ts
+++ b/packages/client-search/src/types/ApiKeyType.ts
@@ -7,6 +7,7 @@ export const ApiKeyACLEnum: Readonly> = {
EditSettings: 'editSettings',
ListIndexes: 'listIndexes',
Logs: 'logs',
+ Personalization: 'personalization',
Recommendation: 'recommendation',
Search: 'search',
SeeUnretrievableAttributes: 'seeUnretrievableAttributes',
@@ -23,6 +24,7 @@ export type ApiKeyACLType =
| 'editSettings'
| 'listIndexes'
| 'logs'
+ | 'personalization'
| 'recommendation'
| 'search'
| 'seeUnretrievableAttributes'
diff --git a/packages/recommend/README.md b/packages/recommend/README.md
new file mode 100644
index 000000000..1cb2198eb
--- /dev/null
+++ b/packages/recommend/README.md
@@ -0,0 +1,72 @@
+
+
Algolia Recommend
+
+ The perfect starting point to integrate Algolia Recommend within your JavaScript project
+
+
+
+
+
+
+
+
+ Documentation •
+ UI library •
+ Community Forum •
+ Stack Overflow •
+ Report a bug •
+ Support
+
+
+## ✨ Features
+
+- Thin & **minimal low-level HTTP client** to interact with Algolia's Recommend API
+- Works both on the **browser** and **node.js**
+- **UMD compatible**, you can use it with any module loader
+- Built with TypeScript
+
+## 💡 Getting Started
+
+First, install Algolia Recommend API Client via the [npm](https://www.npmjs.com/get-npm) package manager:
+
+```bash
+npm install @algolia/recommend
+```
+
+Then, let's retrieve recommendations:
+
+```js
+const algoliarecommend = require('@algolia/recommend');
+
+const client = algoliarecommend('YourApplicationID', 'YourAdminAPIKey');
+
+client
+ .getFrequentlyBoughtTogether({
+ indexName: 'your_index_name',
+ objectID: 'your_object_id',
+ })
+ .then(({ results }) => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+
+client
+ .getRelatedProducts({
+ indexName: 'your_index_name',
+ objectID: 'your_object_id',
+ })
+ .then(({ results }) => {
+ console.log(results);
+ })
+ .catch(err => {
+ console.log(err);
+ });
+```
+
+For full documentation, visit the **[online documentation](https://www.algolia.com/doc/api-client/methods/recommend/)**.
+
+## 📄 License
+
+Algolia Recommend API Client is an open-sourced software licensed under the [MIT license](LICENSE.md).
diff --git a/packages/recommend/api-extractor.json b/packages/recommend/api-extractor.json
new file mode 100644
index 000000000..a522871c3
--- /dev/null
+++ b/packages/recommend/api-extractor.json
@@ -0,0 +1,7 @@
+{
+ "extends": "../../api-extractor.json",
+ "mainEntryPointFilePath": "./dist/packages/recommend/src/builds/node.d.ts",
+ "dtsRollup": {
+ "untrimmedFilePath": "./dist/recommend.d.ts"
+ }
+}
diff --git a/packages/recommend/index.d.ts b/packages/recommend/index.d.ts
new file mode 100644
index 000000000..70d1e5942
--- /dev/null
+++ b/packages/recommend/index.d.ts
@@ -0,0 +1,3 @@
+/* eslint-disable import/no-unresolved*/
+export * from './dist/recommend';
+export { default } from './dist/recommend';
diff --git a/packages/recommend/index.js b/packages/recommend/index.js
new file mode 100644
index 000000000..8122c2480
--- /dev/null
+++ b/packages/recommend/index.js
@@ -0,0 +1,15 @@
+/* eslint-disable functional/immutable-data, import/no-commonjs */
+const recommend = require('./dist/recommend.cjs.js');
+
+/**
+ * The Common JS build is the default entry point for the Node environment. Keep in
+ * in mind, that for the browser environment, we hint the bundler to use the UMD
+ * build instead as specified on the key `browser` of our `package.json` file.
+ */
+module.exports = recommend;
+
+/**
+ * In addition, we also set explicitly the default export below making
+ * this Common JS module in compliance with es6 modules specification.
+ */
+module.exports.default = recommend;
diff --git a/packages/recommend/package.json b/packages/recommend/package.json
new file mode 100644
index 000000000..5b4463a90
--- /dev/null
+++ b/packages/recommend/package.json
@@ -0,0 +1,37 @@
+{
+ "name": "@algolia/recommend",
+ "version": "4.9.3",
+ "private": false,
+ "description": "The perfect starting point to integrate Algolia Recommend within your JavaScript project.",
+ "repository": {
+ "type": "git",
+ "url": "git://github.com/algolia/algoliasearch-client-javascript.git"
+ },
+ "license": "MIT",
+ "sideEffects": false,
+ "main": "index.js",
+ "jsdelivr": "./dist/recommend.umd.js",
+ "unpkg": "./dist/recommend.umd.js",
+ "browser": {
+ "./index.js": "./dist/recommend.umd.js"
+ },
+ "types": "index.d.ts",
+ "files": [
+ "dist",
+ "index.js",
+ "index.d.ts"
+ ],
+ "dependencies": {
+ "@algolia/cache-browser-local-storage": "4.9.3",
+ "@algolia/cache-common": "4.9.3",
+ "@algolia/cache-in-memory": "4.9.3",
+ "@algolia/client-common": "4.9.3",
+ "@algolia/client-search": "4.9.3",
+ "@algolia/logger-common": "4.9.3",
+ "@algolia/logger-console": "4.9.3",
+ "@algolia/requester-browser-xhr": "4.9.3",
+ "@algolia/requester-common": "4.9.3",
+ "@algolia/requester-node-http": "4.9.3",
+ "@algolia/transporter": "4.9.3"
+ }
+}
diff --git a/packages/recommend/src/__tests__/getFrequentlyBoughtTogether.test.ts b/packages/recommend/src/__tests__/getFrequentlyBoughtTogether.test.ts
new file mode 100644
index 000000000..2a6908a64
--- /dev/null
+++ b/packages/recommend/src/__tests__/getFrequentlyBoughtTogether.test.ts
@@ -0,0 +1,77 @@
+import { TestSuite } from '../../../client-common/src/__tests__/TestSuite';
+
+const recommend = new TestSuite('recommend').recommend;
+
+function createMockedClient() {
+ const client = recommend('appId', 'apiKey');
+ jest.spyOn(client.transporter, 'read').mockImplementation(() => Promise.resolve());
+
+ return client;
+}
+
+describe('getFrequentlyBoughtTogether', () => {
+ test('builds the request', async () => {
+ const client = createMockedClient();
+
+ await client.getFrequentlyBoughtTogether(
+ {
+ indexName: 'products',
+ objectID: 'B018APC4LE',
+ },
+ {}
+ );
+
+ expect(client.transporter.read).toHaveBeenCalledTimes(1);
+ expect(client.transporter.read).toHaveBeenCalledWith(
+ {
+ cacheable: true,
+ data: {
+ requests: [
+ {
+ fallbackParameters: {},
+ indexName: 'products',
+ model: 'bought-together',
+ objectID: 'B018APC4LE',
+ threshold: 0,
+ },
+ ],
+ },
+ method: 'POST',
+ path: '1/indexes/*/recommendations',
+ },
+ {}
+ );
+ });
+
+ test('ignores `fallbackParameters`', async () => {
+ const client = createMockedClient();
+
+ await client.getFrequentlyBoughtTogether({
+ // @ts-ignore `fallbackParameters` are not supposed to be passed
+ // according to the types
+ fallbackParameters: {
+ facetFilters: [],
+ },
+ indexName: 'products',
+ objectID: 'B018APC4LE',
+ });
+
+ expect(client.transporter.read).toHaveBeenCalledTimes(1);
+ expect(client.transporter.read).toHaveBeenCalledWith(
+ expect.objectContaining({
+ data: {
+ requests: [
+ {
+ fallbackParameters: {},
+ indexName: 'products',
+ model: 'bought-together',
+ objectID: 'B018APC4LE',
+ threshold: 0,
+ },
+ ],
+ },
+ }),
+ undefined
+ );
+ });
+});
diff --git a/packages/recommend/src/__tests__/getRecommendations.test.ts b/packages/recommend/src/__tests__/getRecommendations.test.ts
new file mode 100644
index 000000000..2852033a8
--- /dev/null
+++ b/packages/recommend/src/__tests__/getRecommendations.test.ts
@@ -0,0 +1,78 @@
+import { TestSuite } from '../../../client-common/src/__tests__/TestSuite';
+
+const recommend = new TestSuite('recommend').recommend;
+
+function createMockedClient() {
+ const client = recommend('appId', 'apiKey');
+ jest.spyOn(client.transporter, 'read').mockImplementation(() => Promise.resolve());
+
+ return client;
+}
+
+describe('getRecommendations', () => {
+ test('builds the request for "bought-together" model', async () => {
+ const client = createMockedClient();
+
+ await client.getRecommendations(
+ {
+ model: 'bought-together',
+ indexName: 'products',
+ objectID: 'B018APC4LE',
+ },
+ {}
+ );
+
+ expect(client.transporter.read).toHaveBeenCalledTimes(1);
+ expect(client.transporter.read).toHaveBeenCalledWith(
+ {
+ cacheable: true,
+ data: {
+ requests: [
+ {
+ indexName: 'products',
+ model: 'bought-together',
+ objectID: 'B018APC4LE',
+ threshold: 0,
+ },
+ ],
+ },
+ method: 'POST',
+ path: '1/indexes/*/recommendations',
+ },
+ {}
+ );
+ });
+
+ test('builds the request for "related-products" model', async () => {
+ const client = createMockedClient();
+
+ await client.getRecommendations(
+ {
+ model: 'related-products',
+ indexName: 'products',
+ objectID: 'B018APC4LE',
+ },
+ {}
+ );
+
+ expect(client.transporter.read).toHaveBeenCalledTimes(1);
+ expect(client.transporter.read).toHaveBeenCalledWith(
+ {
+ cacheable: true,
+ data: {
+ requests: [
+ {
+ indexName: 'products',
+ model: 'related-products',
+ objectID: 'B018APC4LE',
+ threshold: 0,
+ },
+ ],
+ },
+ method: 'POST',
+ path: '1/indexes/*/recommendations',
+ },
+ {}
+ );
+ });
+});
diff --git a/packages/recommend/src/__tests__/getRelatedProducts.test.ts b/packages/recommend/src/__tests__/getRelatedProducts.test.ts
new file mode 100644
index 000000000..6e91cc950
--- /dev/null
+++ b/packages/recommend/src/__tests__/getRelatedProducts.test.ts
@@ -0,0 +1,44 @@
+import { TestSuite } from '../../../client-common/src/__tests__/TestSuite';
+
+const recommend = new TestSuite('recommend').recommend;
+
+function createMockedClient() {
+ const client = recommend('appId', 'apiKey');
+ jest.spyOn(client.transporter, 'read').mockImplementation(() => Promise.resolve());
+
+ return client;
+}
+
+describe('getRelatedProducts', () => {
+ test('builds the request', async () => {
+ const client = createMockedClient();
+
+ await client.getRelatedProducts(
+ {
+ indexName: 'products',
+ objectID: 'B018APC4LE',
+ },
+ {}
+ );
+
+ expect(client.transporter.read).toHaveBeenCalledTimes(1);
+ expect(client.transporter.read).toHaveBeenCalledWith(
+ {
+ cacheable: true,
+ data: {
+ requests: [
+ {
+ indexName: 'products',
+ model: 'related-products',
+ objectID: 'B018APC4LE',
+ threshold: 0,
+ },
+ ],
+ },
+ method: 'POST',
+ path: '1/indexes/*/recommendations',
+ },
+ {}
+ );
+ });
+});
diff --git a/packages/recommend/src/__tests__/recommend-client.test.ts b/packages/recommend/src/__tests__/recommend-client.test.ts
new file mode 100644
index 000000000..d3798a54d
--- /dev/null
+++ b/packages/recommend/src/__tests__/recommend-client.test.ts
@@ -0,0 +1,179 @@
+import { createInMemoryCache } from '@algolia/cache-in-memory';
+import { version } from '@algolia/client-common';
+import { createStatelessHost, createUserAgent } from '@algolia/transporter';
+
+import { TestSuite } from '../../../client-common/src/__tests__/TestSuite';
+
+const recommend = new TestSuite('recommend').recommend;
+
+describe('recommend', () => {
+ test('has a version property', () => {
+ expect(recommend.version).toBe(version);
+ expect(recommend.version.startsWith('4.')).toBe(true);
+ });
+
+ test('gives access to appId', () => {
+ expect(recommend('appId', 'apiKey').appId).toEqual('appId');
+ });
+
+ test('clearCache', async () => {
+ const client = recommend('appId', 'apiKey');
+
+ client.transporter.requestsCache.set('bla', 'blo');
+ client.transporter.responsesCache.set('bla', 'blo');
+
+ if (testing.isBrowser()) {
+ await expect(
+ client.transporter.requestsCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('blo');
+ await expect(
+ client.transporter.responsesCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('blo');
+ } else {
+ // node uses a null cache, so these assertions don't make sense there
+ await expect(
+ client.transporter.requestsCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ await expect(
+ client.transporter.responsesCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ }
+
+ await client.clearCache();
+
+ await expect(
+ client.transporter.requestsCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ await expect(
+ client.transporter.responsesCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ });
+
+ test('clearCache without promise', async () => {
+ const client = recommend('appId', 'apiKey');
+
+ client.transporter.requestsCache.set('bla', 'blo');
+ client.transporter.responsesCache.set('bla', 'blo');
+
+ if (testing.isBrowser()) {
+ await expect(
+ client.transporter.requestsCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('blo');
+ await expect(
+ client.transporter.responsesCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('blo');
+ } else {
+ // node uses a null cache, so these assertions don't make sense there
+ await expect(
+ client.transporter.requestsCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ await expect(
+ client.transporter.responsesCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ }
+
+ // no await, since default memory caches _actually_ are instant
+ client.clearCache();
+
+ await expect(
+ client.transporter.requestsCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ await expect(
+ client.transporter.responsesCache.get('bla', () => Promise.resolve('wrong'))
+ ).resolves.toBe('wrong');
+ });
+
+ it('sets default headers and queryParameters', () => {
+ const client = recommend('appId', 'apiKey');
+
+ if (testing.isBrowser()) {
+ expect(client.transporter.headers).toEqual({
+ 'content-type': 'application/x-www-form-urlencoded',
+ });
+ expect(client.transporter.queryParameters).toEqual({
+ 'x-algolia-application-id': 'appId',
+ 'x-algolia-api-key': 'apiKey',
+ });
+ } else {
+ expect(client.transporter.headers).toEqual({
+ 'content-type': 'application/x-www-form-urlencoded',
+ 'x-algolia-application-id': 'appId',
+ 'x-algolia-api-key': 'apiKey',
+ });
+ expect(client.transporter.queryParameters).toEqual({});
+ }
+ });
+
+ test('sets default user agent', () => {
+ const client = recommend('appId', 'apiKey');
+
+ if (testing.isBrowser()) {
+ expect(client.transporter.userAgent.value).toEqual(
+ `Algolia for JavaScript (${version}); Recommend (${version}); Browser`
+ );
+ } else {
+ const nodeVersion = process.versions.node;
+
+ expect(client.transporter.userAgent.value).toEqual(
+ `Algolia for JavaScript (${version}); Recommend (${version}); Node.js (${nodeVersion})`
+ );
+ }
+ });
+
+ test('allows to customize options', () => {
+ const client = recommend('appId', 'apiKey');
+ const cache = createInMemoryCache();
+ const userAgent = createUserAgent('0.2.0');
+
+ const customClient = recommend('appId', 'apiKey', {
+ hostsCache: cache,
+ requestsCache: cache,
+ userAgent,
+ timeouts: {
+ connect: 45,
+ read: 46,
+ write: 47,
+ },
+ queryParameters: {
+ queryParameter: 'bar',
+ },
+ headers: {
+ header: 'foo',
+ },
+ hosts: [{ url: 'foo.com' }],
+ });
+
+ // Then, on custom options, only the client is impacted
+ expect(client.transporter.hostsCache).not.toBe(cache);
+ expect(customClient.transporter.hostsCache).toBe(cache);
+
+ expect(client.transporter.requestsCache).not.toBe(cache);
+ expect(customClient.transporter.requestsCache).toBe(cache);
+ expect(customClient.transporter.timeouts).toEqual({
+ connect: 45,
+ read: 46,
+ write: 47,
+ });
+
+ expect(customClient.transporter.queryParameters).toEqual(
+ expect.objectContaining({
+ queryParameter: 'bar',
+ })
+ );
+ expect(customClient.transporter.headers).toEqual(
+ expect.objectContaining({
+ 'content-type': 'application/x-www-form-urlencoded',
+ header: 'foo',
+ })
+ );
+ expect(customClient.transporter.hosts).toEqual([createStatelessHost({ url: 'foo.com' })]);
+ });
+
+ test('can be destroyed', () => {
+ const client = recommend('appId', 'apiKey');
+
+ if (!testing.isBrowser()) {
+ expect(client).toHaveProperty('destroy');
+ }
+ });
+});
diff --git a/packages/recommend/src/builds/browser.ts b/packages/recommend/src/builds/browser.ts
new file mode 100644
index 000000000..4e791606f
--- /dev/null
+++ b/packages/recommend/src/builds/browser.ts
@@ -0,0 +1,57 @@
+import { createBrowserLocalStorageCache } from '@algolia/cache-browser-local-storage';
+import { createFallbackableCache } from '@algolia/cache-common';
+import { createInMemoryCache } from '@algolia/cache-in-memory';
+import { AuthMode, version } from '@algolia/client-common';
+import { LogLevelEnum } from '@algolia/logger-common';
+import { createConsoleLogger } from '@algolia/logger-console';
+import { createBrowserXhrRequester } from '@algolia/requester-browser-xhr';
+import { createUserAgent } from '@algolia/transporter';
+
+import { createRecommendClient } from '../createRecommendClient';
+import { getFrequentlyBoughtTogether, getRecommendations, getRelatedProducts } from '../methods';
+import { RecommendClient, RecommendOptions, WithRecommendMethods } from '../types';
+
+export default function recommend(
+ appId: string,
+ apiKey: string,
+ options?: RecommendOptions
+): WithRecommendMethods {
+ const commonOptions = {
+ appId,
+ apiKey,
+ timeouts: {
+ connect: 1,
+ read: 2,
+ write: 30,
+ },
+ requester: createBrowserXhrRequester(),
+ logger: createConsoleLogger(LogLevelEnum.Error),
+ responsesCache: createInMemoryCache(),
+ requestsCache: createInMemoryCache({ serializable: false }),
+ hostsCache: createFallbackableCache({
+ caches: [
+ createBrowserLocalStorageCache({ key: `${version}-${appId}` }),
+ createInMemoryCache(),
+ ],
+ }),
+ userAgent: createUserAgent(version)
+ .add({ segment: 'Recommend', version })
+ .add({ segment: 'Browser' }),
+ authMode: AuthMode.WithinQueryParameters,
+ };
+
+ return createRecommendClient({
+ ...commonOptions,
+ ...options,
+ methods: {
+ getFrequentlyBoughtTogether,
+ getRecommendations,
+ getRelatedProducts,
+ },
+ });
+}
+
+// eslint-disable-next-line functional/immutable-data
+recommend.version = version;
+
+export * from '../types';
diff --git a/packages/recommend/src/builds/node.ts b/packages/recommend/src/builds/node.ts
new file mode 100644
index 000000000..2bd5a74d6
--- /dev/null
+++ b/packages/recommend/src/builds/node.ts
@@ -0,0 +1,57 @@
+import { createNullCache } from '@algolia/cache-common';
+import { createInMemoryCache } from '@algolia/cache-in-memory';
+import { destroy, version } from '@algolia/client-common';
+import { createNullLogger } from '@algolia/logger-common';
+import { Destroyable } from '@algolia/requester-common';
+import { createNodeHttpRequester } from '@algolia/requester-node-http';
+import { createUserAgent } from '@algolia/transporter';
+
+import { createRecommendClient } from '../createRecommendClient';
+import { getFrequentlyBoughtTogether, getRecommendations, getRelatedProducts } from '../methods';
+import {
+ RecommendClient as BaseRecommendClient,
+ RecommendOptions,
+ WithRecommendMethods,
+} from '../types';
+
+export default function recommend(
+ appId: string,
+ apiKey: string,
+ options?: RecommendOptions
+): WithRecommendMethods {
+ const commonOptions = {
+ appId,
+ apiKey,
+ timeouts: {
+ connect: 2,
+ read: 5,
+ write: 30,
+ },
+ requester: createNodeHttpRequester(),
+ logger: createNullLogger(),
+ responsesCache: createNullCache(),
+ requestsCache: createNullCache(),
+ hostsCache: createInMemoryCache(),
+ userAgent: createUserAgent(version)
+ .add({ segment: 'Recommend', version })
+ .add({ segment: 'Node.js', version: process.versions.node }),
+ };
+
+ return createRecommendClient({
+ ...commonOptions,
+ ...options,
+ methods: {
+ destroy,
+ getFrequentlyBoughtTogether,
+ getRecommendations,
+ getRelatedProducts,
+ },
+ });
+}
+
+// eslint-disable-next-line functional/immutable-data
+recommend.version = version;
+
+export type RecommendClient = BaseRecommendClient & Destroyable;
+
+export * from '../types';
diff --git a/packages/recommend/src/createRecommendClient.ts b/packages/recommend/src/createRecommendClient.ts
new file mode 100644
index 000000000..83ed830c7
--- /dev/null
+++ b/packages/recommend/src/createRecommendClient.ts
@@ -0,0 +1,64 @@
+import {
+ addMethods,
+ AuthMode,
+ ClientTransporterOptions,
+ createAuth,
+ CreateClient,
+ shuffle,
+} from '@algolia/client-common';
+import { CallEnum, createTransporter, HostOptions } from '@algolia/transporter';
+
+import { RecommendClient, RecommendClientOptions } from './types';
+
+export const createRecommendClient: CreateClient<
+ RecommendClient,
+ RecommendClientOptions & ClientTransporterOptions
+> = options => {
+ const appId = options.appId;
+
+ const auth = createAuth(
+ options.authMode !== undefined ? options.authMode : AuthMode.WithinHeaders,
+ appId,
+ options.apiKey
+ );
+
+ const transporter = createTransporter({
+ hosts: ([
+ { url: `${appId}-dsn.algolia.net`, accept: CallEnum.Read },
+ { url: `${appId}.algolia.net`, accept: CallEnum.Write },
+ ] as readonly HostOptions[]).concat(
+ shuffle([
+ { url: `${appId}-1.algolianet.com` },
+ { url: `${appId}-2.algolianet.com` },
+ { url: `${appId}-3.algolianet.com` },
+ ])
+ ),
+ ...options,
+ headers: {
+ ...auth.headers(),
+ ...{ 'content-type': 'application/x-www-form-urlencoded' },
+ ...options.headers,
+ },
+
+ queryParameters: {
+ ...auth.queryParameters(),
+ ...options.queryParameters,
+ },
+ });
+
+ const base = {
+ transporter,
+ appId,
+ addAlgoliaAgent(segment: string, version?: string): void {
+ transporter.userAgent.add({ segment, version });
+ },
+ clearCache(): Readonly> {
+ return Promise.all([
+ transporter.requestsCache.clear(),
+ transporter.responsesCache.clear(),
+ ]).then(() => undefined);
+ },
+ };
+
+ return addMethods(base, options.methods);
+};
diff --git a/packages/recommend/src/index.ts b/packages/recommend/src/index.ts
new file mode 100644
index 000000000..e067cd20c
--- /dev/null
+++ b/packages/recommend/src/index.ts
@@ -0,0 +1,5 @@
+/**
+ * @file Automatically generated by barrelsby.
+ */
+
+export * from './types/index';
diff --git a/packages/recommend/src/methods/getFrequentlyBoughtTogether.ts b/packages/recommend/src/methods/getFrequentlyBoughtTogether.ts
new file mode 100644
index 000000000..46b950c42
--- /dev/null
+++ b/packages/recommend/src/methods/getFrequentlyBoughtTogether.ts
@@ -0,0 +1,24 @@
+import { RecommendClient, WithRecommendMethods } from '../types';
+import { getRecommendations, GetRecommendationsOptions } from './getRecommendations';
+
+export type GetFrequentlyBoughtTogetherOptions = Omit<
+ GetRecommendationsOptions,
+ 'model' | 'fallbackParameters'
+>;
+
+type GetFrequentlyBoughtTogether = (
+ base: RecommendClient
+) => WithRecommendMethods['getFrequentlyBoughtTogether'];
+
+export const getFrequentlyBoughtTogether: GetFrequentlyBoughtTogether = base => {
+ return (options, requestOptions) => {
+ return getRecommendations(base)(
+ {
+ ...options,
+ fallbackParameters: {},
+ model: 'bought-together',
+ },
+ requestOptions
+ );
+ };
+};
diff --git a/packages/recommend/src/methods/getRecommendations.ts b/packages/recommend/src/methods/getRecommendations.ts
new file mode 100644
index 000000000..850e83da2
--- /dev/null
+++ b/packages/recommend/src/methods/getRecommendations.ts
@@ -0,0 +1,48 @@
+import { MethodEnum } from '@algolia/requester-common';
+
+import {
+ RecommendClient,
+ RecommendModel,
+ RecommendSearchOptions,
+ WithRecommendMethods,
+} from '../types';
+
+export type GetRecommendationsOptions = {
+ readonly indexName: string;
+ readonly model: RecommendModel;
+ readonly objectID: string;
+ readonly threshold?: number;
+ readonly maxRecommendations?: number;
+ readonly queryParameters?: RecommendSearchOptions;
+ readonly fallbackParameters?: RecommendSearchOptions;
+};
+
+type GetRecommendations = (
+ base: RecommendClient
+) => WithRecommendMethods['getRecommendations'];
+
+export const getRecommendations: GetRecommendations = base => {
+ return (options, requestOptions) => {
+ const requests: readonly GetRecommendationsOptions[] = [
+ {
+ // The `threshold` param is required by the endpoint to make it easier
+ // to provide a default value later, so we default it in the client
+ // so that users don't have to provide a value.
+ threshold: 0,
+ ...options,
+ },
+ ];
+
+ return base.transporter.read(
+ {
+ method: MethodEnum.Post,
+ path: '1/indexes/*/recommendations',
+ data: {
+ requests,
+ },
+ cacheable: true,
+ },
+ requestOptions
+ );
+ };
+};
diff --git a/packages/recommend/src/methods/getRelatedProducts.ts b/packages/recommend/src/methods/getRelatedProducts.ts
new file mode 100644
index 000000000..10b718727
--- /dev/null
+++ b/packages/recommend/src/methods/getRelatedProducts.ts
@@ -0,0 +1,20 @@
+import { RecommendClient, WithRecommendMethods } from '../types';
+import { getRecommendations, GetRecommendationsOptions } from './getRecommendations';
+
+export type GetRelatedProductsOptions = Omit;
+
+type GetRelatedProducts = (
+ base: RecommendClient
+) => WithRecommendMethods['getRelatedProducts'];
+
+export const getRelatedProducts: GetRelatedProducts = base => {
+ return (options, requestOptions) => {
+ return getRecommendations(base)(
+ {
+ ...options,
+ model: 'related-products',
+ },
+ requestOptions
+ );
+ };
+};
diff --git a/packages/recommend/src/methods/index.ts b/packages/recommend/src/methods/index.ts
new file mode 100644
index 000000000..a53053aef
--- /dev/null
+++ b/packages/recommend/src/methods/index.ts
@@ -0,0 +1,7 @@
+/**
+ * @file Automatically generated by barrelsby.
+ */
+
+export * from './getFrequentlyBoughtTogether';
+export * from './getRecommendations';
+export * from './getRelatedProducts';
diff --git a/packages/recommend/src/types/RecommendClient.ts b/packages/recommend/src/types/RecommendClient.ts
new file mode 100644
index 000000000..ee94cab74
--- /dev/null
+++ b/packages/recommend/src/types/RecommendClient.ts
@@ -0,0 +1,23 @@
+import { Transporter } from '@algolia/transporter';
+
+export type RecommendClient = {
+ /**
+ * The application id.
+ */
+ readonly appId: string;
+
+ /**
+ * The underlying transporter.
+ */
+ readonly transporter: Transporter;
+
+ /**
+ * Mutates the transporter, adding the given user agent.
+ */
+ readonly addAlgoliaAgent: (segment: string, version?: string) => void;
+
+ /**
+ * Clears both requests and responses caches.
+ */
+ readonly clearCache: () => Readonly>;
+};
diff --git a/packages/recommend/src/types/RecommendClientOptions.ts b/packages/recommend/src/types/RecommendClientOptions.ts
new file mode 100644
index 000000000..a493f7846
--- /dev/null
+++ b/packages/recommend/src/types/RecommendClientOptions.ts
@@ -0,0 +1,19 @@
+import { AuthModeType } from '@algolia/client-common';
+
+export type RecommendClientOptions = {
+ /**
+ * The application id.
+ */
+ readonly appId: string;
+
+ /**
+ * The api key.
+ */
+ readonly apiKey: string;
+
+ /**
+ * The auth mode type. In browser environments credentials may
+ * be passed within the headers.
+ */
+ readonly authMode?: AuthModeType;
+};
diff --git a/packages/recommend/src/types/RecommendModel.ts b/packages/recommend/src/types/RecommendModel.ts
new file mode 100644
index 000000000..8e3cdfcdf
--- /dev/null
+++ b/packages/recommend/src/types/RecommendModel.ts
@@ -0,0 +1 @@
+export type RecommendModel = 'related-products' | 'bought-together';
diff --git a/packages/recommend/src/types/RecommendOptions.ts b/packages/recommend/src/types/RecommendOptions.ts
new file mode 100644
index 000000000..b37634837
--- /dev/null
+++ b/packages/recommend/src/types/RecommendOptions.ts
@@ -0,0 +1,3 @@
+import { ClientTransporterOptions } from '@algolia/client-common';
+
+export type RecommendOptions = Partial;
diff --git a/packages/recommend/src/types/RecommendSearchOptions.ts b/packages/recommend/src/types/RecommendSearchOptions.ts
new file mode 100644
index 000000000..e14d9ab6c
--- /dev/null
+++ b/packages/recommend/src/types/RecommendSearchOptions.ts
@@ -0,0 +1,6 @@
+import { SearchOptions } from '@algolia/client-search';
+
+export type RecommendSearchOptions = Omit<
+ SearchOptions,
+ 'page' | 'hitsPerPage' | 'offset' | 'length'
+>;
diff --git a/packages/recommend/src/types/WithRecommendMethods.ts b/packages/recommend/src/types/WithRecommendMethods.ts
new file mode 100644
index 000000000..fe5aac59a
--- /dev/null
+++ b/packages/recommend/src/types/WithRecommendMethods.ts
@@ -0,0 +1,34 @@
+import { MultipleQueriesResponse, SearchOptions } from '@algolia/client-search';
+import { RequestOptions } from '@algolia/transporter';
+
+import {
+ GetFrequentlyBoughtTogetherOptions,
+ GetRecommendationsOptions,
+ GetRelatedProductsOptions,
+} from '../methods';
+
+export type WithRecommendMethods = TType & {
+ /**
+ * Returns recommendations.
+ */
+ readonly getRecommendations: (
+ options: GetRecommendationsOptions,
+ requestOptions?: RequestOptions & SearchOptions
+ ) => Readonly>>;
+
+ /**
+ * Returns [Related Products](https://algolia.com/doc/guides/algolia-ai/recommend/#related-products).
+ */
+ readonly getRelatedProducts: (
+ options: GetRelatedProductsOptions,
+ requestOptions?: RequestOptions & SearchOptions
+ ) => Readonly>>;
+
+ /**
+ * Returns [Frequently Bought Together](https://algolia.com/doc/guides/algolia-ai/recommend/#frequently-bought-together) products.
+ */
+ readonly getFrequentlyBoughtTogether: (
+ options: GetFrequentlyBoughtTogetherOptions,
+ requestOptions?: RequestOptions & SearchOptions
+ ) => Readonly>>;
+};
diff --git a/packages/recommend/src/types/index.ts b/packages/recommend/src/types/index.ts
new file mode 100644
index 000000000..b598043e4
--- /dev/null
+++ b/packages/recommend/src/types/index.ts
@@ -0,0 +1,10 @@
+/**
+ * @file Automatically generated by barrelsby.
+ */
+
+export * from './RecommendClient';
+export * from './RecommendClientOptions';
+export * from './RecommendModel';
+export * from './RecommendOptions';
+export * from './RecommendSearchOptions';
+export * from './WithRecommendMethods';
diff --git a/packages/recommend/tsconfig.json b/packages/recommend/tsconfig.json
new file mode 100644
index 000000000..39df687c5
--- /dev/null
+++ b/packages/recommend/tsconfig.json
@@ -0,0 +1,10 @@
+{
+ "extends": "../../tsconfig.json",
+ "compilerOptions": {
+ "declaration": true,
+ "noEmit": false,
+ "allowJs": false,
+ "emitDeclarationOnly": true,
+ "outFile": "dist/types.d.ts"
+ }
+}
diff --git a/rollup.config.js b/rollup.config.js
index 4d6e8b850..76e16ff93 100644
--- a/rollup.config.js
+++ b/rollup.config.js
@@ -29,6 +29,7 @@ const packagesConfig = [
'client-account',
'client-analytics',
'client-common',
+ 'client-personalization',
'client-recommendation',
'logger-common',
'logger-console',
@@ -98,6 +99,21 @@ packagesConfig.push({
});
});
+packagesConfig.push(
+ {
+ output: 'recommend',
+ package: 'recommend',
+ input: `src/builds/browser.ts`,
+ formats: ['esm-browser', 'umd'],
+ },
+ {
+ output: 'recommend',
+ package: 'recommend',
+ input: `src/builds/node.ts`,
+ formats: ['cjs'],
+ }
+);
+
const packagesDir = path.resolve(__dirname, 'packages');
const aliasOptions = { resolve: ['.ts'] };
diff --git a/scripts/test-build-declarations.js b/scripts/test-build-declarations.js
index 19eac4ce5..2e871750d 100755
--- a/scripts/test-build-declarations.js
+++ b/scripts/test-build-declarations.js
@@ -2,7 +2,7 @@
const glob = require('glob');
const execa = require('execa');
-const NUMBER_OF_DECLARATIONS = 18;
+const NUMBER_OF_DECLARATIONS = 21;
(async () => {
const declarations = await new Promise(resolve => {