diff --git a/scripts/cli/utils.ts b/scripts/cli/utils.ts index 87ae04583e..5a1296096c 100644 --- a/scripts/cli/utils.ts +++ b/scripts/cli/utils.ts @@ -25,13 +25,17 @@ type Prompt = { interactive: boolean; }; -export function getClientChoices(job: Job, language?: LangArg): string[] { - const withoutAlgoliaSearch = PROMPT_CLIENTS.filter( +export function getClientChoices( + job: Job, + language?: LangArg, + clientList = PROMPT_CLIENTS +): string[] { + const withoutAlgoliaSearch = clientList.filter( (client) => client !== 'algoliasearch' ); if (!language) { - return job === 'specs' ? withoutAlgoliaSearch : PROMPT_CLIENTS; + return job === 'specs' ? withoutAlgoliaSearch : clientList; } const isJavaScript = language === ALL || language === 'javascript'; @@ -41,7 +45,7 @@ export function getClientChoices(job: Job, language?: LangArg): string[] { case 'build': // Only `JavaScript` provide a lite client, others can build anything but it. if (isJavaScript) { - return PROMPT_CLIENTS.filter((client) => client !== 'lite'); + return clientList.filter((client) => client !== 'lite'); } return withoutAlgoliaSearch.filter((client) => client !== 'lite'); @@ -56,7 +60,7 @@ export function getClientChoices(job: Job, language?: LangArg): string[] { return withoutAlgoliaSearch.filter((client) => client !== 'lite'); default: - return PROMPT_CLIENTS; + return clientList; } } @@ -107,7 +111,7 @@ export async function prompt({ decision.language = langArg; } - decision.clientList = getClientChoices(job, decision.language); + decision.clientList = getClientChoices(job, decision.language, CLIENTS); if (!clientArg || !clientArg.length) { if (interactive) { @@ -117,7 +121,7 @@ export async function prompt({ name: 'client', message: 'Select a client', default: ALL, - choices: decision.clientList, + choices: getClientChoices(job, decision.language), }, ]); @@ -125,7 +129,7 @@ export async function prompt({ } } else { clientArg.forEach((client) => { - if (!decision.clientList.includes(client)) { + if (!PROMPT_CLIENTS.includes(client)) { throw new Error( `The '${clientArg}' client can't run with the given job: '${job}'.\n\nAllowed choices are: ${decision.clientList.join( ', ' diff --git a/templates/javascript/api-single.mustache b/templates/javascript/api-single.mustache index d29ec1a480..74484f2ce7 100644 --- a/templates/javascript/api-single.mustache +++ b/templates/javascript/api-single.mustache @@ -18,7 +18,7 @@ export function create{{capitalizedApiName}}({ hosts: getDefaultHosts({{^hasRegionalHost}}appIdOption{{/hasRegionalHost}}{{#hasRegionalHost}}regionOption{{/hasRegionalHost}}), ...options, algoliaAgent: getAlgoliaAgent({ - algoliaAgents: algoliaAgents, + algoliaAgents, client: '{{{algoliaAgent}}}', version: apiClientVersion, }), @@ -40,30 +40,7 @@ export function create{{capitalizedApiName}}({ return { addAlgoliaAgent, {{#isSearchClient}} - /** - * Wait for a task to complete with `indexName` and `taskID`. - * - * @summary Wait for a task to complete. - * @param waitForTaskProps - The waitForTaskProps object. - * @param waitForTaskProps.indexName - The index in which to perform the request. - * @param waitForTaskProps.taskID - The unique identifier of the task to wait for. - */ - waitForTask({ - indexName, - taskID, - ...createRetryablePromiseOptions, - }: { - indexName: string; - taskID: number; - } & Omit, 'func' | 'validate'>): Promise { - return new Promise((resolve, reject) => { - createRetryablePromise({ - ...createRetryablePromiseOptions, - func: () => this.getTask({ indexName, taskID }), - validate: (response) => response.status === 'published', - }).then(() => resolve()).catch(reject); - }); - }, + {{> api/helpers}} {{/isSearchClient}} {{#operation}} {{> api/operation/jsdoc}} diff --git a/templates/javascript/api/helpers.mustache b/templates/javascript/api/helpers.mustache new file mode 100644 index 0000000000..95712383ed --- /dev/null +++ b/templates/javascript/api/helpers.mustache @@ -0,0 +1,65 @@ +/** + * Helper: Wait for a task to complete with `indexName` and `taskID`. + * + * @summary Wait for a task to complete. + * @param waitForTaskOptions - The waitForTaskOptions object. + * @param waitForTaskOptions.indexName - The `indexName` where the operation was performed. + * @param waitForTaskOptions.taskID - The `taskID` returned in the method response. + */ +waitForTask({ + indexName, + taskID, + ...createRetryablePromiseOptions +}: WaitForTaskOptions): Promise { + return createRetryablePromise({ + ...createRetryablePromiseOptions, + func: () => this.getTask({ indexName, taskID }), + validate: (response) => response.status === 'published', + }); +}, + +/** + * Helper: Wait for an API key to be valid, updated or deleted based on a given `operation`. + * + * @summary Wait for an API key task to be processed. + * @param waitForApiKeyOptions - The waitForApiKeyOptions object. + * @param waitForApiKeyOptions.operation - The `operation` that was done on a `key`. + * @param waitForApiKeyOptions.key - The `key` that has been added, deleted or updated. + * @param waitForApiKeyOptions.apiKey - Necessary to know if an `update` operation has been processed, compare fields of the response with it. + */ +waitForApiKey({ + operation, + key, + apiKey, + ...createRetryablePromiseOptions +}: WaitForApiKeyOptions): Promise { + if (operation === 'update') { + return createRetryablePromise({ + ...createRetryablePromiseOptions, + func: () => this.getApiKey({ key }), + validate: (response) => { + for (const [entry, values] of Object.entries(apiKey)) { + if (Array.isArray(values)) { + if ( + values.length !== response[entry].length || + values.some((val, index) => val !== response[entry][index]) + ) { + return false; + } + } else if (values !== response[entry]) { + return false; + } + } + + return true; + }, + }); + } + + return createRetryablePromise({ + ...createRetryablePromiseOptions, + func: () => this.getApiKey({ key }).catch((error) => error), + validate: (error: ApiError) => + operation === 'add' ? error.status !== 404 : error.status === 404, + }); +}, diff --git a/templates/javascript/api/imports.mustache b/templates/javascript/api/imports.mustache index 9cd54b1a84..e49d5e4643 100644 --- a/templates/javascript/api/imports.mustache +++ b/templates/javascript/api/imports.mustache @@ -15,7 +15,7 @@ import type { RequestOptions, QueryParameters, {{#isSearchClient}} - CreateRetryablePromiseOptions, + ApiError, {{/isSearchClient}} } from '{{{npmNamespace}}}/client-common'; @@ -25,6 +25,10 @@ import { {{classname}} } from '{{filename}}'; {{#operations}} import type { + {{#isSearchClient}} + WaitForTaskOptions, + WaitForApiKeyOptions, + {{/isSearchClient}} {{#operation}} {{#vendorExtensions}} {{#x-create-wrapping-object}} diff --git a/templates/javascript/clientMethodProps.mustache b/templates/javascript/clientMethodProps.mustache index 85a86dfb8c..8d0dd38108 100644 --- a/templates/javascript/clientMethodProps.mustache +++ b/templates/javascript/clientMethodProps.mustache @@ -7,6 +7,9 @@ import { {{classname}} } from '{{filename}}'; {{! Imports for the legacy search method signature }} {{#operations}}{{#operation}}{{#vendorExtensions.x-legacy-signature}}{{> api/operation/legacySearchCompatible/imports}}{{/vendorExtensions.x-legacy-signature}}{{/operation}}{{/operations}} +{{! Imports for the helpers method of the search client }} +{{#isSearchClient}}import type { CreateRetryablePromiseOptions } from '@experimental-api-clients-automation/client-common';{{/isSearchClient}} + {{#operations}} {{#operation}} @@ -33,4 +36,48 @@ export type {{#lambda.titlecase}}{{nickname}}{{/lambda.titlecase}}Props = { {{/operation}} {{/operations}} + +{{#isSearchClient}} +type WaitForOptions = Omit< + CreateRetryablePromiseOptions, + 'func' | 'validate' +>; + +export type WaitForTaskOptions = WaitForOptions & { + /** + * The `indexName` where the operation was performed. + */ + indexName: string; + /** + * The `taskID` returned by the method response. + */ + taskID: number; +}; + +export type WaitForApiKeyOptions = WaitForOptions & { + /** + * The API Key. + */ + key: string; +} & ( + | { + /** + * The operation that has been performed, used to compute the stop condition. + */ + operation: 'add' | 'delete'; + apiKey?: never; + } + | { + /** + * The operation that has been performed, used to compute the stop condition. + */ + operation: 'update'; + /** + * The updated fields, used to compute the stop condition. + */ + apiKey: Partial; + } + ); +{{/isSearchClient}} + {{/apiInfo.apis.0}} diff --git a/website/docs/clients/guides/copy-index-to-another-application.md b/website/docs/clients/guides/copy-index-to-another-application.md index d6ff43e9db..cb31dfa81d 100644 --- a/website/docs/clients/guides/copy-index-to-another-application.md +++ b/website/docs/clients/guides/copy-index-to-another-application.md @@ -1,5 +1,5 @@ --- -title: Copy or move an index +title: Copy an index to another application --- :::caution diff --git a/website/docs/clients/guides/wait-for-api-key-to-be-valid.mdx b/website/docs/clients/guides/wait-for-api-key-to-be-valid.mdx new file mode 100644 index 0000000000..9ece4901b4 --- /dev/null +++ b/website/docs/clients/guides/wait-for-api-key-to-be-valid.mdx @@ -0,0 +1,53 @@ +--- +title: Wait for an API key to be valid +--- + +import { TabsLanguage } from '../../../src/components/TabsLanguage'; +import TabItem from '@theme/TabItem'; + +> The `waitForApiKey` method is only available in the `search` client context. + +Adding, updating or deleting API keys is not always instantaneous, which is why you might want to ensure the job has been processed before jumping to an other task. + +We provide a `waitForApiKey` helper method for you to easily wait for a specific `operation` made on a `key`. + + + + +> An `operation` can either be `add` | `update` | `delete` + +```js +import { algoliasearch } from '@experimental-api-clients-automation/algoliasearch'; + +const client = algoliasearch('', ''); + +const { key } = await client.addApiKey({ + acl: ['analytics', 'browse', 'editSettings'], +}); + +// Poll the task status with defaults values +await client.waitForApiKey({ operation: 'add', key }); + +// The fields to update on your API key +const updatesToPerform: ApiKey = { + acl: ['analytics', 'search'], + indexes: ['products'], +}; + +// Call for update +await client.updateApiKey({ + key, + apiKey: updatesToPerform, +}); + +// Wait for update to be done +await client.waitForApiKey({ + operation: 'update', + key, + // We provide the updated fields to check if the changes have been applied + apiKey: updatesToPerform, +}); +``` + + + diff --git a/website/sidebars.js b/website/sidebars.js index 91657cbc13..63cd42f84f 100644 --- a/website/sidebars.js +++ b/website/sidebars.js @@ -79,6 +79,7 @@ const sidebars = { 'clients/guides/retrieving-facets', 'clients/guides/customized-client-usage', 'clients/guides/wait-for-a-task-to-finish', + 'clients/guides/wait-for-api-key-to-be-valid', 'clients/guides/copy-or-move-index', 'clients/guides/copy-index-to-another-application', ], diff --git a/website/src/components/TabsLanguage.js b/website/src/components/TabsLanguage.js index 21638c270f..8adba2364d 100644 --- a/website/src/components/TabsLanguage.js +++ b/website/src/components/TabsLanguage.js @@ -9,7 +9,7 @@ export const languagesTabValues = [ export function TabsLanguage(props) { return ( - + {props.children} );