diff --git a/clients/algoliasearch-client-javascript/packages/algoliasearch/__tests__/algoliasearch.common.test.ts b/clients/algoliasearch-client-javascript/packages/algoliasearch/__tests__/algoliasearch.common.test.ts index ffdcf3b9a0..c76d2baa18 100644 --- a/clients/algoliasearch-client-javascript/packages/algoliasearch/__tests__/algoliasearch.common.test.ts +++ b/clients/algoliasearch-client-javascript/packages/algoliasearch/__tests__/algoliasearch.common.test.ts @@ -143,131 +143,110 @@ describe('api', () => { }); }); - describe('init clients', () => { - test('provides an init method for the analytics client', () => { - expect(client.initAnalytics).not.toBeUndefined(); - }); - - test('provides an init method for the abtesting client', () => { - expect(client.initAbtesting).not.toBeUndefined(); + describe('bridge methods', () => { + test('throws when missing transformation.region', () => { + //@ts-expect-error + expect(() => algoliasearch('APP_ID', 'API_KEY', { transformation: {} })).toThrow( + '`region` must be provided when leveraging the transformation pipeline', + ); }); - test('provides an init method for the personalization client', () => { - expect(client.initPersonalization).not.toBeUndefined(); - }); + test('throws when calling the transformation methods without init parameters', async () => { + await expect( + client.saveObjectsWithTransformation({ + indexName: 'foo', + objects: [{ objectID: 'bar', baz: 42 }], + waitForTasks: true, + }), + ).rejects.toThrow('`transformation.region` must be provided at client instantiation before calling this method.'); - test('provides an init method for the recommend client', () => { - expect(client.initRecommend).not.toBeUndefined(); + await expect( + client.partialUpdateObjectsWithTransformation({ + indexName: 'foo', + objects: [{ objectID: 'bar', baz: 42 }], + waitForTasks: true, + }), + ).rejects.toThrow('`transformation.region` must be provided at client instantiation before calling this method.'); }); - test('default `init` clients to the root `algoliasearch` credentials', async () => { - const abtestingClient = client.initAbtesting({ options: { requester: browserEchoRequester() } }); - const analyticsClient = client.initAnalytics({ options: { requester: browserEchoRequester() } }); - const recommendClient = client.initRecommend({ options: { requester: browserEchoRequester() } }); - const personalizationClient = client.initPersonalization({ - region: 'eu', - options: { requester: browserEchoRequester() }, + test('exposes the transformation methods at the root of the client', async () => { + const ingestionClient = algoliasearch('APP_ID', 'API_KEY', { + requester: browserEchoRequester(), + transformation: { region: 'us' }, }); - const res1 = (await abtestingClient.customGet({ - path: 'abtestingClient', - })) as unknown as EchoResponse; - const res2 = (await analyticsClient.customGet({ - path: 'analyticsClient', - })) as unknown as EchoResponse; - const res3 = (await personalizationClient.customGet({ - path: 'personalizationClient', - })) as unknown as EchoResponse; - const res4 = (await recommendClient.customGet({ - path: 'recommendClient', + expect(ingestionClient.saveObjectsWithTransformation).not.toBeUndefined(); + + let res = (await ingestionClient.saveObjectsWithTransformation({ + indexName: 'foo', + objects: [{ objectID: 'bar', baz: 42 }], + waitForTasks: true, })) as unknown as EchoResponse; - expect(res1.headers).toEqual( - expect.objectContaining({ - 'x-algolia-application-id': 'APP_ID', - 'x-algolia-api-key': 'API_KEY', - }), - ); - expect(res2.headers).toEqual( + expect(res.headers).toEqual( expect.objectContaining({ 'x-algolia-application-id': 'APP_ID', 'x-algolia-api-key': 'API_KEY', }), ); - expect(res3.headers).toEqual( - expect.objectContaining({ - 'x-algolia-application-id': 'APP_ID', - 'x-algolia-api-key': 'API_KEY', - }), - ); - expect(res4.headers).toEqual( + expect(res.url.startsWith('https://data.us.algolia.com/1/push/foo?watch=true')).toBeTruthy(); + expect(res.data).toEqual({ + action: 'addObject', + records: [ + { + baz: 42, + objectID: 'bar', + }, + ], + }); + expect(ingestionClient.partialUpdateObjectsWithTransformation).not.toBeUndefined(); + + res = (await ingestionClient.partialUpdateObjectsWithTransformation({ + indexName: 'foo', + objects: [{ objectID: 'bar', baz: 42 }], + waitForTasks: true, + createIfNotExists: true, + })) as unknown as EchoResponse; + + expect(res.headers).toEqual( expect.objectContaining({ 'x-algolia-application-id': 'APP_ID', 'x-algolia-api-key': 'API_KEY', }), ); - }); - - test('`init` clients accept different credentials', async () => { - const abtestingClient = client.initAbtesting({ - appId: 'appId1', - apiKey: 'apiKey1', - options: { requester: browserEchoRequester() }, - }); - const analyticsClient = client.initAnalytics({ - appId: 'appId2', - apiKey: 'apiKey2', - options: { requester: browserEchoRequester() }, - }); - const personalizationClient = client.initPersonalization({ - appId: 'appId3', - apiKey: 'apiKey3', - region: 'eu', - options: { requester: browserEchoRequester() }, - }); - const recommendClient = client.initRecommend({ - appId: 'appId4', - apiKey: 'apiKey4', - options: { requester: browserEchoRequester() }, + expect(res.url.startsWith('https://data.us.algolia.com/1/push/foo?watch=true')).toBeTruthy(); + expect(res.data).toEqual({ + action: 'partialUpdateObject', + records: [ + { + baz: 42, + objectID: 'bar', + }, + ], }); - const res1 = (await abtestingClient.customGet({ - path: 'abtestingClient', - })) as unknown as EchoResponse; - const res2 = (await analyticsClient.customGet({ - path: 'analyticsClient', - })) as unknown as EchoResponse; - const res3 = (await personalizationClient.customGet({ - path: 'personalizationClient', - })) as unknown as EchoResponse; - const res4 = (await recommendClient.customGet({ - path: 'recommendClient', + res = (await ingestionClient.partialUpdateObjectsWithTransformation({ + indexName: 'foo', + objects: [{ objectID: 'bar', baz: 42 }], + waitForTasks: true, })) as unknown as EchoResponse; - expect(res1.headers).toEqual( - expect.objectContaining({ - 'x-algolia-application-id': 'appId1', - 'x-algolia-api-key': 'apiKey1', - }), - ); - expect(res2.headers).toEqual( - expect.objectContaining({ - 'x-algolia-application-id': 'appId2', - 'x-algolia-api-key': 'apiKey2', - }), - ); - expect(res3.headers).toEqual( + expect(res.headers).toEqual( expect.objectContaining({ - 'x-algolia-application-id': 'appId3', - 'x-algolia-api-key': 'apiKey3', - }), - ); - expect(res4.headers).toEqual( - expect.objectContaining({ - 'x-algolia-application-id': 'appId4', - 'x-algolia-api-key': 'apiKey4', + 'x-algolia-application-id': 'APP_ID', + 'x-algolia-api-key': 'API_KEY', }), ); + expect(res.url.startsWith('https://data.us.algolia.com/1/push/foo?watch=true')).toBeTruthy(); + expect(res.data).toEqual({ + action: 'partialUpdateObjectNoCreate', + records: [ + { + baz: 42, + objectID: 'bar', + }, + ], + }); }); }); }); diff --git a/playground/javascript/node/algoliasearch.ts b/playground/javascript/node/algoliasearch.ts index 247699e5d5..9b69b52311 100644 --- a/playground/javascript/node/algoliasearch.ts +++ b/playground/javascript/node/algoliasearch.ts @@ -6,18 +6,19 @@ import type { SearchResponses } from 'algoliasearch'; const appId = process.env.ALGOLIA_APPLICATION_ID || '**** APP_ID *****'; const apiKey = process.env.ALGOLIA_SEARCH_KEY || '**** SEARCH_API_KEY *****'; +const adminApiKey = process.env.ALGOLIA_ADMIN_KEY || '**** ADMIN_API_KEY *****'; const searchIndex = process.env.SEARCH_INDEX || 'test_index'; const searchQuery = process.env.SEARCH_QUERY || 'test_query'; const analyticsIndex = process.env.ANALYTICS_INDEX || 'test_index'; -// Init client with appId and apiKey -const client = algoliasearch(appId, apiKey); -const clientLite = liteClient(appId, apiKey); - -client.addAlgoliaAgent('algoliasearch node playground', '0.0.1'); - async function testAlgoliasearch() { + // Init client with appId and apiKey + const client = algoliasearch(appId, apiKey); + const clientLite = liteClient(appId, apiKey); + + client.addAlgoliaAgent('algoliasearch node playground', '0.0.1'); + try { const res: SearchResponses = await client.search({ requests: [ @@ -131,4 +132,14 @@ async function testAlgoliasearch() { } } -testAlgoliasearch(); +async function testAlgoliasearchBridgeIngestion() { + // Init client with appId and apiKey + const client = algoliasearch(appId, adminApiKey, { transformation: { region: 'eu'}}); + + await client.saveObjectsWithTransformation({indexName: "foo", objects: [{objectID: "foo", data: {baz: "baz", win: 42}}], waitForTasks: true }) + + await client.partialUpdateObjectsWithTransformation({indexName: "foo", objects: [{objectID: "foo", data: {baz: "baz", win: 42}}], waitForTasks: true, createIfNotExists: false }) +} + +// testAlgoliasearch(); +testAlgoliasearchBridgeIngestion() diff --git a/templates/javascript/clients/algoliasearch/builds/definition.mustache b/templates/javascript/clients/algoliasearch/builds/definition.mustache index e340ef69ab..72cc59995f 100644 --- a/templates/javascript/clients/algoliasearch/builds/definition.mustache +++ b/templates/javascript/clients/algoliasearch/builds/definition.mustache @@ -1,12 +1,15 @@ // {{{generationBanner}}} -import type { ClientOptions } from '@algolia/client-common'; +import type { ClientOptions, RequestOptions } from '@algolia/client-common'; {{#dependencies}} import { {{{dependencyName}}}Client } from '{{{dependencyPackage}}}'; import type { {{#lambda.titlecase}}{{{dependencyName}}}{{/lambda.titlecase}}Client } from '{{{dependencyPackage}}}'; {{/dependencies}} +import type { PartialUpdateObjectsOptions, SaveObjectsOptions } from '@algolia/client-search'; +import type { PushTaskRecords, WatchResponse } from '@algolia/ingestion'; + import type { InitClientOptions, {{#dependencies}} @@ -20,14 +23,55 @@ import type { export * from './models'; export type Algoliasearch = SearchClient & { - {{#dependencies}} - {{#withInitMethod}} - init{{#lambda.titlecase}}{{{dependencyName}}}{{/lambda.titlecase}}: (initOptions{{^dependencyHasRegionalHosts}}?{{/dependencyHasRegionalHosts}}: InitClientOptions {{#dependencyHasRegionalHosts}}& {{#lambda.titlecase}}{{{dependencyName}}}RegionOptions{{/lambda.titlecase}}{{/dependencyHasRegionalHosts}}) => {{#lambda.titlecase}}{{{dependencyName}}}{{/lambda.titlecase}}Client; - {{/withInitMethod}} - {{/dependencies}} + {{#dependencies}} + {{#withInitMethod}} + init{{#lambda.titlecase}}{{{dependencyName}}}{{/lambda.titlecase}}: (initOptions{{^dependencyHasRegionalHosts}}?{{/dependencyHasRegionalHosts}}: InitClientOptions {{#dependencyHasRegionalHosts}}& {{#lambda.titlecase}}{{{dependencyName}}}RegionOptions{{/lambda.titlecase}}{{/dependencyHasRegionalHosts}}) => {{#lambda.titlecase}}{{{dependencyName}}}{{/lambda.titlecase}}Client; + {{/withInitMethod}} + {{/dependencies}} + + // Bridge helpers to expose along with the search endpoints at the root of the API client + + /** + * Helper: Similar to the `saveObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. The `region` must've been passed to the client instantiation method. + * + * @summary Save objects to an Algolia index by leveraging the Transformation pipeline setup in the Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/). + * @param saveObjects - The `saveObjects` object. + * @param saveObjects.indexName - The `indexName` to save `objects` in. + * @param saveObjects.objects - The array of `objects` to store in the given Algolia `indexName`. + * @param saveObjects.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. + * @param saveObjects.waitForTasks - Whether or not we should wait until every `batch` tasks has been processed, this operation may slow the total execution time of this method but is more reliable. + * @param requestOptions - The requestOptions to send along with the query, they will be forwarded to the `batch` method and merged with the transporter requestOptions. + */ + saveObjectsWithTransformation: (options: SaveObjectsOptions, requestOptions?: RequestOptions) => Promise; + + /** + * Helper: Similar to the `partialUpdateObjects` method but requires a Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/) to be created first, in order to transform records before indexing them to Algolia. The `region` must've been passed to the client instantiation method. + * + * @summary Save objects to an Algolia index by leveraging the Transformation pipeline setup in the Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/). + * @param partialUpdateObjects - The `partialUpdateObjects` object. + * @param partialUpdateObjects.indexName - The `indexName` to update `objects` in. + * @param partialUpdateObjects.objects - The array of `objects` to update in the given Algolia `indexName`. + * @param partialUpdateObjects.createIfNotExists - To be provided if non-existing objects are passed, otherwise, the call will fail.. + * @param partialUpdateObjects.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. + * @param partialUpdateObjects.waitForTasks - Whether or not we should wait until every `batch` tasks has been processed, this operation may slow the total execution time of this method but is more reliable. + * @param requestOptions - The requestOptions to send along with the query, they will be forwarded to the `getTask` method and merged with the transporter requestOptions. + */ + partialUpdateObjectsWithTransformation: (options: PartialUpdateObjectsOptions, requestOptions?: RequestOptions) => Promise; +}; + +export type TransformationOptions = { + // When provided, a second transporter will be created in order to leverage the `*WithTransformation` methods exposed by the Push connector (https://www.algolia.com/doc/guides/sending-and-managing-data/send-and-update-your-data/connectors/push/). + transformation?: { + // The region of your Algolia application ID, used to target the correct hosts of the transformation service. + region: IngestionRegion; + }; }; -export function algoliasearch(appId: string, apiKey: string, options?: ClientOptions): Algoliasearch { +export function algoliasearch( + appId: string, + apiKey: string, + options?: ClientOptions & TransformationOptions, +): Algoliasearch { if (!appId || typeof appId !== 'string') { throw new Error('`appId` is missing.'); } @@ -38,9 +82,66 @@ export function algoliasearch(appId: string, apiKey: string, options?: ClientOpt const client = searchClient(appId, apiKey, options); + let ingestionTransporter: IngestionClient | undefined; + + if (options?.transformation) { + if (!options.transformation.region) { + throw new Error('`region` must be provided when leveraging the transformation pipeline'); + } + + ingestionTransporter = ingestionClient(appId, apiKey, options.transformation.region, options); + } + return { ...client, + async saveObjectsWithTransformation({ indexName, objects, waitForTasks }, requestOptions): Promise { + if (!ingestionTransporter) { + throw new Error('`transformation.region` must be provided at client instantiation before calling this method.'); + } + + if (!options?.transformation?.region) { + throw new Error('`region` must be provided when leveraging the transformation pipeline'); + } + + return ingestionTransporter?.push( + { + indexName, + watch: waitForTasks, + pushTaskPayload: { + action: 'addObject', + records: objects as PushTaskRecords[], + }, + }, + requestOptions, + ); + }, + + async partialUpdateObjectsWithTransformation( + { indexName, objects, createIfNotExists, waitForTasks }, + requestOptions, + ): Promise { + if (!ingestionTransporter) { + throw new Error('`transformation.region` must be provided at client instantiation before calling this method.'); + } + + if (!options?.transformation?.region) { + throw new Error('`region` must be provided when leveraging the transformation pipeline'); + } + + return ingestionTransporter?.push( + { + indexName, + watch: waitForTasks, + pushTaskPayload: { + action: createIfNotExists ? 'partialUpdateObject' : 'partialUpdateObjectNoCreate', + records: objects as PushTaskRecords[], + }, + }, + requestOptions, + ); + }, + /** * Get the value of the `algoliaAgent`, used by our libraries internally and telemetry system. */ diff --git a/templates/javascript/clients/client/api/helpers.mustache b/templates/javascript/clients/client/api/helpers.mustache index 3ad1d48443..7cb260944c 100644 --- a/templates/javascript/clients/client/api/helpers.mustache +++ b/templates/javascript/clients/client/api/helpers.mustache @@ -318,7 +318,7 @@ async chunkedBatch({ indexName, objects, action = 'addObject', waitForTasks, bat * @param saveObjects - The `saveObjects` object. * @param saveObjects.indexName - The `indexName` to save `objects` in. * @param saveObjects.objects - The array of `objects` to store in the given Algolia `indexName`. - * @param chunkedBatch.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. + * @param saveObjects.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. * @param saveObjects.waitForTasks - Whether or not we should wait until every `batch` tasks has been processed, this operation may slow the total execution time of this method but is more reliable. * @param requestOptions - The requestOptions to send along with the query, they will be forwarded to the `batch` method and merged with the transporter requestOptions. */ @@ -339,7 +339,7 @@ async saveObjects( * @param deleteObjects - The `deleteObjects` object. * @param deleteObjects.indexName - The `indexName` to delete `objectIDs` from. * @param deleteObjects.objectIDs - The objectIDs to delete. - * @param chunkedBatch.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. + * @param deleteObjects.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. * @param deleteObjects.waitForTasks - Whether or not we should wait until every `batch` tasks has been processed, this operation may slow the total execution time of this method but is more reliable. * @param requestOptions - The requestOptions to send along with the query, they will be forwarded to the `batch` method and merged with the transporter requestOptions. */ @@ -367,7 +367,7 @@ async deleteObjects( * @param partialUpdateObjects.indexName - The `indexName` to update `objects` in. * @param partialUpdateObjects.objects - The array of `objects` to update in the given Algolia `indexName`. * @param partialUpdateObjects.createIfNotExists - To be provided if non-existing objects are passed, otherwise, the call will fail.. - * @param chunkedBatch.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. + * @param partialUpdateObjects.batchSize - The size of the chunk of `objects`. The number of `batch` calls will be equal to `length(objects) / batchSize`. Defaults to 1000. * @param partialUpdateObjects.waitForTasks - Whether or not we should wait until every `batch` tasks has been processed, this operation may slow the total execution time of this method but is more reliable. * @param requestOptions - The requestOptions to send along with the query, they will be forwarded to the `getTask` method and merged with the transporter requestOptions. */