From 914ba48053eb7c69b339b10c52cf8561e65dbec0 Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Wed, 29 May 2024 10:35:54 +0200 Subject: [PATCH 01/12] fix: correct link to changelog (#161) --- Makefile | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 614dfe0f..18ca53fb 100644 --- a/Makefile +++ b/Makefile @@ -44,7 +44,7 @@ endif git -C $(DOCS_FOLDER) checkout -B feat/cli-'$(GITHUB_REF:refs/tags/v%=%)' git -C $(DOCS_FOLDER) commit -m 'feat: update cli commands data for $(GITHUB_REF:refs/tags/v%=%) version' || true git -C $(DOCS_FOLDER) push --set-upstream origin feat/cli-'$(GITHUB_REF:refs/tags/v%=%)' - cd $(DOCS_FOLDER); gh pr create -f -b "Changelog: https://github.com/algolia/cli/releases/tag/$(GITHUB_REF:refs/tags/v%=%)" + cd $(DOCS_FOLDER); gh pr create -f -b "Changelog: https://github.com/algolia/cli/releases/tag/$(GITHUB_REF:refs/tags/%=%)" ## Create a new PR (or update the existing one) to update the API specs api-specs-pr: @@ -77,4 +77,4 @@ install: # Uninstall Algolia CLI uninstall: rm ${bindir}/algolia -.PHONY: uninstall \ No newline at end of file +.PHONY: uninstall From 83400e39977ededaa330bf17a97acd7c2faba3ae Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Mon, 10 Jun 2024 10:14:43 +0200 Subject: [PATCH 02/12] fix: correct link to changelog (#161) (#163) --- pkg/cmd/rules/import/import.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmd/rules/import/import.go b/pkg/cmd/rules/import/import.go index 8a5a3a73..ce312c0a 100644 --- a/pkg/cmd/rules/import/import.go +++ b/pkg/cmd/rules/import/import.go @@ -65,7 +65,7 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co $ algolia rules browse SERIES | algolia rules import MOVIES -F - # Import rules from the "rules.ndjson" file to the "MOVIES" index and don't forward them to the index replicas - $ algolia import MOVIES -F rules.ndjson -f=false + $ algolia rules import MOVIES -F rules.ndjson -f=false `), RunE: func(cmd *cobra.Command, args []string) error { opts.Indice = args[0] From f960c07372bcea804fd835169674941546e7fbd1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Denoix?= Date: Thu, 13 Jun 2024 12:51:43 +0200 Subject: [PATCH 03/12] chore: update search api specs (#162) Co-authored-by: algolia-ci --- api/specs/search.yml | 248 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 220 insertions(+), 28 deletions(-) diff --git a/api/specs/search.yml b/api/specs/search.yml index 8243d1cb..31a2c738 100644 --- a/api/specs/search.yml +++ b/api/specs/search.yml @@ -1012,17 +1012,7 @@ paths: content: application/json: schema: - title: batchResponse - type: object - additionalProperties: false - properties: - taskID: - $ref: '#/components/schemas/taskID' - objectIDs: - $ref: '#/components/schemas/objectIDs' - required: - - taskID - - objectIDs + $ref: '#/components/schemas/batchResponse' '400': $ref: '#/components/responses/BadRequest' '402': @@ -2927,6 +2917,45 @@ paths: $ref: '#/components/responses/MethodNotAllowed' '404': $ref: '#/components/responses/IndexNotFound' + /1/task/{taskID}: + get: + tags: + - search + operationId: getAppTask + x-acl: + - editSettings + description: | + Checks the status of a given application task. + summary: Check application task status + parameters: + - name: taskID + in: path + description: Unique task identifier. + required: true + schema: + type: integer + format: int64 + example: 1506303845001 + responses: + '200': + description: OK + content: + application/json: + schema: + title: getTaskResponse + type: object + additionalProperties: false + properties: + status: + $ref: '#/components/schemas/taskStatus' + required: + - status + '400': + $ref: '#/components/responses/BadRequest' + '402': + $ref: '#/components/responses/FeatureNotEnabled' + '403': + $ref: '#/components/responses/MethodNotAllowed' /1/indexes/{indexName}/task/{taskID}: get: tags: @@ -2993,8 +3022,8 @@ paths: Copies or moves (renames) an index within the same Algolia application. - - Existing destination indices are overwritten, except for - index-specific API keys and analytics data. + - Existing destination indices are overwritten, except for their + analytics data. - If the destination index doesn't exist yet, it'll be created. @@ -3207,6 +3236,134 @@ paths: type: string '400': $ref: '#/components/responses/IndexNotFound' + /replaceAllObjects: + get: + x-helper: true + tags: + - search + operationId: replaceAllObjects + summary: Replace all records in an index + description: > + Replace all records from your index with a new set of records. + + + This method lets you replace all records in your index without downtime. + It performs these operations: + 1. Copy settings, synonyms, and rules from your original index to a temporary index. + 2. Add your new records to the temporary index. + 3. Replace your original index with the temporary index. + + Use the safe parameter to ensure that these (asynchronous) operations + are performed in sequence. + + If there's an error duing one of these steps, the temporary index won't + be deleted. + + This operation is rate-limited. + + This method creates a temporary index: your record count is temporarily + doubled. Algolia doesn't count the three days with the highest number of + records towards your monthly usage. + + If you're on a legacy plan (before July 2020), this method counts two + operations towards your usage (in addition to the number of records): + copySettings and moveIndex. + + The API key you use for this operation must have access to the index + YourIndex and the temporary index YourIndex_tmp. + parameters: + - in: query + name: indexName + description: The `indexName` to replace `objects` in. + required: true + schema: + type: string + - in: query + name: objects + description: List of objects to replace the current objects with. + required: true + schema: + type: array + items: + type: object + - in: query + name: batchSize + description: >- + The size of the chunk of `objects`. The number of `batch` calls will + be equal to `length(objects) / batchSize`. Defaults to 1000. + required: false + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/replaceAllObjectsResponse' + '400': + $ref: '#/components/responses/IndexNotFound' + /chunkedBatch: + get: + x-helper: true + tags: + - search + operationId: chunkedBatch + summary: Replace all records in an index + description: > + Helper: Chunks the given `objects` list in subset of 1000 elements max + in order to make it fit in `batch` requests. + parameters: + - in: query + name: indexName + description: The `indexName` to replace `objects` in. + required: true + schema: + type: string + - in: query + name: objects + description: List of objects to replace the current objects with. + required: true + schema: + type: array + items: + type: object + - in: query + name: action + description: >- + The `batch` `action` to perform on the given array of `objects`, + defaults to `addObject`. + required: false + schema: + $ref: '#/components/schemas/action' + - in: query + name: waitForTasks + description: >- + 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. + required: false + schema: + type: boolean + - in: query + name: batchSize + description: >- + The size of the chunk of `objects`. The number of `batch` calls will + be equal to `length(objects) / batchSize`. Defaults to 1000. + required: false + schema: + type: integer + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/batchResponse' + '400': + $ref: '#/components/responses/IndexNotFound' components: securitySchemes: appId: @@ -3260,6 +3417,7 @@ components: ForwardToReplicas: in: query name: forwardToReplicas + required: false description: Whether changes are applied to replica indices. schema: type: boolean @@ -3314,6 +3472,7 @@ components: description: | Requested page of the API response. If `null`, the API response is not paginated. + required: false schema: oneOf: - type: integer @@ -3324,6 +3483,7 @@ components: in: query name: hitsPerPage description: Number of hits per page. + required: false schema: type: integer default: 100 @@ -5522,13 +5682,10 @@ components: type: boolean description: Whether the record is re-ranked. required: - - promoted - nbTypos - firstMatchedWord - geoDistance - nbExactWords - - words - - filters - userScore _distinctSeqID: type: integer @@ -5788,6 +5945,18 @@ components: $ref: '#/components/schemas/insideBoundingBox' insidePolygon: $ref: '#/components/schemas/insidePolygon' + updatedAtResponse: + type: object + description: Response, taskID, and update timestamp. + additionalProperties: false + required: + - taskID + - updatedAt + properties: + taskID: + $ref: '#/components/schemas/taskID' + updatedAt: + $ref: '#/components/schemas/updatedAt' builtInOperationType: type: string enum: @@ -5837,6 +6006,17 @@ components: - record-1 - record-2 description: Unique record identifiers. + batchResponse: + type: object + additionalProperties: false + properties: + taskID: + $ref: '#/components/schemas/taskID' + objectIDs: + $ref: '#/components/schemas/objectIDs' + required: + - taskID + - objectIDs baseIndexSettings: type: object title: Index settings. @@ -7255,6 +7435,29 @@ components: an issue if many users search from the same IP address. To avoid this, add a user token to each generated API key. + replaceAllObjectsResponse: + type: object + additionalProperties: false + properties: + copyOperationResponse: + description: >- + The response of the `operationIndex` request for the `copy` + operation. + $ref: '#/components/schemas/updatedAtResponse' + batchResponses: + type: array + description: The response of the `batch` request(s). + items: + $ref: '#/components/schemas/batchResponse' + moveOperationResponse: + description: >- + The response of the `operationIndex` request for the `move` + operation. + $ref: '#/components/schemas/updatedAtResponse' + required: + - copyOperationResponse + - batchResponses + - moveOperationResponse responses: BadRequest: description: Bad request or request arguments. @@ -7320,18 +7523,7 @@ components: content: application/json: schema: - title: updatedAtResponse - description: Response, taskID, and update timestamp. - additionalProperties: false - type: object - required: - - taskID - - updatedAt - properties: - taskID: - $ref: '#/components/schemas/taskID' - updatedAt: - $ref: '#/components/schemas/updatedAt' + $ref: '#/components/schemas/updatedAtResponse' CreatedAt: description: OK content: From 0e3184a3c450fc38bd3558d912c75aa280ffc935 Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Mon, 22 Jul 2024 07:54:35 +0200 Subject: [PATCH 04/12] fix: examples for dictionary entries clear (#167) --- pkg/cmd/dictionary/entries/clear/clear.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/dictionary/entries/clear/clear.go b/pkg/cmd/dictionary/entries/clear/clear.go index 70717a1c..7c8513a5 100644 --- a/pkg/cmd/dictionary/entries/clear/clear.go +++ b/pkg/cmd/dictionary/entries/clear/clear.go @@ -59,11 +59,11 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm This command deletes all entries from the specified %s dictionaries. `, cs.Bold("custom")), Example: heredoc.Doc(` - # Delete all entries from the "stopword" dictionary - $ algolia dictionary entries clear stopword + # Delete all entries from the "stopwords" dictionary + $ algolia dictionary entries clear stopwords - # Delete all entries from the "stopword" and "plural" dictionaries - $ algolia dictionary entries clear stopword plural + # Delete all entries from the "stopwords" and "plurals" dictionaries + $ algolia dictionary entries clear stopwords plurals # Delete all entries from all dictionaries $ algolia dictionary entries clear --all From e922d0de62d54fedec4859c6db13ced7221cd42e Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Mon, 22 Jul 2024 07:59:20 +0200 Subject: [PATCH 05/12] fix: handle virtual replicas (#168) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Clément Denoix --- pkg/cmd/indices/delete/delete.go | 52 ++++++++++++++++++++++++++------ 1 file changed, 43 insertions(+), 9 deletions(-) diff --git a/pkg/cmd/indices/delete/delete.go b/pkg/cmd/indices/delete/delete.go index 1157fe12..4f5dc26f 100644 --- a/pkg/cmd/indices/delete/delete.go +++ b/pkg/cmd/indices/delete/delete.go @@ -2,6 +2,7 @@ package delete import ( "fmt" + "regexp" "strings" "github.com/MakeNowJust/heredoc" @@ -66,7 +67,9 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -80,7 +83,8 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co } cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") - cmd.Flags().BoolVarP(&opts.IncludeReplicas, "includeReplicas", "r", false, "delete replica indices too") + cmd.Flags(). + BoolVarP(&opts.IncludeReplicas, "includeReplicas", "r", false, "delete replica indices too") return cmd } @@ -117,13 +121,17 @@ func runDeleteCmd(opts *DeleteOptions) error { if opts.IncludeReplicas { settings, err := index.GetSettings() - if err != nil { return fmt.Errorf("can't get settings of index %q: %w", indexName, err) } replicas := settings.Replicas for _, replicaName := range replicas.Get() { + pattern := regexp.MustCompile(`^virtual\((.*)\)$`) + matches := pattern.FindStringSubmatch(replicaName) + if len(matches) > 1 { + replicaName = matches[1] + } replica := client.InitIndex(replicaName) indices = append(indices, replica) } @@ -151,7 +159,9 @@ func runDeleteCmd(opts *DeleteOptions) error { } if err != nil { - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprint("Deleting replica index ", index.GetName())) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprint("Deleting replica index ", index.GetName()), + ) err := deleteReplicaIndex(client, index) opts.IO.StopProgressIndicator() if err != nil { @@ -162,7 +172,12 @@ func runDeleteCmd(opts *DeleteOptions) error { cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Deleted indices %s\n", cs.SuccessIcon(), strings.Join(opts.Indices, ", ")) + fmt.Fprintf( + opts.IO.Out, + "%s Deleted indices %s\n", + cs.SuccessIcon(), + strings.Join(opts.Indices, ", "), + ) } return nil @@ -178,7 +193,12 @@ func deleteReplicaIndex(client *search.Client, replicaIndex *search.Index) error err = detachReplicaIndex(replicaName, primaryName, client) if err != nil { - return fmt.Errorf("can't unlink replica index %s from primary index %s: %w", replicaName, primaryName, err) + return fmt.Errorf( + "can't unlink replica index %s from primary index %s: %w", + replicaName, + primaryName, + err, + ) } _, err = replicaIndex.Delete() @@ -193,7 +213,6 @@ func deleteReplicaIndex(client *search.Client, replicaIndex *search.Index) error func findPrimaryIndex(replicaIndex *search.Index) (string, error) { replicaName := replicaIndex.GetName() settings, err := replicaIndex.GetSettings() - if err != nil { return "", fmt.Errorf("can't get settings of replica index %q: %w", replicaName, err) } @@ -210,12 +229,15 @@ func findPrimaryIndex(replicaIndex *search.Index) (string, error) { func detachReplicaIndex(replicaName string, primaryName string, client *search.Client) error { primaryIndex := client.InitIndex(primaryName) settings, err := primaryIndex.GetSettings() - if err != nil { return fmt.Errorf("can't get settings of primary index %q: %w", primaryName, err) } replicas := settings.Replicas.Get() + isVirtual := isVirtualReplica(replicas, replicaName) + if isVirtual { + replicaName = fmt.Sprintf("virtual(%s)", replicaName) + } indexOfReplica := findIndex(replicas, replicaName) // Delete the replica at position `indexOfReplica` from the array @@ -226,7 +248,6 @@ func detachReplicaIndex(replicaName string, primaryName string, client *search.C Replicas: opt.Replicas(replicas...), }, ) - if err != nil { return fmt.Errorf("can't update settings of index %q: %w", primaryName, err) } @@ -245,3 +266,16 @@ func findIndex(arr []string, target string) int { } return -1 } + +func isVirtualReplica(replicas []string, replicaName string) bool { + pattern := regexp.MustCompile(fmt.Sprintf(`^virtual\(%s\)$`, replicaName)) + + for _, i := range replicas { + matches := pattern.MatchString(i) + if matches { + return true + } + } + + return false +} From 666f84ea49a9ac73d78dd0968a0a0b1346e44a0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Denoix?= Date: Wed, 24 Jul 2024 15:10:55 +0900 Subject: [PATCH 06/12] chore: update search api specs (#165) Co-authored-by: algolia-ci --- api/specs/search.yml | 280 ++++++++++++++++++++++++++++++++++++++----- 1 file changed, 247 insertions(+), 33 deletions(-) diff --git a/api/specs/search.yml b/api/specs/search.yml index 31a2c738..38a0e382 100644 --- a/api/specs/search.yml +++ b/api/specs/search.yml @@ -570,10 +570,39 @@ paths: - Records are ranked by attributes and custom ranking. - - Deduplication (`distinct`) is turned off. - - There's no ranking for: typo-tolerance, number of matched words, proximity, geo distance. + + + Browse requests automatically apply these settings: + + + - `advancedSyntax`: `false` + + - `attributesToHighlight`: `[]` + + - `attributesToSnippet`: `[]` + + - `distinct`: `false` + + - `enablePersonalization`: `false` + + - `enableRules`: `false` + + - `facets`: `[]` + + - `getRankingInfo`: `false` + + - `ignorePlurals`: `false` + + - `optionalFilters`: `[]` + + - `typoTolerance`: `true` or `false` (`min` and `strict` is evaluated to + `true`) + + + If you send these parameters with your browse requests, they'll be + ignored. parameters: - $ref: '#/components/parameters/IndexName' requestBody: @@ -3145,18 +3174,18 @@ paths: summary: Wait for an API key operation description: Waits for an API key to be added, updated, or deleted. parameters: - - in: query - name: operation - description: Whether the API key was created, updated, or deleted. - required: true - schema: - $ref: '#/components/schemas/apiKeyOperation' - in: query name: key description: API key to wait for. required: true schema: type: string + - in: query + name: operation + description: Whether the API key was created, updated, or deleted. + required: true + schema: + $ref: '#/components/schemas/apiKeyOperation' - in: query name: apiKey description: >- @@ -3177,6 +3206,7 @@ paths: /generateSecuredApiKey: get: x-helper: true + x-asynchronous-helper: false tags: - search operationId: generateSecuredApiKey @@ -3214,7 +3244,7 @@ paths: key, or be more restrictive. parameters: - in: query - name: apiKey + name: parentApiKey description: >- API key from which the secured API key will inherit its restrictions. @@ -3226,7 +3256,7 @@ paths: description: Restrictions to add to the API key. required: true schema: - $ref: '#/components/schemas/securedAPIKeyRestrictions' + $ref: '#/components/schemas/securedApiKeyRestrictions' responses: '200': description: OK @@ -3364,6 +3394,146 @@ paths: $ref: '#/components/schemas/batchResponse' '400': $ref: '#/components/responses/IndexNotFound' + /saveObjects: + get: + x-helper: true + tags: + - search + operationId: saveObjects + summary: Saves the given array of objects in the given index + description: > + Helper: Saves the given array of objects in the given index. The + `chunkedBatch` helper is used under the hood, which creates a `batch` + requests with at most 1000 objects in it. + parameters: + - in: query + name: indexName + description: The `indexName` to save `objects` into. + required: true + schema: + type: string + - in: query + name: objects + description: The objects to save in the index. + required: true + schema: + type: array + items: + type: object + - in: query + name: requestOptions + description: The request options to pass to the `batch` method. + required: false + schema: + type: object + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/batchResponse' + '400': + $ref: '#/components/responses/IndexNotFound' + /deleteObjects: + post: + x-helper: true + tags: + - search + operationId: deleteObjects + summary: Deletes every records for the given objectIDs + description: > + Helper: Deletes every records for the given objectIDs. The + `chunkedBatch` helper is used under the hood, which creates a `batch` + requests with at most 1000 objectIDs in it. + parameters: + - in: query + name: indexName + description: The `indexName` to delete `objectIDs` from. + required: true + schema: + type: string + - in: query + name: objectIDs + description: The objectIDs to delete. + required: true + schema: + type: array + items: + type: string + - in: query + name: requestOptions + description: The request options to pass to the `batch` method. + required: false + schema: + type: object + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/batchResponse' + '400': + $ref: '#/components/responses/IndexNotFound' + /partialUpdateObjects: + post: + x-helper: true + tags: + - search + operationId: partialUpdateObjects + summary: >- + Replaces object content of all the given objects according to their + respective `objectID` field + description: > + Helper: Replaces object content of all the given objects according to + their respective `objectID` field. The `chunkedBatch` helper is used + under the hood, which creates a `batch` requests with at most 1000 + objects in it. + parameters: + - in: query + name: indexName + description: The `indexName` where to update `objects`. + required: true + schema: + type: string + - in: query + name: objects + description: The objects to update. + required: true + schema: + type: array + items: + type: object + - in: query + name: createIfNotExists + description: >- + To be provided if non-existing objects are passed, otherwise, the + call will fail. + required: false + schema: + type: boolean + - in: query + name: requestOptions + description: The request options to pass to the `batch` method. + required: false + schema: + type: object + responses: + '200': + description: OK + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/batchResponse' + '400': + $ref: '#/components/responses/IndexNotFound' components: securitySchemes: appId: @@ -4532,6 +4702,11 @@ components: - count - alpha - hidden + hide: + description: Hide facet values. + type: array + items: + type: string value: type: object additionalProperties: false @@ -4540,6 +4715,8 @@ components: $ref: '#/components/schemas/order' sortRemainingBy: $ref: '#/components/schemas/sortRemainingBy' + hide: + $ref: '#/components/schemas/hide' values: description: Order of facet values. One object for each facet. type: object @@ -4555,6 +4732,13 @@ components: $ref: '#/components/schemas/facets' values: $ref: '#/components/schemas/values' + redirectURL: + description: The redirect rule container. + type: object + additionalProperties: false + properties: + url: + type: string renderingContent: description: > Extra data that can be used in the search UI. @@ -4569,6 +4753,8 @@ components: properties: facetOrdering: $ref: '#/components/schemas/facetOrdering' + redirect: + $ref: '#/components/schemas/redirectURL' x-categories: - Advanced reRankingApplyFilter: @@ -5245,14 +5431,6 @@ components: oneOf: - $ref: '#/components/schemas/searchParamsString' - $ref: '#/components/schemas/searchParamsObject' - nbHits: - type: integer - description: Number of results (hits). - example: 20 - nbPages: - type: integer - description: Number of pages of results. - example: 1 processingTimeMS: type: integer description: Time the server took to process the request, in milliseconds. @@ -5287,13 +5465,14 @@ components: - dest - source userData: + type: object example: settingID: f2a7b51e3503acc6a39b3784ffb84300 pluginVersion: 1.6.0 description: | An object with custom data. - You can store up to 32 kB as custom data. + You can store up to 32kB as custom data. default: {} x-categories: - Advanced @@ -5301,10 +5480,6 @@ components: type: object additionalProperties: true required: - - nbHits - - page - - nbPages - - hitsPerPage - processingTimeMS properties: abTestID: @@ -5427,8 +5602,6 @@ components: type: number format: double description: Sum of all values in the results. - hitsPerPage: - $ref: '#/components/schemas/hitsPerPage' index: type: string example: indexName @@ -5442,16 +5615,10 @@ components: message: type: string description: Warnings about the query. - nbHits: - $ref: '#/components/schemas/nbHits' - nbPages: - $ref: '#/components/schemas/nbPages' nbSortedHits: type: integer description: Number of hits selected and sorted by the relevant sort algorithm. example: 20 - page: - $ref: '#/components/schemas/page' parsedQuery: type: string description: >- @@ -5476,7 +5643,8 @@ components: type: object description: > [Redirect results to a - URL](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects/). + URL](https://www.algolia.com/doc/guides/managing-results/rules/merchandising-and-promoting/how-to/redirects/), + this this parameter is for internal use only. properties: index: type: array @@ -5500,6 +5668,31 @@ components: Unique identifier for the query. This is used for [click analytics](https://www.algolia.com/doc/guides/analytics/click-analytics/). example: a00dbc80a8d13c4565a442e7e2dca80a + nbHits: + type: integer + description: Number of results (hits). + example: 20 + nbPages: + type: integer + description: Number of pages of results. + example: 1 + SearchPagination: + type: object + additionalProperties: false + properties: + page: + $ref: '#/components/schemas/page' + nbHits: + $ref: '#/components/schemas/nbHits' + nbPages: + $ref: '#/components/schemas/nbPages' + hitsPerPage: + $ref: '#/components/schemas/hitsPerPage' + required: + - page + - nbHits + - nbPages + - hitsPerPage objectID: type: string description: Unique record identifier. @@ -5740,6 +5933,7 @@ components: additionalProperties: true allOf: - $ref: '#/components/schemas/baseSearchResponse' + - $ref: '#/components/schemas/SearchPagination' - $ref: '#/components/schemas/searchHits' indexName: type: string @@ -5894,9 +6088,22 @@ components: oneOf: - $ref: '#/components/schemas/searchParamsString' - $ref: '#/components/schemas/browseParamsObject' + BrowsePagination: + type: object + additionalProperties: false + properties: + page: + $ref: '#/components/schemas/page' + nbHits: + $ref: '#/components/schemas/nbHits' + nbPages: + $ref: '#/components/schemas/nbPages' + hitsPerPage: + $ref: '#/components/schemas/hitsPerPage' browseResponse: allOf: - $ref: '#/components/schemas/baseSearchResponse' + - $ref: '#/components/schemas/BrowsePagination' - $ref: '#/components/schemas/searchHits' - $ref: '#/components/schemas/cursor' createdAt: @@ -7339,6 +7546,13 @@ components: example: - T02_push - T2replica + virtual: + type: boolean + description: >- + Only present if the index is a [virtual + replica](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/how-to/sort-an-index-alphabetically/#virtual-replicas). + x-categories: + - Ranking required: - name - createdAt @@ -7370,7 +7584,7 @@ components: - add - delete - update - securedAPIKeyRestrictions: + securedApiKeyRestrictions: type: object additionalProperties: false properties: From f46d38d2a15b1014af625340304afcebbea10793 Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Wed, 29 Jan 2025 14:03:26 +0100 Subject: [PATCH 07/12] fix: apply options correctly for indices config import (#175) --- pkg/cmd/indices/config/import/import.go | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/pkg/cmd/indices/config/import/import.go b/pkg/cmd/indices/config/import/import.go index bdcccfe9..eb6366e3 100644 --- a/pkg/cmd/indices/config/import/import.go +++ b/pkg/cmd/indices/config/import/import.go @@ -123,22 +123,21 @@ func runImportCmd(opts *config.ImportOptions) error { if err != nil { return err } - _, err = indice.SaveSynonyms(synonyms, - []interface{}{ - opt.ForwardToReplicas(opts.ForwardSynonymsToReplicas), - opt.ReplaceExistingSynonyms(opts.ClearExistingSynonyms), - }, + _, err = indice.SaveSynonyms( + synonyms, + opt.ForwardToReplicas(opts.ForwardSynonymsToReplicas), + opt.ReplaceExistingSynonyms(opts.ClearExistingSynonyms), ) if err != nil { return fmt.Errorf("%s An error occurred when saving synonyms: %w", cs.FailureIcon(), err) } } if len(opts.ImportConfig.Rules) > 0 && utils.Contains(opts.Scope, "rules") { - _, err = indice.SaveRules(opts.ImportConfig.Rules, - []interface{}{ - opt.ForwardToReplicas(opts.ForwardRulesToReplicas), - opt.ClearExistingRules(opts.ClearExistingRules), - }) + _, err = indice.SaveRules( + opts.ImportConfig.Rules, + opt.ForwardToReplicas(opts.ForwardRulesToReplicas), + opt.ClearExistingRules(opts.ClearExistingRules), + ) if err != nil { return fmt.Errorf("%s An error occurred when saving rules: %w", cs.FailureIcon(), err) } From 087b7c663d5c49247cc2cf2caf6d9c5b5419486f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Say?= Date: Tue, 11 Mar 2025 09:57:52 +0100 Subject: [PATCH 08/12] feat: bump max buffer (#116) --- pkg/cmdutil/file_input.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/cmdutil/file_input.go b/pkg/cmdutil/file_input.go index ba9a16d9..513411eb 100644 --- a/pkg/cmdutil/file_input.go +++ b/pkg/cmdutil/file_input.go @@ -7,7 +7,7 @@ import ( "os" ) -const maxCapacity = 1024 * 1024 // 1MB +const maxCapacity = 1024 * 5120 // 5MB func ReadFile(filename string, stdin io.ReadCloser) ([]byte, error) { if filename == "-" { From 4776dc5f5e73ac60b310f14560dc9b5b15473caa Mon Sep 17 00:00:00 2001 From: gazconroy Date: Tue, 11 Mar 2025 10:14:30 +0000 Subject: [PATCH 09/12] fix: Editorial changes to the CLI docs (#173) * fix: Algolia CLI editorial update * Ignore docs directory * Update pkg/cmd/crawler/reindex/reindex.go Co-authored-by: Dylan Tientcheu * fix: tests --------- Co-authored-by: Gary Conroy Co-authored-by: Gary Conroy Co-authored-by: Dylan Tientcheu --- .gitignore | 5 +- Makefile | 2 +- pkg/cmd/apikeys/create/create.go | 47 ++++--- pkg/cmd/apikeys/delete/delete.go | 4 +- pkg/cmd/apikeys/get/get.go | 2 +- pkg/cmd/apikeys/list/list.go | 2 +- pkg/cmd/crawler/crawl/crawl.go | 8 +- pkg/cmd/crawler/pause/pause.go | 2 +- pkg/cmd/crawler/reindex/reindex.go | 2 +- pkg/cmd/crawler/run/run.go | 6 +- pkg/cmd/crawler/test/test.go | 5 +- pkg/cmd/dictionary/entries/browse/browse.go | 20 +-- pkg/cmd/dictionary/entries/clear/clear.go | 4 +- pkg/cmd/dictionary/entries/delete/delete.go | 2 +- pkg/cmd/dictionary/entries/entries.go | 4 +- pkg/cmd/dictionary/entries/import/import.go | 4 +- pkg/cmd/dictionary/settings/set/set.go | 16 +-- pkg/cmd/dictionary/settings/settings.go | 2 +- pkg/cmd/indices/clear/clear.go | 6 +- pkg/cmd/indices/config/export/export.go | 6 +- pkg/cmd/indices/copy/copy.go | 8 +- pkg/cmd/indices/delete/delete.go | 11 +- pkg/cmd/indices/move/move.go | 6 +- pkg/cmd/objects/browse/browse.go | 12 +- pkg/cmd/objects/delete/delete.go | 18 +-- pkg/cmd/objects/import/import.go | 16 +-- pkg/cmd/objects/objects.go | 2 +- pkg/cmd/objects/update/update.go | 20 +-- pkg/cmd/open/open.go | 20 +-- pkg/cmd/profile/application.go | 2 +- pkg/cmd/profile/remove/remove.go | 2 +- pkg/cmd/rules/browse/browse.go | 2 +- pkg/cmd/rules/delete/delete.go | 6 +- pkg/cmd/rules/import/import.go | 14 +- pkg/cmd/search/search.go | 14 +- pkg/cmd/settings/import/import.go | 4 +- pkg/cmd/settings/set/set.go | 4 +- pkg/cmd/settings/settings.go | 2 +- pkg/cmd/synonyms/browse/browse.go | 6 +- pkg/cmd/synonyms/delete/delete.go | 6 +- pkg/cmd/synonyms/import/import.go | 6 +- pkg/cmd/synonyms/save/save.go | 32 ++--- pkg/cmdutil/jsonpath_flags.go | 12 +- pkg/cmdutil/print_flags.go | 2 +- pkg/cmdutil/spec_flags.go | 134 ++++++++++---------- 45 files changed, 257 insertions(+), 253 deletions(-) diff --git a/.gitignore b/.gitignore index 7f7bde97..1f042521 100644 --- a/.gitignore +++ b/.gitignore @@ -16,4 +16,7 @@ vendor/ # local build -algolia \ No newline at end of file +algolia + +# Ignore docs directory +docs/ \ No newline at end of file diff --git a/Makefile b/Makefile index 18ca53fb..25566f01 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ VARIATION ?= old ifeq ($(VARIATION),old) DOCS_FOLDER = docs DOCS_GENERATED_PATH = app_data/cli/commands -DOCS_REPO_URL = https://github.com/algolia/doc.git +DOCS_REPO_URL = git@github.com:algolia/doc.git DOCS_BRANCH = master DOCS_EXTENSION = yml else ifeq ($(VARIATION),new) diff --git a/pkg/cmd/apikeys/create/create.go b/pkg/cmd/apikeys/create/create.go index aac5f274..b9f55ccc 100644 --- a/pkg/cmd/apikeys/create/create.go +++ b/pkg/cmd/apikeys/create/create.go @@ -61,42 +61,41 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co } cmd.Flags().StringSliceVar(&opts.ACL, "acl", nil, heredoc.Docf(` - ACL of the API Key. - - %[1]ssearch%[1]s: allowed to perform search operations. - %[1]sbrowse%[1]s: allowed to retrieve all index data with the browse endpoint. - %[1]saddObject%[1]s: allowed to add or update a records in the index. - %[1]sdeleteObject%[1]s: allowed to delete an existing record. - %[1]slistIndexes%[1]s: allowed to get a list of all existing indices. - %[1]sdeleteIndex%[1]s: allowed to delete an index. - %[1]ssettings%[1]s: allowed to read all index settings. - %[1]seditSettings%[1]s: allowed to update all index settings. - %[1]sanalytics%[1]s: allowed to retrieve data with the Analytics API. - %[1]srecommendation%[1]s: allowed to interact with the Recommendation API. - %[1]susage%[1]s: allowed to retrieve data with the Usage API. - %[1]slogs%[1]s: allowed to query the logs. - %[1]sseeUnretrievableAttributes%[1]s: allowed to retrieve unretrievableAttributes for all operations that return records. + API key's ACL. + + %[1]ssearch%[1]s: can perform search operations. + %[1]sbrowse%[1]s: can retrieve all index data with the browse endpoint. + %[1]saddObject%[1]s: can add or update records in the index. + %[1]sdeleteObject%[1]s: can delete an existing record. + %[1]slistIndexes%[1]s: can get a list of all indices. + %[1]sdeleteIndex%[1]s: can delete an index. + %[1]ssettings%[1]s: can read all index settings. + %[1]seditSettings%[1]s: can update all index settings. + %[1]sanalytics%[1]s: can retrieve data with the Analytics API. + %[1]srecommendation%[1]s: can interact with the Recommendation API. + %[1]susage%[1]s: can retrieve data with the Usage API. + %[1]slogs%[1]s: can query the logs. + %[1]sseeUnretrievableAttributes%[1]s: can retrieve unretrievableAttributes for all operations that return records. `, "`")) cmd.Flags().StringSliceVarP(&opts.Indices, "indices", "i", nil, heredoc.Docf(` - Specify the list of targeted indices. - You can target all indices starting with a prefix or ending with a suffix using the %[1]s*%[1]s character. - For example, %[1]sdev_*%[1]s matches all indices starting with %[1]sdev_%[1]s and %[1]s*_dev%[1]s matches all indices ending with %[1]s_dev%[1]s. + Index names or patterns that this API key can access. By default, an API key can access all indices in the same application. + + You can use leading and trailing wildcard characters (%[1]s*%[1]s). + For example, %[1]sdev_*%[1]s matches all indices starting with %[1]sdev_%[1]s. %[1]s*_dev%[1]s matches all indices ending with %[1]s_dev%[1]s. %[1]s*_products_*%[1]s matches all indices containing %[1]sproducts%[1]s. `, "`")) cmd.Flags().DurationVarP(&opts.Validity, "validity", "u", 0, heredoc.Doc(` - How long this API key is valid, in seconds. - A value of 0 means the API key doesn’t expire.`, + Duration (in seconds) after which the API key expires. By default (a value of 0), API keys don't expire.`, )) cmd.Flags().StringSliceVarP(&opts.Referers, "referers", "r", nil, heredoc.Docf(` Specify the list of referrers that can perform an operation. - You can use the %[1]s*%[1]s (asterisk) character as a wildcard to match subdomains, or all pages of a website. + You can use the wildcard character (%[1]s*%[1]s) to match subdomains or entire websites. `, "`")) cmd.Flags().StringVarP(&opts.Description, "description", "d", "", heredoc.Doc(` - Specify a description of the API key. - Used for informative purposes only. It has no impact on the functionality of the API key.`, + Describe an API key to help you identify its uses.`, )) _ = cmd.RegisterFlagCompletionFunc("indices", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { @@ -130,7 +129,7 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co "usage": "retrieve data with the Usage API", "logs": "query the logs", "seeUnretrievableAttributes": "retrieve unretrievableAttributes for all operations that return records", - }, "allowed to")) + }, "can")) return cmd } diff --git a/pkg/cmd/apikeys/delete/delete.go b/pkg/cmd/apikeys/delete/delete.go index 05eccfc1..2406b96d 100644 --- a/pkg/cmd/apikeys/delete/delete.go +++ b/pkg/cmd/apikeys/delete/delete.go @@ -36,7 +36,7 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "delete ", - Short: "Delete API key", + Short: "Deletes the API key", Args: validators.ExactArgs(1), Annotations: map[string]string{ "acls": "admin", @@ -58,7 +58,7 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete API key confirmation prompt") return cmd } diff --git a/pkg/cmd/apikeys/get/get.go b/pkg/cmd/apikeys/get/get.go index 78d5ed9f..3f89f9e1 100644 --- a/pkg/cmd/apikeys/get/get.go +++ b/pkg/cmd/apikeys/get/get.go @@ -37,7 +37,7 @@ func NewGetCmd(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command cmd := &cobra.Command{ Use: "get ", - Short: "Get API key", + Short: "Get the API key", Long: heredoc.Doc(` Get the details of a given API Key (ACLs, description, indexes, and other attributes). `), diff --git a/pkg/cmd/apikeys/list/list.go b/pkg/cmd/apikeys/list/list.go index e8cf28d4..c9f0e2ee 100644 --- a/pkg/cmd/apikeys/list/list.go +++ b/pkg/cmd/apikeys/list/list.go @@ -40,7 +40,7 @@ func NewListCmd(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman Annotations: map[string]string{ "acls": "admin", }, - Short: "List API keys", + Short: "Lists all API keys associated with your Algolia application, including their permissions and restrictions.", RunE: func(cmd *cobra.Command, args []string) error { if runF != nil { return runF(opts) diff --git a/pkg/cmd/crawler/crawl/crawl.go b/pkg/cmd/crawler/crawl/crawl.go index c04306ea..7ac8331a 100644 --- a/pkg/cmd/crawler/crawl/crawl.go +++ b/pkg/cmd/crawler/crawl/crawl.go @@ -39,7 +39,7 @@ func NewCrawlCmd(f *cmdutil.Factory, runF func(*CrawlOptions) error) *cobra.Comm ValidArgsFunction: cmdutil.CrawlerIDs(opts.CrawlerClient), Short: "Crawl specific URLs", Long: heredoc.Doc(` - Immediately crawl the given URLs. + Immediately crawl these URLs. The generated records are pushed to the live index if there's no ongoing reindex, and to the temporary index otherwise. `), Example: heredoc.Doc(` @@ -71,9 +71,9 @@ func NewCrawlCmd(f *cmdutil.Factory, runF func(*CrawlOptions) error) *cobra.Comm _ = cmd.MarkFlagRequired("urls") cmd.Flags().BoolVarP(&opts.Save, "save", "s", false, heredoc.Doc(` - When true, the given URLs are added to the extraUrls list of your configuration (unless already present in startUrls or sitemaps). - When false, the URLs aren't saved in the configuration. - When unspecified, the URLs are added to the extraUrls list of your configuration, but only if they haven't been indexed during the last reindex, and they aren't already present in startUrls or sitemaps. + When true, the URLs are added to your %[1]sextraUrls%[1]s (unless present in %[1]sstartUrls%[1]s or %[1]ssitemaps%[1]s). + When false, the URLs aren't added. + When unspecified, the URLs are added to your %[1]sextraUrls%[1]s (unless present in %[1]sstartUrls%[1]s or %[1]ssitemaps%[1]s or they weren't indexed during the preceding reindex). `)) return cmd diff --git a/pkg/cmd/crawler/pause/pause.go b/pkg/cmd/crawler/pause/pause.go index 766a9f3a..b604e3bd 100644 --- a/pkg/cmd/crawler/pause/pause.go +++ b/pkg/cmd/crawler/pause/pause.go @@ -35,7 +35,7 @@ func NewPauseCmd(f *cmdutil.Factory, runF func(*PauseOptions) error) *cobra.Comm ValidArgsFunction: cmdutil.CrawlerIDs(opts.CrawlerClient), Short: "Pause one or multiple crawlers", Long: heredoc.Doc(` - Request the specified crawler to pause its execution. + Pauses the specified crawler. `), Example: heredoc.Doc(` # Pause the crawler with the ID "my-crawler" diff --git a/pkg/cmd/crawler/reindex/reindex.go b/pkg/cmd/crawler/reindex/reindex.go index a4dc25cf..543ed7c6 100644 --- a/pkg/cmd/crawler/reindex/reindex.go +++ b/pkg/cmd/crawler/reindex/reindex.go @@ -33,7 +33,7 @@ func NewReindexCmd(f *cmdutil.Factory, runF func(*ReindexOptions) error) *cobra. Use: "reindex ...", Args: cobra.MinimumNArgs(1), ValidArgsFunction: cmdutil.CrawlerIDs(opts.CrawlerClient), - Short: "Reindex one or multiple crawlers", + Short: "Reindexes the specified crawlers", Long: heredoc.Doc(` Request the specified crawler to start (or restart) crawling. `), diff --git a/pkg/cmd/crawler/run/run.go b/pkg/cmd/crawler/run/run.go index 9429f068..2bd81e4b 100644 --- a/pkg/cmd/crawler/run/run.go +++ b/pkg/cmd/crawler/run/run.go @@ -33,10 +33,10 @@ func NewRunCmd(f *cmdutil.Factory, runF func(*RunOptions) error) *cobra.Command Use: "run ", Args: cobra.ExactArgs(1), ValidArgsFunction: cmdutil.CrawlerIDs(opts.CrawlerClient), - Short: "Run a crawler", + Short: "Start or resume a crawler", Long: heredoc.Doc(` - Unpause the specified crawler. If a crawl was previously ongoing, it will be resumed. - Otherwise, the crawler will go into the active state and wait for the next schedule. + Unpause the specified crawler. + Previously ongoing crawls will be resumed. Otherwise, the crawler waits for its next scheduled run. `), Example: heredoc.Doc(` # Run the crawler with the ID "my-crawler" diff --git a/pkg/cmd/crawler/test/test.go b/pkg/cmd/crawler/test/test.go index b01a0d53..91544ada 100644 --- a/pkg/cmd/crawler/test/test.go +++ b/pkg/cmd/crawler/test/test.go @@ -42,10 +42,9 @@ func NewTestCmd(f *cmdutil.Factory, runF func(*TestOptions) error) *cobra.Comman Use: "test --url [-F ]", Args: cobra.ExactArgs(1), ValidArgsFunction: cmdutil.CrawlerIDs(opts.CrawlerClient), - Short: "Test a URL on a crawler", + Short: "Tests a URL with the crawler's configuration and shows the extracted records.", Long: heredoc.Doc(` - Test an URL against the given crawler's configuration and see what will be processed. - You can also override parts of the configuration to try your changes before updating the configuration. + You can override parts of the configuration to test your changes before updating the configuration. `), Example: heredoc.Doc(` # Test the URL "https://www.example.com" against the crawler with the ID "my-crawler" diff --git a/pkg/cmd/dictionary/entries/browse/browse.go b/pkg/cmd/dictionary/entries/browse/browse.go index 8c1d40c4..05b19a80 100644 --- a/pkg/cmd/dictionary/entries/browse/browse.go +++ b/pkg/cmd/dictionary/entries/browse/browse.go @@ -28,7 +28,7 @@ type BrowseOptions struct { PrintFlags *cmdutil.PrintFlags } -// DictionaryEntry can be plural, compound or stopword entry. +// DictionaryEntry can be plural, compound, or stop word entry. type DictionaryEntry struct { Type shared.EntryType Word string `json:"word,omitempty"` @@ -63,7 +63,7 @@ func NewBrowseCmd(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co This command retrieves all entries from the specified %s dictionaries. `, cs.Bold("custom")), Example: heredoc.Doc(` - # Retrieve all entries from the "stopwords" dictionary (doesn't include default stopwords) + # Retrieve all entries from the "stopwords" dictionary (not including the Algolia default stop words) $ algolia dictionary entries browse stopwords # Retrieve all entries from the "stopwords" and "plurals" dictionaries @@ -72,7 +72,7 @@ func NewBrowseCmd(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co # Retrieve all entries from all dictionaries $ algolia dictionary entries browse --all - # Retrieve all entries from the "stopwords" dictionaries (including default stopwords) + # Retrieve all entries from the "stopwords" dictionaries (including the Algolia default stop words) $ algolia dictionary entries browse stopwords --include-defaults `), RunE: func(cmd *cobra.Command, args []string) error { @@ -120,7 +120,7 @@ func runBrowseCmd(opts *BrowseOptions) error { pageCount := 0 maxPages := 1 - // implement infinite pagination + // Infinite pagination for pageCount < maxPages { res, err := client.SearchDictionaryEntries(dictionary, "", opt.HitsPerPage(1000), opt.Page(pageCount)) if err != nil { @@ -131,13 +131,13 @@ func runBrowseCmd(opts *BrowseOptions) error { data, err := json.Marshal(res.Hits) if err != nil { - return fmt.Errorf("cannot unmarshal dictionary entries: error while marshalling original dictionary entries: %v", err) + return fmt.Errorf("can't unmarshal dictionary entries: error while marshalling original dictionary entries: %v", err) } var entries []DictionaryEntry err = json.Unmarshal(data, &entries) if err != nil { - return fmt.Errorf("cannot unmarshal dictionary entries: error while unmarshalling original dictionary entries: %v", err) + return fmt.Errorf("can't unmarshal dictionary entries: error while unmarshalling original dictionary entries: %v", err) } if len(entries) != 0 { @@ -146,12 +146,12 @@ func runBrowseCmd(opts *BrowseOptions) error { for _, entry := range entries { if opts.IncludeDefaultStopwords { - // print all entries (default stopwords included) + // Print all entries (inlcuding the default Algolia stop words) if err = p.Print(opts.IO, entry); err != nil { return err } } else if entry.Type == shared.CustomEntryType { - // print only custom entries + // Print only custom entries if err = p.Print(opts.IO, entry); err != nil { return err } @@ -161,12 +161,12 @@ func runBrowseCmd(opts *BrowseOptions) error { pageCount++ } - // in case no entry is found in all the dictionaries + // If no entry is found in all the dictionaries if hasNoEntries { if _, err = fmt.Fprintf(opts.IO.Out, "%s No entries found.\n\n", cs.WarningIcon()); err != nil { return err } - // go to the next dictionary + // Go to the next dictionary break } } diff --git a/pkg/cmd/dictionary/entries/clear/clear.go b/pkg/cmd/dictionary/entries/clear/clear.go index 7c8513a5..54617320 100644 --- a/pkg/cmd/dictionary/entries/clear/clear.go +++ b/pkg/cmd/dictionary/entries/clear/clear.go @@ -97,8 +97,8 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") - cmd.Flags().BoolVarP(&opts.All, "all", "a", false, "clear all dictionaries") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the clear dictionary entry confirmation prompt") + cmd.Flags().BoolVarP(&opts.All, "all", "a", false, "Clear all dictionaries") return cmd } diff --git a/pkg/cmd/dictionary/entries/delete/delete.go b/pkg/cmd/dictionary/entries/delete/delete.go index 357f56df..81e8f425 100644 --- a/pkg/cmd/dictionary/entries/delete/delete.go +++ b/pkg/cmd/dictionary/entries/delete/delete.go @@ -78,7 +78,7 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd.Flags().StringSliceVarP(&opts.ObjectIDs, "object-ids", "", nil, "Object IDs to delete") _ = cmd.MarkFlagRequired("object-ids") - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete dictionary entry confirmation prompt") return cmd } diff --git a/pkg/cmd/dictionary/entries/entries.go b/pkg/cmd/dictionary/entries/entries.go index 106e82f9..00127541 100644 --- a/pkg/cmd/dictionary/entries/entries.go +++ b/pkg/cmd/dictionary/entries/entries.go @@ -10,11 +10,11 @@ import ( "github.com/algolia/cli/pkg/cmdutil" ) -// NewEntriesCmd returns a new command for dictionaries' entries. +// NewEntriesCmd returns a new command for dictionary entries. func NewEntriesCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "entries", - Short: "Manage your Algolia dictionaries entries", + Short: "Manage your Algolia dictionary entries", } cmd.AddCommand(clear.NewClearCmd(f, nil)) diff --git a/pkg/cmd/dictionary/entries/import/import.go b/pkg/cmd/dictionary/entries/import/import.go index 84a0b9d8..c2b249a4 100644 --- a/pkg/cmd/dictionary/entries/import/import.go +++ b/pkg/cmd/dictionary/entries/import/import.go @@ -60,10 +60,10 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co The file must contains one single JSON object per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). `), Example: heredoc.Doc(` - # Import entries from the "entries.ndjson" file to "stopwords" dictionary + # Import entries from the "entries.ndjson" file to the "stopwords" dictionary $ algolia dictionary import stopwords -F entries.ndjson - # Import entries from the "entries.ndjson" file to "plurals" dictionary and continue importing entries even if some entries are invalid + # Import entries from the "entries.ndjson" file to the "plurals" dictionary and continue importing entries even if some entries are invalid $ algolia dictionary import plurals -F entries.ndjson --continue-on-errors `), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/dictionary/settings/set/set.go b/pkg/cmd/dictionary/settings/set/set.go index 877349f3..96417baa 100644 --- a/pkg/cmd/dictionary/settings/set/set.go +++ b/pkg/cmd/dictionary/settings/set/set.go @@ -41,16 +41,16 @@ func NewSetCmd(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command Long: heredoc.Doc(` Set the dictionary settings. - For now, the only setting available is to enable/disable the standard entries for the stopwords dictionary. + You can turn the standard stop words dictionary on or off. `), Example: heredoc.Doc(` - # Disable standard entries for English and French + # Tuen off standard entries for English and French $ algolia dictionary settings set --disable-standard-entries en,fr - # Enable standard entries for English and French languages + # Enable standard entries for English and French $ algolia dictionary settings set --enable-standard-entries en,fr - # Disable standard entries for English and French languages and enable standard entries for Spanish language. + # Turn off standard entries for English and French and enable standard entries for Spanish. $ algolia dictionary settings set --disable-standard-entries en,fr --enable-standard-entries es # Reset standard entries to their default values @@ -62,12 +62,12 @@ func NewSetCmd(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command return cmdutil.FlagErrorf("Either --disable-standard-entries and/or --enable-standard-entries or --reset-standard-entries must be set") } - // Check that the user is not resetting standard entries and trying to disable or enable standard entries at the same time + // Check that the user isn't resetting standard entries and trying to turn standard entries on or off at the same time if opts.ResetStandardEntries && (len(opts.DisableStandardEntries) > 0 || len(opts.EnableStandardEntries) > 0) { return cmdutil.FlagErrorf("You cannot reset standard entries and disable or enable standard entries at the same time") } - // Check if the user is trying to disable and enable standard entries for the same languages at the same time + // Check if the user is trying to turn standard entries on or off for the same languages at the same time for _, disableLanguage := range opts.DisableStandardEntries { for _, enableLanguage := range opts.EnableStandardEntries { if disableLanguage == enableLanguage { @@ -84,8 +84,8 @@ func NewSetCmd(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command }, } - cmd.Flags().StringSliceVarP(&opts.DisableStandardEntries, "disable-standard-entries", "d", []string{}, "Disable standard entries for the given languages") - cmd.Flags().StringSliceVarP(&opts.EnableStandardEntries, "enable-standard-entries", "e", []string{}, "Enable standard entries for the given languages") + cmd.Flags().StringSliceVarP(&opts.DisableStandardEntries, "disable-standard-entries", "d", []string{}, "Turn off standard entries for these languages") + cmd.Flags().StringSliceVarP(&opts.EnableStandardEntries, "enable-standard-entries", "e", []string{}, "Turn on standard entries for these languages") cmd.Flags().BoolVarP(&opts.ResetStandardEntries, "reset-standard-entries", "r", false, "Reset standard entries to their default values") SupportedLanguages := make(map[string]string, len(LanguagesWithStopwordsSupport)) diff --git a/pkg/cmd/dictionary/settings/settings.go b/pkg/cmd/dictionary/settings/settings.go index 34240cf7..0d3d7f5b 100644 --- a/pkg/cmd/dictionary/settings/settings.go +++ b/pkg/cmd/dictionary/settings/settings.go @@ -12,7 +12,7 @@ import ( func NewSettingsCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "settings", - Short: "Manage your Algolia dictionaries settings", + Short: "Manage your Algolia dictionary settings", } cmd.AddCommand(set.NewSetCmd(f, nil)) diff --git a/pkg/cmd/indices/clear/clear.go b/pkg/cmd/indices/clear/clear.go index 0bf68ef9..c91e08cf 100644 --- a/pkg/cmd/indices/clear/clear.go +++ b/pkg/cmd/indices/clear/clear.go @@ -41,9 +41,9 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm Annotations: map[string]string{ "acls": "deleteIndex", }, - Short: "Clear the specified index", + Short: "Remove all records from the specified index but don't delete the index.", Long: heredoc.Doc(` - Clear the objects of an index without affecting its settings. + Remove an indices record without affecting its settings. `), Example: heredoc.Doc(` # Clear the index named "MOVIES" @@ -67,7 +67,7 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the clear index confirmation prompt") return cmd } diff --git a/pkg/cmd/indices/config/export/export.go b/pkg/cmd/indices/config/export/export.go index 2960ba15..3104fa92 100644 --- a/pkg/cmd/indices/config/export/export.go +++ b/pkg/cmd/indices/config/export/export.go @@ -37,13 +37,13 @@ func NewExportCmd(f *cmdutil.Factory) *cobra.Command { Export an index configuration (settings, synonyms, rules) to a file. `), Example: heredoc.Doc(` - # Export the config of the index 'MOVIES' to a .json in the current folder + # Export the config of the index 'MOVIES' to a .json filr in the current folder $ algolia index config export MOVIES - # Export the synonyms and rules of the index 'MOVIES' to a .json in the current folder + # Export the synonyms and rules of the index 'MOVIES' to a .json file in the current folder $ algolia index config export MOVIES --scope synonyms,rules - # Export the config of the index 'MOVIES' to a .json into 'exports' folder + # Export the config of the index 'MOVIES' to a .json file in the 'exports' folder $ algolia index config export MOVIES --directory exports `), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/indices/copy/copy.go b/pkg/cmd/indices/copy/copy.go index f10c8f92..a572ecfa 100644 --- a/pkg/cmd/indices/copy/copy.go +++ b/pkg/cmd/indices/copy/copy.go @@ -54,7 +54,7 @@ func NewCopyCmd(f *cmdutil.Factory, runF func(*CopyOptions) error) *cobra.Comman Make a copy of an index, including its records, settings, synonyms, and rules except for the "enableReRanking" setting. `), Example: heredoc.Doc(` - # Copy the records, settings, synonyms and rules from the "SERIES" index to the "MOVIES" index + # Copy the records, settings, synonyms, and rules from the "SERIES" index to the "MOVIES" index $ algolia indices copy SERIES MOVIES # Copy only the synonyms of the "SERIES" to the "MOVIES" index @@ -88,9 +88,9 @@ func NewCopyCmd(f *cmdutil.Factory, runF func(*CopyOptions) error) *cobra.Comman }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") - cmd.Flags().StringSliceVarP(&opts.Scope, "scope", "s", []string{}, "scope to copy (default: all)") - cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the copy index confirmation prompt") + cmd.Flags().StringSliceVarP(&opts.Scope, "scope", "s", []string{}, "Which index scopes to copy. Choose from settings, synonyms, and rules (default: all)") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") _ = cmd.RegisterFlagCompletionFunc("scope", cmdutil.StringSliceCompletionFunc(map[string]string{ diff --git a/pkg/cmd/indices/delete/delete.go b/pkg/cmd/indices/delete/delete.go index 4f5dc26f..b5c5cd90 100644 --- a/pkg/cmd/indices/delete/delete.go +++ b/pkg/cmd/indices/delete/delete.go @@ -44,10 +44,13 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co Annotations: map[string]string{ "acls": "deleteIndex", }, - Short: "Delete one or multiple indices", + Short: "Deletes the specified index and all its settings.", Long: heredoc.Doc(` - Delete one or multiples indices. - This command permanently removes one or multiple indices from your application, and removes their metadata and configured settings. + Delete an index. + Deleting an index does not delete its analytics data. + If you try to delete a non-existing index, the operation is ignored without warning. + If the index you want to delete has replica indices, the replicas become independent indices. + If the index you want to delete is a replica index, you must first unlink it from its primary index before you can delete it. `), Example: heredoc.Doc(` # Delete the index named "MOVIES" @@ -82,7 +85,7 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete index confirmation prompt") cmd.Flags(). BoolVarP(&opts.IncludeReplicas, "includeReplicas", "r", false, "delete replica indices too") diff --git a/pkg/cmd/indices/move/move.go b/pkg/cmd/indices/move/move.go index d95c451e..d0b94156 100644 --- a/pkg/cmd/indices/move/move.go +++ b/pkg/cmd/indices/move/move.go @@ -48,7 +48,7 @@ func NewMoveCmd(f *cmdutil.Factory, runF func(*MoveOptions) error) *cobra.Comman }, Short: "Move an index", Long: heredoc.Doc(` - Move the full content (objects, synonyms, rules, settings) of the given source index into the destination one, effectively deleting the source index. + Move the full source index (records, synonyms, rules, settings) to a destination index, effectively deleting the source. `), Example: heredoc.Doc(` # Move the "TEST_MOVIES" index to "DEV_MOVIES" @@ -73,8 +73,8 @@ func NewMoveCmd(f *cmdutil.Factory, runF func(*MoveOptions) error) *cobra.Comman }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") - cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the move index confirmation prompt") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") return cmd } diff --git a/pkg/cmd/objects/browse/browse.go b/pkg/cmd/objects/browse/browse.go index 64affd93..6b2f46c2 100644 --- a/pkg/cmd/objects/browse/browse.go +++ b/pkg/cmd/objects/browse/browse.go @@ -43,21 +43,21 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { "runInWebCLI": "true", "acls": "browse", }, - Short: "Browse the index objects", + Short: "Browse records in an index.", Long: heredoc.Doc(` - This command browse the objects of the specified index. + This command browses records in the specified index. `), Example: heredoc.Doc(` - # Browse the objects from the "MOVIES" index + # Browse records in the "MOVIES" index $ algolia objects browse MOVIES - # Browse the objects from the "MOVIES" index and select which attributes to retrieve + # Browse records in the "MOVIES" index and select which attributes to retrieve $ algolia objects browse MOVIES --attributesToRetrieve title,overview - # Browse the objects from the "MOVIES" index with filters + # Browse records in the "MOVIES" index with filters $ algolia objects browse MOVIES --filters "genres:Drama" - # Browse the objects from the "MOVIES" and export the results to a new line delimited JSON (ndjson) file + # Browse records in the "MOVIES" and export the results to a new line delimited JSON (ndjson) file $ algolia objects browse MOVIES > movies.ndjson `), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/objects/delete/delete.go b/pkg/cmd/objects/delete/delete.go index 98eb712e..03bb7046 100644 --- a/pkg/cmd/objects/delete/delete.go +++ b/pkg/cmd/objects/delete/delete.go @@ -49,20 +49,20 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co Annotations: map[string]string{ "acls": "deleteObject", }, - Short: "Delete objects from an index", + Short: "Remove records from an index", Long: heredoc.Doc(` - This command deletes the objects from the specified index. + This command deletes records from the specified index. - You can either directly specify the objects to delete by theirs IDs and/or use the filters related flags to delete the matching objects. + You can specify the records to delete with their objectIDs or use the filter-related flags. `), Example: heredoc.Doc(` - # Delete one single object with the ID "1" from the "MOVIES" index + # Delete a record with the objectID "1" from the "MOVIES" index $ algolia objects delete MOVIES --object-ids 1 - # Delete multiple objects with the IDs "1" and "2" from the "MOVIES" index + # Delete records with the objectIDs "1" and "2" from the "MOVIES" index $ algolia objects delete MOVIES --object-ids 1,2 - # Delete all objects matching the filters "type:Scripted" from the "MOVIES" index + # Delete all records matching the filters "type:Scripted" from the "MOVIES" index $ algolia objects delete MOVIES --filters "type:Scripted" --confirm `), RunE: func(cmd *cobra.Command, args []string) error { @@ -92,11 +92,11 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().StringSliceVarP(&opts.ObjectIDs, "object-ids", "", nil, "Object IDs to delete") + cmd.Flags().StringSliceVarP(&opts.ObjectIDs, "object-ids", "", nil, "objectIDs to delete") cmdutil.AddDeleteByParamsFlags(cmd) - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") - cmd.Flags().BoolVar(&opts.Wait, "wait", false, "wait for all the operations to complete before returning") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete record confirmation prompt") + cmd.Flags().BoolVar(&opts.Wait, "wait", false, "Wait for all the operations to complete before returning") return cmd } diff --git a/pkg/cmd/objects/import/import.go b/pkg/cmd/objects/import/import.go index 3fc2f555..0efeb118 100644 --- a/pkg/cmd/objects/import/import.go +++ b/pkg/cmd/objects/import/import.go @@ -45,19 +45,19 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { Annotations: map[string]string{ "acls": "addObject", }, - Short: "Import objects to the specified index", + Short: "Import records into an index", Long: heredoc.Doc(` - Import objects to the specified index from a file / the standard input. - The file must contains one single JSON object per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). + Import records into the specified index from a file or the standard input. + The file must contain one JSON object per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). `), Example: heredoc.Doc(` - # Import objects from the "data.ndjson" file to the "MOVIES" index + # Import records from the "data.ndjson" file into the "MOVIES" index $ algolia objects import MOVIES -F data.ndjson - # Import objects from the standard input to the "MOVIES" index + # Import records from the standard input into the "MOVIES" index $ cat data.ndjson | algolia objects import MOVIES -F - - # Browse the objects in the "SERIES" index and import them to the "MOVIES" index + # Browse records in the "SERIES" index and import them into the "MOVIES" index $ algolia objects browse SERIES | algolia objects import MOVIES -F - `), RunE: func(cmd *cobra.Command, args []string) error { @@ -72,10 +72,10 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().StringVarP(&file, "file", "F", "", "Read records to import from `file` (use \"-\" to read from standard input)") + cmd.Flags().StringVarP(&file, "file", "F", "", "Import records from a `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVar(&opts.AutoGenerateObjectIDIfNotExist, "auto-generate-object-id-if-not-exist", false, "Automatically generate object ID if not exist") + cmd.Flags().BoolVar(&opts.AutoGenerateObjectIDIfNotExist, "auto-generate-object-id-if-not-exist", false, "Add objectID fields and values to imported records if they aren't present.") cmd.Flags().IntVarP(&opts.BatchSize, "batch-size", "b", 1000, "Specify the upload batch size") return cmd } diff --git a/pkg/cmd/objects/objects.go b/pkg/cmd/objects/objects.go index 43f6c67b..beb56883 100644 --- a/pkg/cmd/objects/objects.go +++ b/pkg/cmd/objects/objects.go @@ -15,7 +15,7 @@ import ( func NewObjectsCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "objects", - Short: "Manage your indices' objects", + Short: "Manage records in your indices", } cmd.AddCommand(browse.NewBrowseCmd(f)) diff --git a/pkg/cmd/objects/update/update.go b/pkg/cmd/objects/update/update.go index cdf0da86..6e499cb3 100644 --- a/pkg/cmd/objects/update/update.go +++ b/pkg/cmd/objects/update/update.go @@ -52,23 +52,23 @@ func NewUpdateCmd(f *cmdutil.Factory, runF func(*UpdateOptions) error) *cobra.Co Annotations: map[string]string{ "acls": "addObject", }, - Short: "Update objects from a file to the specified index", + Short: "Update an index with records from a file", Long: heredoc.Doc(` - Update objects from a file to the specified index. + Update a specified index with records from a file. - The file must contains one single JSON object per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). + The file must contains one JSON object per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). `), Example: heredoc.Doc(` - # Update objects from the "objects.ndjson" file to the "MOVIES" index + # Update the "MOVIES" index with records from the "objects.ndjson" file $ algolia objects update MOVIES -F objects.ndjson - # Update objects from the "objects.ndjson" file to the "MOVIES" index and create the objects if they don't exist + # Update the "MOVIES" index with records from the "objects.ndjson" file and create the records if they don't exist $ algolia objects update MOVIES -F objects.ndjson --create-if-not-exists - # Update objects from the "objects.ndjson" file to the "MOVIES" index and wait for the operation to complete + # Update the "MOVIES" index with records from the "objects.ndjson" file and wait for the operation to complete $ algolia objects update MOVIES -F objects.ndjson --wait - # Update objects from the "objects.ndjson" file to the "MOVIES" index and continue updating objects even if some objects are invalid + # Update the "MOVIES" index with records from the "objects.ndjson" file and continue updating records even if some are invalid $ algolia objects update MOVIES -F objects.ndjson --continue-on-error `), RunE: func(cmd *cobra.Command, args []string) error { @@ -88,13 +88,13 @@ func NewUpdateCmd(f *cmdutil.Factory, runF func(*UpdateOptions) error) *cobra.Co }, } - cmd.Flags().StringVarP(&opts.File, "file", "F", "", "Read objects to update from `file` (use \"-\" to read from standard input)") + cmd.Flags().StringVarP(&opts.File, "file", "F", "", "Records to update from `file` (use \"-\" to use the standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.CreateIfNotExists, "create-if-not-exists", "c", false, "If provided, updating a nonexistent object will create a new object with the objectID and the attributes defined in the object") + cmd.Flags().BoolVarP(&opts.CreateIfNotExists, "create-if-not-exists", "c", false, "If provided, updating a nonexistent record will create a new opne with the objectID and the attributes defined in the file") cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete before returning") - cmd.Flags().BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue updating objects even if some objects are invalid.") + cmd.Flags().BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue updating records even if some are invalid.") return cmd } diff --git a/pkg/cmd/open/open.go b/pkg/cmd/open/open.go index eb9473b1..80b157e7 100644 --- a/pkg/cmd/open/open.go +++ b/pkg/cmd/open/open.go @@ -77,28 +77,28 @@ func NewOpenCmd(f *cmdutil.Factory) *cobra.Command { } return openNames(), cobra.ShellCompDirectiveNoFileComp }, - Short: "Quickly open Algolia pages", - Long: `The open command provices shortcuts to quickly let you open pages to Algolia within your browser. 'algolia open --list' for a list of supported shortcuts.`, + Short: "Access Algolia support resources", + Long: `The open command provides links to Algolia support resources. 'algolia open --list' for a list of support links.`, Example: heredoc.Doc(` - # Display the list of supported shortcuts + # The support links $ algolia open --list - # Open the dashboard for the current application + # The Algolia dashboard for the current application $ algolia open dashboard - # Open the API reference + # The Algolia REST APIs $ algolia open api - # Open the documentation + # The Algolia documentation home page $ algolia open docs - # Open the CLI documentation + # The Algolia CLI documentation $ algolia open cli-docs - # Open the status page + # Algolia's status page $ algolia open status - # Open Algolia supported languages page + # Algolia's supported languages page $ algolia open languages `), RunE: func(cmd *cobra.Command, args []string) error { @@ -109,7 +109,7 @@ func NewOpenCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().BoolP("list", "l", false, "List all supported shortcuts") + cmd.Flags().BoolP("list", "l", false, "List all support links") auth.DisableAuthCheck(cmd) diff --git a/pkg/cmd/profile/application.go b/pkg/cmd/profile/application.go index 6106692e..f3f44c85 100644 --- a/pkg/cmd/profile/application.go +++ b/pkg/cmd/profile/application.go @@ -16,7 +16,7 @@ func NewProfileCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "profile", Aliases: []string{"profiles"}, - Short: "Manage your profiles", + Short: "Manage your Algolia CLI profiles", } auth.DisableAuthCheck(cmd) diff --git a/pkg/cmd/profile/remove/remove.go b/pkg/cmd/profile/remove/remove.go index 1e0e069d..7d0ef278 100644 --- a/pkg/cmd/profile/remove/remove.go +++ b/pkg/cmd/profile/remove/remove.go @@ -71,7 +71,7 @@ func NewRemoveCmd(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the remove profile confirmation prompt") return cmd } diff --git a/pkg/cmd/rules/browse/browse.go b/pkg/cmd/rules/browse/browse.go index b6b13a00..65a497c0 100644 --- a/pkg/cmd/rules/browse/browse.go +++ b/pkg/cmd/rules/browse/browse.go @@ -38,7 +38,7 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { Use: "browse ", Args: validators.ExactArgs(1), ValidArgsFunction: cmdutil.IndexNames(opts.SearchClient), - Short: "List all the rules of an index", + Short: "List an indices' rules.", Annotations: map[string]string{ "runInWebCLI": "true", "acls": "settings", diff --git a/pkg/cmd/rules/delete/delete.go b/pkg/cmd/rules/delete/delete.go index 42fb9193..02f67459 100644 --- a/pkg/cmd/rules/delete/delete.go +++ b/pkg/cmd/rules/delete/delete.go @@ -47,7 +47,7 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co Annotations: map[string]string{ "acls": "editSettings", }, - Short: "Delete rules from an index", + Short: "Delete an indices' rules.", Long: heredoc.Doc(` This command deletes the rules from the specified index. `), @@ -77,9 +77,9 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd.Flags().StringSliceVarP(&opts.RuleIDs, "rule-ids", "", nil, "Rule IDs to delete") _ = cmd.MarkFlagRequired("rule-ids") - cmd.Flags().BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Forward the delete request to the replicas") + cmd.Flags().BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Whether changes are applied to replica indices.") - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete rule confirmation prompt") return cmd } diff --git a/pkg/cmd/rules/import/import.go b/pkg/cmd/rules/import/import.go index ce312c0a..eff2ea3b 100644 --- a/pkg/cmd/rules/import/import.go +++ b/pkg/cmd/rules/import/import.go @@ -49,10 +49,10 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co Annotations: map[string]string{ "acls": "editSettings", }, - Short: "Import rules to the specified index", + Short: "Import Rules into an index.", Long: heredoc.Doc(` - Import rules to the specified index. - The file must contains one JSON rule per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). + Import Rules into an index. + File imports must contain one JSON rule per line (newline delimited JSON objects - ndjson format: https://ndjson.org/). `), Example: heredoc.Doc(` # Import rules from the "rules.ndjson" file to the "MOVIES" index @@ -91,13 +91,13 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the rule import confirmation prompt.") - cmd.Flags().StringVarP(&file, "file", "F", "", "Read rules to import from `file` (use \"-\" to read from standard input)") + cmd.Flags().StringVarP(&file, "file", "F", "", "Import rules from a `file` (use \"-\" to read from standard input).") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Forward the rules to the index replicas") - cmd.Flags().BoolVarP(&opts.ClearExistingRules, "clear-existing-rules", "c", false, "Clear existing rules before importing new ones") + cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Whether changes are applied to replica indices.") + cmd.Flags().BoolVarP(&opts.ClearExistingRules, "clear-existing-rules", "c", false, "Delete existing index rules before importing new ones.") return cmd } diff --git a/pkg/cmd/search/search.go b/pkg/cmd/search/search.go index 4132cfca..05d47064 100644 --- a/pkg/cmd/search/search.go +++ b/pkg/cmd/search/search.go @@ -37,28 +37,28 @@ func NewSearchCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "search ", - Short: "Search the given index", + Short: "Search an index", Args: validators.ExactArgs(1), ValidArgsFunction: cmdutil.IndexNames(opts.SearchClient), - Long: `Search for objects in your index.`, + Long: `Search for records in your index.`, Annotations: map[string]string{ "runInWebCLI": "true", "acls": "search", }, Example: heredoc.Doc(` - # Search for objects in the "MOVIES" index matching the query "toy story" + # Search for records in the "MOVIES" index matching the query "toy story" $ algolia search MOVIES --query "toy story" - # Search for objects in the "MOVIES" index matching the query "toy story" with filters + # Search for records in the "MOVIES" index matching the query "toy story" with filters $ algolia search MOVIES --query "toy story" --filters "'(genres:Animation OR genres:Family) AND original_language:en'" - # Search for objects in the "MOVIES" index matching the query "toy story" while setting the number of hits per page and specifying the page to retrieve + # Search for records in the "MOVIES" index matching the query "toy story" while setting the number of hits per page and specifying the page to retrieve $ algolia search MOVIES --query "toy story" --hitsPerPage 2 --page 4 - # Search for objects in the "MOVIES" index matching the query "toy story" and export the response to a .json file + # Search for records in the "MOVIES" index matching the query "toy story" and export the response to a .json file $ algolia search MOVIES --query "toy story" > movies.json - # Search for objects in the "MOVIES" index matching the query "toy story" and only export the results to a .json file + # Search for records in the "MOVIES" index matching the query "toy story" and only export the results to a .json file $ algolia search MOVIES --query "toy story" --output="jsonpath={$.Hits}" > movies.json `), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/settings/import/import.go b/pkg/cmd/settings/import/import.go index 585c2597..366dab20 100644 --- a/pkg/cmd/settings/import/import.go +++ b/pkg/cmd/settings/import/import.go @@ -41,7 +41,7 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { Annotations: map[string]string{ "acls": "editSettings", }, - Short: "Import the index settings from the given file", + Short: "Import index settings from a file.", Example: heredoc.Doc(` # Import the settings from "settings.json" to the "MOVIES" index $ algolia settings import MOVIES -F settings.json @@ -60,7 +60,7 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().StringVarP(&settingsFile, "file", "F", "", "Read settings from `file` (use \"-\" to read from standard input)") + cmd.Flags().StringVarP(&settingsFile, "file", "F", "", "Import index settings from a `file` (use \"-\" to read from standard input).") _ = cmd.MarkFlagRequired("file") return cmd diff --git a/pkg/cmd/settings/set/set.go b/pkg/cmd/settings/set/set.go index 47e96724..b4c5a26b 100644 --- a/pkg/cmd/settings/set/set.go +++ b/pkg/cmd/settings/set/set.go @@ -40,7 +40,7 @@ func NewSetCmd(f *cmdutil.Factory) *cobra.Command { Annotations: map[string]string{ "acls": "editSettings", }, - Short: "Set the settings of the specified index.", + Short: "Specify index settings.", Example: heredoc.Doc(` # Set the typo tolerance to false on the MOVIES index $ algolia settings set MOVIES --typoTolerance="false" @@ -68,7 +68,7 @@ func NewSetCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Forward the settings to the replicas") + cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Whether changes are applied to replica indices.") cmdutil.AddIndexSettingsFlags(cmd) diff --git a/pkg/cmd/settings/settings.go b/pkg/cmd/settings/settings.go index f364e47a..b147d03a 100644 --- a/pkg/cmd/settings/settings.go +++ b/pkg/cmd/settings/settings.go @@ -13,7 +13,7 @@ import ( func NewSettingsCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "settings", - Short: "Manage your Algolia settings", + Short: "Manage your Algolia index settings.", } cmd.AddCommand(get.NewGetCmd(f)) diff --git a/pkg/cmd/synonyms/browse/browse.go b/pkg/cmd/synonyms/browse/browse.go index 5cbe217f..c5569a76 100644 --- a/pkg/cmd/synonyms/browse/browse.go +++ b/pkg/cmd/synonyms/browse/browse.go @@ -37,16 +37,16 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { Use: "browse ", Args: validators.ExactArgs(1), ValidArgsFunction: cmdutil.IndexNames(opts.SearchClient), - Short: "List all the the synonyms of the given index", + Short: "List all the synonyms in this index", Annotations: map[string]string{ "runInWebCLI": "true", "acls": "settings", }, Example: heredoc.Doc(` - # List all the synonyms of the 'MOVIES' index + # List all the synonyms in the 'MOVIES' index $ algolia synonyms browse MOVIES - # List all the synonyms of the 'MOVIES' and save them to the 'synonyms.json' file + # List all the synonyms in the 'MOVIES' index and save them in the 'synonyms.json' file $ algolia synonyms browse MOVIES > synonyms.json `), RunE: func(cmd *cobra.Command, args []string) error { diff --git a/pkg/cmd/synonyms/delete/delete.go b/pkg/cmd/synonyms/delete/delete.go index 7dcb1c82..baefdd5d 100644 --- a/pkg/cmd/synonyms/delete/delete.go +++ b/pkg/cmd/synonyms/delete/delete.go @@ -75,11 +75,11 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().StringSliceVarP(&opts.SynonymIDs, "synonym-ids", "", nil, "Synonym IDs to delete") + cmd.Flags().StringSliceVarP(&opts.SynonymIDs, "synonym-ids", "", nil, "Synonym IDs to delete.") _ = cmd.MarkFlagRequired("synonym-ids") - cmd.Flags().BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Forward the delete request to the replicas") + cmd.Flags().BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Whether changes are applied to replica indices.") - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "skip confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete synonym confirmation prompt.") return cmd } diff --git a/pkg/cmd/synonyms/import/import.go b/pkg/cmd/synonyms/import/import.go index 394d8a85..62cbc7a9 100644 --- a/pkg/cmd/synonyms/import/import.go +++ b/pkg/cmd/synonyms/import/import.go @@ -83,11 +83,11 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co }, } - cmd.Flags().StringVarP(&file, "file", "F", "", "Read synonyms to import from `file` (use \"-\" to read from standard input)") + cmd.Flags().StringVarP(&file, "file", "F", "", "Import synonyms from a `file` (use \"-\" to read from standard input).") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Forward the synonyms to the replicas of the index") - cmd.Flags().BoolVarP(&opts.ReplaceExistingSynonyms, "replace-existing-synonyms", "r", false, "Replace existing synonyms in the index") + cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Whether changes are applied to replica indices.") + cmd.Flags().BoolVarP(&opts.ReplaceExistingSynonyms, "replace-existing-synonyms", "r", false, "Replace existing synonyms in the index.") return cmd } diff --git a/pkg/cmd/synonyms/save/save.go b/pkg/cmd/synonyms/save/save.go index 9407b8c8..7780a23f 100644 --- a/pkg/cmd/synonyms/save/save.go +++ b/pkg/cmd/synonyms/save/save.go @@ -45,11 +45,11 @@ func NewSaveCmd(f *cmdutil.Factory, runF func(*SaveOptions) error) *cobra.Comman Annotations: map[string]string{ "acls": "editSettings", }, - Short: "Save a synonym to the given index", + Short: "Add a synonym to an index", Aliases: []string{"create", "edit"}, Long: heredoc.Doc(` - This command save a synonym to the specified index. - If the synonym doesn't exist yet, a new one is created. + This command adds a synonym to the specified index. + If the synonym doesn't exist, a new one is created. `), Example: heredoc.Doc(` # Save one standard synonym with ID "1" and "foo" and "bar" synonyms to the "MOVIES" index @@ -91,26 +91,26 @@ func NewSaveCmd(f *cmdutil.Factory, runF func(*SaveOptions) error) *cobra.Comman // Common cmd.Flags().StringVarP(&flags.SynonymID, "id", "i", "", "Synonym ID to save") - cmd.Flags().StringVarP(&flags.SynonymType, "type", "t", "", "Synonym type to save (default to regular)") + cmd.Flags().StringVarP(&flags.SynonymType, "type", "t", "", "Synonym type. One of altCorrection1, altCorrection2, oneWaySynonym, placeholder, synonym.") _ = cmd.RegisterFlagCompletionFunc("type", cmdutil.StringCompletionFunc(map[string]string{ shared.Regular: "(default) Used when you want a word or phrase to find its synonyms or the other way around.", shared.OneWay: "Used when you want a word or phrase to find its synonyms, but not the reverse.", - shared.AltCorrection1: "Used when you want records with an exact query match to rank higher than a synonym match. (will return matches with one typo)", - shared.AltCorrection2: "Used when you want records with an exact query match to rank higher than a synonym match. (will return matches with two typos)", - shared.Placeholder: "Used to place not-yet-defined “tokens” (that can take any value from a list of defined words).", + shared.AltCorrection1: "Used when you want records with an exact query match to rank higher than a synonym match. Will return matches with one typo.", + shared.AltCorrection2: "Used when you want records with an exact query match to rank higher than a synonym match. Will return matches with two typos.", + shared.Placeholder: "Used to place not-yet-defined tokens (that can take any value from a list of defined words).", })) - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Forward the save request to the replicas") + cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Whether changes are applied to replica indices.") // Regular synonym - cmd.Flags().StringSliceVarP(&flags.Synonyms, "synonyms", "s", nil, "Synonyms to save") - // One way synonym - cmd.Flags().StringVarP(&flags.SynonymInput, "input", "n", "", "Word of phrases to appear in query strings (one way synonyms only)") + cmd.Flags().StringSliceVarP(&flags.Synonyms, "synonyms", "s", nil, "Words or phrases considered equivalent.") + // One-way synonym + cmd.Flags().StringVarP(&flags.SynonymInput, "input", "n", "", "Word or phrases to appear in query strings (one-way synonyms only).") // Placeholder synonym - cmd.Flags().StringVarP(&flags.SynonymPlaceholder, "placeholder", "l", "", "A single word, used as the basis for the below array of replacements (placeholder synonyms only)") - cmd.Flags().StringSliceVarP(&flags.SynonymReplacements, "replacements", "r", nil, "An list of replacements of the placeholder (placeholder synonyms only)") - // Alt correction synonym - cmd.Flags().StringVarP(&flags.SynonymWord, "word", "w", "", "A single word, used as the basis for the array of corrections (alt correction synonyms only)") - cmd.Flags().StringSliceVarP(&flags.SynonymCorrections, "corrections", "c", nil, "A list of corrections of the word (alt correction synonyms only)") + cmd.Flags().StringVarP(&flags.SynonymPlaceholder, "placeholder", "l", "", "Placeholder token to represent a synonym within records.") + cmd.Flags().StringSliceVarP(&flags.SynonymReplacements, "replacements", "r", nil, "Query words that will match the placeholder synonym token.") + // Alternative correction synonym + cmd.Flags().StringVarP(&flags.SynonymWord, "word", "w", "", "Word or phrase to appear in query strings (for altcorrection1 and altcorrection2).") + cmd.Flags().StringSliceVarP(&flags.SynonymCorrections, "corrections", "c", nil, "Words to be matched in records (alternative correction synonyms only).") return cmd } diff --git a/pkg/cmdutil/jsonpath_flags.go b/pkg/cmdutil/jsonpath_flags.go index cb2ad440..063e5aeb 100644 --- a/pkg/cmdutil/jsonpath_flags.go +++ b/pkg/cmdutil/jsonpath_flags.go @@ -11,8 +11,8 @@ import ( "github.com/algolia/cli/pkg/printers" ) -// templates are logically optional for specifying a format. -// this allows a user to specify a template format value +// Templates are logically optional for specifying a format. +// this lets a user specify a template format value // as --output=jsonpath= var jsonFormats = map[string]bool{ "jsonpath": true, @@ -24,13 +24,13 @@ var jsonFormats = map[string]bool{ // Given the following flag values, a printer can be requested that knows // how to handle printing based on these values. type JSONPathPrintFlags struct { - // indicates if it is OK to ignore missing keys for rendering + // Indicates if it's OK to ignore missing keys for rendering // an output template. AllowMissingKeys *bool TemplateArgument *string } -// AllowedFormats returns slice of string of allowed JSONPath printing format +// AllowedFormats returns slice of string of allowed JSONPath printing formats func (f *JSONPathPrintFlags) AllowedFormats() []string { formats := make([]string, 0, len(jsonFormats)) for format := range jsonFormats { @@ -103,12 +103,12 @@ func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.Printer, // flags related to template printing to it func (f *JSONPathPrintFlags) AddFlags(c *cobra.Command) { if f.TemplateArgument != nil { - c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to template file to use when --output=jsonpath, --output=jsonpath-file.") + c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to the template file to use when --output=jsonpath, --output=jsonpath-file.") _ = c.Flags().SetAnnotation("template", "IsPrint", []string{"true"}) _ = c.MarkFlagFilename("template") } if f.AllowMissingKeys != nil { - c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore any errors in templates when a field or map key is missing in the template. Only applies to golang and jsonpath output formats.") + c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore template errors caused by missing fields or map keys. This only applies to golang and jsonpath output formats.") _ = c.Flags().SetAnnotation("allow-missing-template-keys", "IsPrint", []string{"true"}) } } diff --git a/pkg/cmdutil/print_flags.go b/pkg/cmdutil/print_flags.go index 0dd2e233..b0cfb79e 100644 --- a/pkg/cmdutil/print_flags.go +++ b/pkg/cmdutil/print_flags.go @@ -80,7 +80,7 @@ func (f *PrintFlags) AddFlags(cmd *cobra.Command) { f.JSONPathPrintFlags.AddFlags(cmd) if f.OutputFormat != nil { - cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: (%s).`, strings.Join(f.AllowedFormats(), ", "))) + cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: %s.`, strings.Join(f.AllowedFormats(), ", "))) _ = cmd.Flags().SetAnnotation("output", "IsPrint", []string{"true"}) if f.OutputFlagSpecified == nil { f.OutputFlagSpecified = func() bool { diff --git a/pkg/cmdutil/spec_flags.go b/pkg/cmdutil/spec_flags.go index aa3c0d1b..5ffe03ed 100644 --- a/pkg/cmdutil/spec_flags.go +++ b/pkg/cmdutil/spec_flags.go @@ -251,30 +251,30 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntaxFeat cmd.Flags().Bool("allowTyposOnNumericTokens", true, heredoc.Doc(`Whether to allow typos on numbers in the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/`)) cmd.Flags().SetAnnotation("allowTyposOnNumericTokens", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Alternatives of query words that should be considered as exact matches by the Exact ranking criterion. + cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered as exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) - cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether this search will be included in Analytics. + cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether to include this query in Algolia's search analytics. See: https://www.algolia.com/doc/api-reference/api-parameters/analytics/`)) cmd.Flags().SetAnnotation("analytics", "Categories", []string{"Analytics"}) - cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Tags to apply to the query for segmenting analytics data. + cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Search analytics tags for query data segmentation. See: https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/`)) cmd.Flags().SetAnnotation("analyticsTags", "Categories", []string{"Analytics"}) - cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. + cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle: expressed as a comma-separated string of latitude and longitude values. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/`)) cmd.Flags().SetAnnotation("aroundLatLng", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to obtain the coordinates from the request's IP address. + cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to use the location computed from the user's IP address. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/`)) cmd.Flags().SetAnnotation("aroundLatLngViaIP", "Categories", []string{"Geo-Search"}) aroundPrecision := NewJSONVar([]string{"integer", "array"}...) - cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Precision of a coordinate-based search in meters to group results with similar distances. + cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Groups similar distances into range bands. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/`)) cmd.Flags().SetAnnotation("aroundPrecision", "Categories", []string{"Geo-Search"}) aroundRadius := NewJSONVar([]string{"integer", "string"}...) cmd.Flags().Var(aroundRadius, "aroundRadius", heredoc.Doc(`Maximum radius for a search around a central location. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/`)) cmd.Flags().SetAnnotation("aroundRadius", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. + cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. See: https://www.algolia.com/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity/`)) cmd.Flags().SetAnnotation("attributeCriteriaComputedByMinProximity", "Categories", []string{"Advanced"}) cmd.Flags().StringSlice("attributesToHighlight", []string{}, heredoc.Doc(`Attributes to highlight. @@ -289,7 +289,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/attributesToSnippe cmd.Flags().Bool("clickAnalytics", false, heredoc.Doc(`Whether to include a queryID attribute in the response. See: https://www.algolia.com/doc/api-reference/api-parameters/clickAnalytics/`)) cmd.Flags().SetAnnotation("clickAnalytics", "Categories", []string{"Analytics"}) - cmd.Flags().String("cursor", "", heredoc.Doc(`Cursor to get the next page of the response.`)) + cmd.Flags().String("cursor", "", heredoc.Doc(`Cursor to get to the next page of the response.`)) cmd.Flags().StringSlice("customRanking", []string{}, heredoc.Doc(`Attributes to use as custom ranking. See: https://www.algolia.com/doc/api-reference/api-parameters/customRanking/`)) cmd.Flags().SetAnnotation("customRanking", "Categories", []string{"Ranking"}) @@ -306,7 +306,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleran cmd.Flags().Var(distinct, "distinct", heredoc.Doc(`Determines how many records of a group are included in the search results. See: https://www.algolia.com/doc/api-reference/api-parameters/distinct/`)) cmd.Flags().SetAnnotation("distinct", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to enable A/B testing for this search. + cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to include this search in currently running A/B tests. See: https://www.algolia.com/doc/api-reference/api-parameters/enableABTest/`)) cmd.Flags().SetAnnotation("enableABTest", "Categories", []string{"Advanced"}) cmd.Flags().Bool("enablePersonalization", false, heredoc.Doc(`Whether to enable Personalization. @@ -325,13 +325,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/exactOnSingleWordQ cmd.Flags().Var(facetFilters, "facetFilters", heredoc.Doc(`Filter the search by facet values, so that only records with the same facet values are retrieved. See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().SetAnnotation("facetFilters", "Categories", []string{"Filtering"}) - cmd.Flags().Bool("facetingAfterDistinct", false, heredoc.Doc(`Whether faceting should be applied after deduplication with distinct. + cmd.Flags().Bool("facetingAfterDistinct", false, heredoc.Doc(`Whether to apply faceting after deduplication with distinct. See: https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/`)) cmd.Flags().SetAnnotation("facetingAfterDistinct", "Categories", []string{"Faceting"}) - cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Facets for which to retrieve facet values that match the search criteria and the number of matching facet values. + cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Retrieve the specified facets and their facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/facets/`)) cmd.Flags().SetAnnotation("facets", "Categories", []string{"Faceting"}) - cmd.Flags().String("filters", "", heredoc.Doc(`Filter expression to only include items that match the filter criteria in the response. + cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) cmd.Flags().Bool("getRankingInfo", false, heredoc.Doc(`Whether the search response should include detailed ranking information. @@ -355,7 +355,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().String("keepDiacriticsOnCharacters", "", heredoc.Doc(`Characters for which diacritics should be preserved. See: https://www.algolia.com/doc/api-reference/api-parameters/keepDiacriticsOnCharacters/`)) cmd.Flags().SetAnnotation("keepDiacriticsOnCharacters", "Categories", []string{"Languages"}) - cmd.Flags().Int("length", 0, heredoc.Doc(`Number of hits to retrieve (used in combination with offset). + cmd.Flags().Int("length", 0, heredoc.Doc(`If you've specified an offset, this determines the number of hits to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/length/`)) cmd.Flags().SetAnnotation("length", "Categories", []string{"Pagination"}) cmd.Flags().Int("maxFacetHits", 10, heredoc.Doc(`Maximum number of facet values to return when searching for facet values. @@ -373,42 +373,42 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor1Typ cmd.Flags().Int("minWordSizefor2Typos", 8, heredoc.Doc(`Minimum number of characters a word in the search query must contain to accept matches with two typos. See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor2Typos/`)) cmd.Flags().SetAnnotation("minWordSizefor2Typos", "Categories", []string{"Typos"}) - cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`Minimum radius (in meters) for a search around a location when aroundRadius isn't set. + cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`If aroundRadius isn't set, defines a [minimum radius] for aroundLatLng and aroundLatLngViaIP (in meters). See: https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/`)) cmd.Flags().SetAnnotation("minimumAroundRadius", "Categories", []string{"Geo-Search"}) cmd.Flags().String("mode", "keywordSearch", heredoc.Doc(`Search mode the index will use to query for results. One of: neuralSearch, keywordSearch. See: https://www.algolia.com/doc/api-reference/api-parameters/mode/`)) cmd.Flags().SetAnnotation("mode", "Categories", []string{"Query strategy"}) - cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches). + cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`Change the default settings for several natural language parameters in a single operation: ignorePlurals, removeStopWords, removeWordsIfNoResults, analyticsTags, and ruleContexts. See: https://www.algolia.com/doc/api-reference/api-parameters/naturalLanguages/`)) cmd.Flags().SetAnnotation("naturalLanguages", "Categories", []string{"Languages"}) numericFilters := NewJSONVar([]string{"array", "string"}...) cmd.Flags().Var(numericFilters, "numericFilters", heredoc.Doc(`Filter by numeric facets. See: https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/`)) cmd.Flags().SetAnnotation("numericFilters", "Categories", []string{"Filtering"}) - cmd.Flags().Int("offset", 0, heredoc.Doc(`Position of the first hit to retrieve. + cmd.Flags().Int("offset", 0, heredoc.Doc(`Out of the results list, indicate which one you want to show first. See: https://www.algolia.com/doc/api-reference/api-parameters/offset/`)) cmd.Flags().SetAnnotation("offset", "Categories", []string{"Pagination"}) optionalFilters := NewJSONVar([]string{"array", "string"}...) - cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Filters to promote or demote records in the search results. + cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Create filters for ranking purposes. Records that match the filter will rank higher (or lower for a negative filter). See: https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/`)) cmd.Flags().SetAnnotation("optionalFilters", "Categories", []string{"Filtering"}) - cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`Words that should be considered optional when found in the query. + cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`If a search doesn't return enough results, you can increase the number of hits by setting these words as optional. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) cmd.Flags().SetAnnotation("optionalWords", "Categories", []string{"Query strategy"}) - cmd.Flags().Int("page", 0, heredoc.Doc(`Page of search results to retrieve. + cmd.Flags().Int("page", 0, heredoc.Doc(`Requested page of search results. Algolia uses page and hitsPerPage to control how search results are displayed (paginated). See: https://www.algolia.com/doc/api-reference/api-parameters/page/`)) cmd.Flags().SetAnnotation("page", "Categories", []string{"Pagination"}) - cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this search when calculating processing-time percentiles. + cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this query in the processing-time percentile computation. See: https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/`)) cmd.Flags().SetAnnotation("percentileComputation", "Categories", []string{"Advanced"}) - cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Impact that Personalization should have on this search. + cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Determines the impact of the Personalization feature on results: from 0 (none) to 100 (maximum). See: https://www.algolia.com/doc/api-reference/api-parameters/personalizationImpact/`)) cmd.Flags().SetAnnotation("personalizationImpact", "Categories", []string{"Personalization"}) - cmd.Flags().String("query", "", heredoc.Doc(`Search query. + cmd.Flags().String("query", "", heredoc.Doc(`The text to search for in the index. See: https://www.algolia.com/doc/api-reference/api-parameters/query/`)) cmd.Flags().SetAnnotation("query", "Categories", []string{"Search"}) - cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/`)) cmd.Flags().SetAnnotation("queryLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("queryType", "prefixLast", heredoc.Doc(`Determines if and how query words are interpreted as prefixes. One of: prefixLast, prefixAll, prefixNone. @@ -436,13 +436,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/renderingContent/` cmd.Flags().Bool("replaceSynonymsInHighlight", false, heredoc.Doc(`Whether to replace a highlighted word with the matched synonym. See: https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInHighlight/`)) cmd.Flags().SetAnnotation("replaceSynonymsInHighlight", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in the API response of search and browse requests. + cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in search and browse API responses. See: https://www.algolia.com/doc/api-reference/api-parameters/responseFields/`)) cmd.Flags().SetAnnotation("responseFields", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that at least partially matched the search query. + cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that partially or fully matched the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/`)) cmd.Flags().SetAnnotation("restrictHighlightAndSnippetArrays", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restricts a search to a subset of your searchable attributes. + cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restrict the query to look at only the specified searchable attributes. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/`)) cmd.Flags().SetAnnotation("restrictSearchableAttributes", "Categories", []string{"Filtering"}) cmd.Flags().StringSlice("ruleContexts", []string{}, heredoc.Doc(`Assigns a rule context to the search query. @@ -450,7 +450,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/`)) cmd.Flags().SetAnnotation("ruleContexts", "Categories", []string{"Rules"}) semanticSearch := NewJSONVar([]string{}...) cmd.Flags().Var(semanticSearch, "semanticSearch", heredoc.Doc(`Settings for the semantic search part of NeuralSearch.`)) - cmd.Flags().String("similarQuery", "", heredoc.Doc(`Keywords to be used instead of the search query to conduct a more broader search. + cmd.Flags().String("similarQuery", "", heredoc.Doc(`Overrides the query parameter and performs a more generic search to find "similar" results. See: https://www.algolia.com/doc/api-reference/api-parameters/similarQuery/`)) cmd.Flags().SetAnnotation("similarQuery", "Categories", []string{"Search"}) cmd.Flags().String("snippetEllipsisText", "…", heredoc.Doc(`String used as an ellipsis indicator when a snippet is truncated. @@ -459,10 +459,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisTex cmd.Flags().String("sortFacetValuesBy", "count", heredoc.Doc(`Order in which to retrieve facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/`)) cmd.Flags().SetAnnotation("sortFacetValuesBy", "Categories", []string{"Faceting"}) - cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`Whether to sum all filter scores. + cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`How to calculate the filtering score. Whether to sum the scores of each matched filter or use the highest score of the filters. See: https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/`)) cmd.Flags().SetAnnotation("sumOrFiltersScores", "Categories", []string{"Filtering"}) - cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to take into account an index's synonyms for this search. + cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to use or disregard an index's synonyms for this search. See: https://www.algolia.com/doc/api-reference/api-parameters/synonyms/`)) cmd.Flags().SetAnnotation("synonyms", "Categories", []string{"Advanced"}) tagFilters := NewJSONVar([]string{"array", "string"}...) @@ -473,13 +473,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/`)) cmd.Flags().Var(typoTolerance, "typoTolerance", heredoc.Doc(`Whether typo tolerance is enabled and how it is applied. See: https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/`)) cmd.Flags().SetAnnotation("typoTolerance", "Categories", []string{"Typos"}) - cmd.Flags().String("userToken", "", heredoc.Doc(`Unique pseudonymous or anonymous user identifier. + cmd.Flags().String("userToken", "", heredoc.Doc(`Link the current search to a specific user with a user token (a unique pseudonymous or anonymous identifier). See: https://www.algolia.com/doc/api-reference/api-parameters/userToken/`)) cmd.Flags().SetAnnotation("userToken", "Categories", []string{"Personalization"}) } func AddDeleteByParamsFlags(cmd *cobra.Command) { - cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. + cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle: expressed as a comma-separated string of latitude and longitude values. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/`)) cmd.Flags().SetAnnotation("aroundLatLng", "Categories", []string{"Geo-Search"}) aroundRadius := NewJSONVar([]string{"integer", "string"}...) @@ -490,7 +490,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/`)) cmd.Flags().Var(facetFilters, "facetFilters", heredoc.Doc(`Filter the search by facet values, so that only records with the same facet values are retrieved. See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().SetAnnotation("facetFilters", "Categories", []string{"Filtering"}) - cmd.Flags().String("filters", "", heredoc.Doc(`Filter expression to only include items that match the filter criteria in the response. + cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) cmd.Flags().SetAnnotation("insideBoundingBox", "Categories", []string{"Geo-Search"}) @@ -518,10 +518,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/allowCompressionOf cmd.Flags().Bool("allowTyposOnNumericTokens", true, heredoc.Doc(`Whether to allow typos on numbers in the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/`)) cmd.Flags().SetAnnotation("allowTyposOnNumericTokens", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Alternatives of query words that should be considered as exact matches by the Exact ranking criterion. + cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered as exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) - cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. + cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. See: https://www.algolia.com/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity/`)) cmd.Flags().SetAnnotation("attributeCriteriaComputedByMinProximity", "Categories", []string{"Advanced"}) cmd.Flags().String("attributeForDistinct", "", heredoc.Doc(`Attribute that should be used to establish groups of results. @@ -599,7 +599,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/`)) cmd.Flags().Var(ignorePlurals, "ignorePlurals", heredoc.Doc(`Treat singular, plurals, and other forms of declensions as equivalent. See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().SetAnnotation("ignorePlurals", "Categories", []string{"Languages"}) - cmd.Flags().StringSlice("indexLanguages", []string{}, heredoc.Doc(`Languages for language-specific processing steps, such as word detection and dictionary settings. + cmd.Flags().StringSlice("indexLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/indexLanguages/`)) cmd.Flags().SetAnnotation("indexLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("keepDiacriticsOnCharacters", "", heredoc.Doc(`Characters for which diacritics should be preserved. @@ -626,12 +626,12 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/mode/`)) cmd.Flags().StringSlice("numericAttributesForFiltering", []string{}, heredoc.Doc(`Numeric attributes that can be used as numerical filters. See: https://www.algolia.com/doc/api-reference/api-parameters/numericAttributesForFiltering/`)) cmd.Flags().SetAnnotation("numericAttributesForFiltering", "Categories", []string{"Performance"}) - cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`Words that should be considered optional when found in the query. + cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`If a search doesn't return enough results, you can increase the number of hits by setting these words as optional. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) cmd.Flags().SetAnnotation("optionalWords", "Categories", []string{"Query strategy"}) cmd.Flags().Int("paginationLimitedTo", 1000, heredoc.Doc(`Maximum number of search results that can be obtained through pagination. See: https://www.algolia.com/doc/api-reference/api-parameters/paginationLimitedTo/`)) - cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/`)) cmd.Flags().SetAnnotation("queryLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("queryType", "prefixLast", heredoc.Doc(`Determines if and how query words are interpreted as prefixes. One of: prefixLast, prefixAll, prefixNone. @@ -662,10 +662,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInH cmd.Flags().StringSlice("replicas", []string{}, heredoc.Doc(`Creates replica indices. See: https://www.algolia.com/doc/api-reference/api-parameters/replicas/`)) cmd.Flags().SetAnnotation("replicas", "Categories", []string{"Ranking"}) - cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in the API response of search and browse requests. + cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in search and browse API responses. See: https://www.algolia.com/doc/api-reference/api-parameters/responseFields/`)) cmd.Flags().SetAnnotation("responseFields", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that at least partially matched the search query. + cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that partially or fully matched the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/`)) cmd.Flags().SetAnnotation("restrictHighlightAndSnippetArrays", "Categories", []string{"Highlighting and Snippeting"}) cmd.Flags().StringSlice("searchableAttributes", []string{}, heredoc.Doc(`Attributes used for searching. Attribute names are case-sensitive. @@ -705,30 +705,30 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntaxFeat cmd.Flags().Bool("allowTyposOnNumericTokens", true, heredoc.Doc(`Whether to allow typos on numbers in the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/`)) cmd.Flags().SetAnnotation("allowTyposOnNumericTokens", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Alternatives of query words that should be considered as exact matches by the Exact ranking criterion. + cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered as exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) - cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether this search will be included in Analytics. + cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether to include this query in Algolia's search analytics. See: https://www.algolia.com/doc/api-reference/api-parameters/analytics/`)) cmd.Flags().SetAnnotation("analytics", "Categories", []string{"Analytics"}) - cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Tags to apply to the query for segmenting analytics data. + cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Search analytics tags for query data segmentation. See: https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/`)) cmd.Flags().SetAnnotation("analyticsTags", "Categories", []string{"Analytics"}) - cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. + cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle: a comma-separated string of latitude and longitude values. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/`)) cmd.Flags().SetAnnotation("aroundLatLng", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to obtain the coordinates from the request's IP address. + cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to use the location computed from the user's IP address. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/`)) cmd.Flags().SetAnnotation("aroundLatLngViaIP", "Categories", []string{"Geo-Search"}) aroundPrecision := NewJSONVar([]string{"integer", "array"}...) - cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Precision of a coordinate-based search in meters to group results with similar distances. + cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Groups similar distances into range bands. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/`)) cmd.Flags().SetAnnotation("aroundPrecision", "Categories", []string{"Geo-Search"}) aroundRadius := NewJSONVar([]string{"integer", "string"}...) cmd.Flags().Var(aroundRadius, "aroundRadius", heredoc.Doc(`Maximum radius for a search around a central location. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/`)) cmd.Flags().SetAnnotation("aroundRadius", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. + cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. See: https://www.algolia.com/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity/`)) cmd.Flags().SetAnnotation("attributeCriteriaComputedByMinProximity", "Categories", []string{"Advanced"}) cmd.Flags().StringSlice("attributesToHighlight", []string{}, heredoc.Doc(`Attributes to highlight. @@ -759,7 +759,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleran cmd.Flags().Var(distinct, "distinct", heredoc.Doc(`Determines how many records of a group are included in the search results. See: https://www.algolia.com/doc/api-reference/api-parameters/distinct/`)) cmd.Flags().SetAnnotation("distinct", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to enable A/B testing for this search. + cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to include this search in currently running A/B tests. See: https://www.algolia.com/doc/api-reference/api-parameters/enableABTest/`)) cmd.Flags().SetAnnotation("enableABTest", "Categories", []string{"Advanced"}) cmd.Flags().Bool("enablePersonalization", false, heredoc.Doc(`Whether to enable Personalization. @@ -781,10 +781,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().Bool("facetingAfterDistinct", false, heredoc.Doc(`Whether faceting should be applied after deduplication with distinct. See: https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/`)) cmd.Flags().SetAnnotation("facetingAfterDistinct", "Categories", []string{"Faceting"}) - cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Facets for which to retrieve facet values that match the search criteria and the number of matching facet values. + cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Retrieve the specified facets and their facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/facets/`)) cmd.Flags().SetAnnotation("facets", "Categories", []string{"Faceting"}) - cmd.Flags().String("filters", "", heredoc.Doc(`Filter expression to only include items that match the filter criteria in the response. + cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) cmd.Flags().Bool("getRankingInfo", false, heredoc.Doc(`Whether the search response should include detailed ranking information. @@ -808,7 +808,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().String("keepDiacriticsOnCharacters", "", heredoc.Doc(`Characters for which diacritics should be preserved. See: https://www.algolia.com/doc/api-reference/api-parameters/keepDiacriticsOnCharacters/`)) cmd.Flags().SetAnnotation("keepDiacriticsOnCharacters", "Categories", []string{"Languages"}) - cmd.Flags().Int("length", 0, heredoc.Doc(`Number of hits to retrieve (used in combination with offset). + cmd.Flags().Int("length", 0, heredoc.Doc(`If you've specified an offset, this determines the number of hits to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/length/`)) cmd.Flags().SetAnnotation("length", "Categories", []string{"Pagination"}) cmd.Flags().Int("maxFacetHits", 10, heredoc.Doc(`Maximum number of facet values to return when searching for facet values. @@ -826,42 +826,42 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor1Typ cmd.Flags().Int("minWordSizefor2Typos", 8, heredoc.Doc(`Minimum number of characters a word in the search query must contain to accept matches with two typos. See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor2Typos/`)) cmd.Flags().SetAnnotation("minWordSizefor2Typos", "Categories", []string{"Typos"}) - cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`Minimum radius (in meters) for a search around a location when aroundRadius isn't set. + cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`If aroundRadius isn't set, defines a [minimum radius] for aroundLatLng and aroundLatLngViaIP (in meters). See: https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/`)) cmd.Flags().SetAnnotation("minimumAroundRadius", "Categories", []string{"Geo-Search"}) cmd.Flags().String("mode", "keywordSearch", heredoc.Doc(`Search mode the index will use to query for results. One of: neuralSearch, keywordSearch. See: https://www.algolia.com/doc/api-reference/api-parameters/mode/`)) cmd.Flags().SetAnnotation("mode", "Categories", []string{"Query strategy"}) - cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches). + cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`Change the default settings for several natural language parameters in a single operation: ignorePlurals, removeStopWords, removeWordsIfNoResults, analyticsTags, and ruleContexts. See: https://www.algolia.com/doc/api-reference/api-parameters/naturalLanguages/`)) cmd.Flags().SetAnnotation("naturalLanguages", "Categories", []string{"Languages"}) numericFilters := NewJSONVar([]string{"array", "string"}...) cmd.Flags().Var(numericFilters, "numericFilters", heredoc.Doc(`Filter by numeric facets. See: https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/`)) cmd.Flags().SetAnnotation("numericFilters", "Categories", []string{"Filtering"}) - cmd.Flags().Int("offset", 0, heredoc.Doc(`Position of the first hit to retrieve. + cmd.Flags().Int("offset", 0, heredoc.Doc(`Out of the results list, indicate which one you want to show first. See: https://www.algolia.com/doc/api-reference/api-parameters/offset/`)) cmd.Flags().SetAnnotation("offset", "Categories", []string{"Pagination"}) optionalFilters := NewJSONVar([]string{"array", "string"}...) - cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Filters to promote or demote records in the search results. + cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Create filters for ranking purposes. Records that match the filter will rank higher (or lower for a negative filter). See: https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/`)) cmd.Flags().SetAnnotation("optionalFilters", "Categories", []string{"Filtering"}) - cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`Words that should be considered optional when found in the query. + cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`If a search doesn't return enough results, you can increase the number of hits by setting these words as optional. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) cmd.Flags().SetAnnotation("optionalWords", "Categories", []string{"Query strategy"}) - cmd.Flags().Int("page", 0, heredoc.Doc(`Page of search results to retrieve. + cmd.Flags().Int("page", 0, heredoc.Doc(`Requested page of search results. Algolia uses page and hitsPerPage to control how search results are displayed (paginated). See: https://www.algolia.com/doc/api-reference/api-parameters/page/`)) cmd.Flags().SetAnnotation("page", "Categories", []string{"Pagination"}) - cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this search when calculating processing-time percentiles. + cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this query in the processing-time percentile computation. See: https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/`)) cmd.Flags().SetAnnotation("percentileComputation", "Categories", []string{"Advanced"}) - cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Impact that Personalization should have on this search. + cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Determines the impact of the Personalization feature on results: from 0 (none) to 100 (maximum). See: https://www.algolia.com/doc/api-reference/api-parameters/personalizationImpact/`)) cmd.Flags().SetAnnotation("personalizationImpact", "Categories", []string{"Personalization"}) - cmd.Flags().String("query", "", heredoc.Doc(`Search query. + cmd.Flags().String("query", "", heredoc.Doc(`The text to search for in the index. See: https://www.algolia.com/doc/api-reference/api-parameters/query/`)) cmd.Flags().SetAnnotation("query", "Categories", []string{"Search"}) - cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/`)) cmd.Flags().SetAnnotation("queryLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("queryType", "prefixLast", heredoc.Doc(`Determines if and how query words are interpreted as prefixes. One of: prefixLast, prefixAll, prefixNone. @@ -889,13 +889,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/renderingContent/` cmd.Flags().Bool("replaceSynonymsInHighlight", false, heredoc.Doc(`Whether to replace a highlighted word with the matched synonym. See: https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInHighlight/`)) cmd.Flags().SetAnnotation("replaceSynonymsInHighlight", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in the API response of search and browse requests. + cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in search and browse API responses. See: https://www.algolia.com/doc/api-reference/api-parameters/responseFields/`)) cmd.Flags().SetAnnotation("responseFields", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that at least partially matched the search query. + cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that partially or fully matched the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/`)) cmd.Flags().SetAnnotation("restrictHighlightAndSnippetArrays", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restricts a search to a subset of your searchable attributes. + cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restrict the query to look at only the specified searchable attributes. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/`)) cmd.Flags().SetAnnotation("restrictSearchableAttributes", "Categories", []string{"Filtering"}) cmd.Flags().StringSlice("ruleContexts", []string{}, heredoc.Doc(`Assigns a rule context to the search query. @@ -903,7 +903,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/`)) cmd.Flags().SetAnnotation("ruleContexts", "Categories", []string{"Rules"}) semanticSearch := NewJSONVar([]string{}...) cmd.Flags().Var(semanticSearch, "semanticSearch", heredoc.Doc(`Settings for the semantic search part of NeuralSearch.`)) - cmd.Flags().String("similarQuery", "", heredoc.Doc(`Keywords to be used instead of the search query to conduct a more broader search. + cmd.Flags().String("similarQuery", "", heredoc.Doc(`Overrides the query parameter and performs a more generic search to find "similar" results. See: https://www.algolia.com/doc/api-reference/api-parameters/similarQuery/`)) cmd.Flags().SetAnnotation("similarQuery", "Categories", []string{"Search"}) cmd.Flags().String("snippetEllipsisText", "…", heredoc.Doc(`String used as an ellipsis indicator when a snippet is truncated. @@ -912,10 +912,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisTex cmd.Flags().String("sortFacetValuesBy", "count", heredoc.Doc(`Order in which to retrieve facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/`)) cmd.Flags().SetAnnotation("sortFacetValuesBy", "Categories", []string{"Faceting"}) - cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`Whether to sum all filter scores. + cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`How to calculate the filtering score. Whether to sum the scores of each matched filter or use the highest score of the filters. See: https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/`)) cmd.Flags().SetAnnotation("sumOrFiltersScores", "Categories", []string{"Filtering"}) - cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to take into account an index's synonyms for this search. + cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to use or disregard an index's synonyms for this search. See: https://www.algolia.com/doc/api-reference/api-parameters/synonyms/`)) cmd.Flags().SetAnnotation("synonyms", "Categories", []string{"Advanced"}) tagFilters := NewJSONVar([]string{"array", "string"}...) @@ -926,7 +926,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/`)) cmd.Flags().Var(typoTolerance, "typoTolerance", heredoc.Doc(`Whether typo tolerance is enabled and how it is applied. See: https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/`)) cmd.Flags().SetAnnotation("typoTolerance", "Categories", []string{"Typos"}) - cmd.Flags().String("userToken", "", heredoc.Doc(`Unique pseudonymous or anonymous user identifier. + cmd.Flags().String("userToken", "", heredoc.Doc(`Link the current search to a specific user with a user token (a unique pseudonymous or anonymous identifier). See: https://www.algolia.com/doc/api-reference/api-parameters/userToken/`)) cmd.Flags().SetAnnotation("userToken", "Categories", []string{"Personalization"}) } From 397370762ce7d8469cb9602fc76e488eebb61918 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Denoix?= Date: Tue, 11 Mar 2025 11:26:54 +0100 Subject: [PATCH 10/12] chore: update search api specs (#171) Co-authored-by: algolia-ci Co-authored-by: Dylan Tientcheu --- api/specs/search.yml | 1523 ++++++++++++++++++++++++------------- pkg/cmdutil/spec_flags.go | 80 +- 2 files changed, 1032 insertions(+), 571 deletions(-) diff --git a/api/specs/search.yml b/api/specs/search.yml index 38a0e382..8d0ec50a 100644 --- a/api/specs/search.yml +++ b/api/specs/search.yml @@ -2,7 +2,7 @@ openapi: 3.0.2 info: title: Search API description: > - The Algolia Search API lets you search, configure, and mange your indices + The Algolia Search API lets you search, configure, and manage your indices and records. @@ -42,7 +42,7 @@ info: ## Retry strategy - To guarantee a high availability, implement a retry strategy for all API + To guarantee high availability, implement a retry strategy for all API requests using the URLs of your servers as fallbacks: @@ -134,23 +134,23 @@ servers: - url: https://{appId}.algolia.net variables: appId: - default: myAppId + default: ALGOLIA_APPLICATION_ID - url: https://{appId}-1.algolianet.com variables: appId: - default: myAppId + default: ALGOLIA_APPLICATION_ID - url: https://{appId}-2.algolianet.com variables: appId: - default: myAppId + default: ALGOLIA_APPLICATION_ID - url: https://{appId}-3.algolianet.com variables: appId: - default: myAppId + default: ALGOLIA_APPLICATION_ID - url: https://{appId}-dsn.algolia.net variables: appId: - default: myAppId + default: ALGOLIA_APPLICATION_ID security: - appId: [] apiKey: [] @@ -221,6 +221,9 @@ tags: order determined by your ranking. Records are schemaless JSON objects. + + When adding or updating many records, check the [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). externalDocs: url: >- https://www.algolia.com/doc/guides/sending-and-managing-data/prepare-your-data/ @@ -233,10 +236,10 @@ tags: Rules are _if-then_ statements that you can use to curate search results. - Rules have _conditions_ which can trigger _consequences_. + Rules have _conditions_ that can trigger _consequences_. Consequences are changes to the search results, such as changing the order - of search results, or boosting a facet. + of search results or boosting a facet. This can be useful for tuning specific queries or for merchandising. externalDocs: @@ -273,7 +276,7 @@ paths: get: operationId: customGet summary: Send requests to the Algolia REST API - description: This method allow you to send requests to the Algolia REST API. + description: This method lets you send requests to the Algolia REST API. parameters: - $ref: '#/components/parameters/PathInPath' - $ref: '#/components/parameters/Parameters' @@ -303,7 +306,7 @@ paths: schema: type: object summary: Send requests to the Algolia REST API - description: This method allow you to send requests to the Algolia REST API. + description: This method lets you send requests to the Algolia REST API. parameters: - $ref: '#/components/parameters/PathInPath' - $ref: '#/components/parameters/Parameters' @@ -333,7 +336,7 @@ paths: schema: type: object summary: Send requests to the Algolia REST API - description: This method allow you to send requests to the Algolia REST API. + description: This method lets you send requests to the Algolia REST API. parameters: - $ref: '#/components/parameters/PathInPath' - $ref: '#/components/parameters/Parameters' @@ -357,7 +360,7 @@ paths: delete: operationId: customDelete summary: Send requests to the Algolia REST API - description: This method allow you to send requests to the Algolia REST API. + description: This method lets you send requests to the Algolia REST API. parameters: - $ref: '#/components/parameters/PathInPath' - $ref: '#/components/parameters/Parameters' @@ -389,7 +392,7 @@ paths: - search summary: Search an index description: > - Searches a single index and return matching search results (_hits_). + Searches a single index and returns matching search results (_hits_). This method lets you retrieve up to 1,000 hits. @@ -431,7 +434,7 @@ paths: - search summary: Search multiple indices description: > - Sends multiple search request to one or more indices. + Sends multiple search requests to one or more indices. This can be useful in these cases: @@ -551,6 +554,7 @@ paths: tags: - search operationId: browse + x-use-read-transporter: true x-acl: - browse summary: Browse for records @@ -597,7 +601,7 @@ paths: - `optionalFilters`: `[]` - - `typoTolerance`: `true` or `false` (`min` and `strict` is evaluated to + - `typoTolerance`: `true` or `false` (`min` and `strict` evaluate to `true`) @@ -633,7 +637,7 @@ paths: x-acl: - addObject description: > - Adds a record to an index or replace it. + Adds a record to an index or replaces it. - If the record doesn't have an object ID, a new record with an @@ -650,17 +654,21 @@ paths: To update _some_ attributes of a record, use the [`partial` - operation](#tag/Records/operation/partial). + operation](#tag/Records/operation/partialUpdateObject). To add, update, or replace multiple records, use the [`batch` operation](#tag/Records/operation/batch). - summary: Add or replace a record + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). + summary: Add a new record (with auto-generated object ID) parameters: - $ref: '#/components/parameters/IndexName' requestBody: required: true description: >- - The record, a schemaless object with attributes that are useful in the + The record. A schemaless object with attributes that are useful in the context of search and discovery. content: application/json: @@ -777,11 +785,8 @@ paths: content: application/json: schema: - title: getObjectResponse type: object description: The requested record. - additionalProperties: - $ref: '#/components/schemas/attribute' '400': $ref: '#/components/responses/BadRequest' '402': @@ -804,6 +809,9 @@ paths: Otherwise, a new record is added to the index. + If you want to use auto-generated object IDs, use the [`saveObject` + operation](#tag/Records/operation/saveObject). + To update _some_ attributes of an existing record, use the [`partial` operation](#tag/Records/operation/partialUpdateObject) instead. @@ -815,7 +823,7 @@ paths: requestBody: required: true description: >- - The record, a schemaless object with attributes that are useful in the + The record. A schemaless object with attributes that are useful in the context of search and discovery. content: application/json: @@ -869,16 +877,29 @@ paths: operationId: deleteBy x-acl: - deleteIndex - summary: Delete records matching a query + summary: Delete records matching a filter description: > - This operation doesn't accept empty queries or filters. + This operation doesn't accept empty filters. + + This operation is resource-intensive. + + You should only use it if you can't get the object IDs of the records + you want to delete. It's more efficient to get a list of object IDs with the [`browse` operation](#tag/Search/operation/browse), and then delete the records using the [`batch` - operation](tag/Records/operation/batch). + operation](#tag/Records/operation/batch). + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). + externalDocs: + url: >- + https://support.algolia.com/hc/en-us/articles/16385098766353-Should-I-use-the-deleteby-method-for-deleting-records-matching-a-query- + description: Should I use the deleteBy method for deleting records. parameters: - $ref: '#/components/parameters/IndexName' requestBody: @@ -889,7 +910,7 @@ paths: $ref: '#/components/schemas/deleteByParams' responses: '200': - $ref: '#/components/responses/DeletedAt' + $ref: '#/components/responses/UpdatedAt' '400': $ref: '#/components/responses/BadRequest' '402': @@ -906,9 +927,12 @@ paths: x-acl: - deleteIndex summary: Delete all records from an index - description: >- + description: > Deletes only the records from an index while keeping settings, synonyms, and rules. + + This operation is resource-intensive and subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). parameters: - $ref: '#/components/parameters/IndexName' responses: @@ -931,14 +955,65 @@ paths: - addObject summary: Add or update attributes x-codegen-request-body-name: attributesToUpdate - description: | - Adds new attributes to a record, or update existing ones. + description: > + Adds new attributes to a record, or updates existing ones. + - If a record with the specified object ID doesn't exist, a new record is added to the index **if** `createIfNotExists` is true. - If the index doesn't exist yet, this method creates a new index. + - You can use any first-level attribute but not nested attributes. - If you specify a nested attribute, the engine treats it as a replacement for its first-level ancestor. + If you specify a nested attribute, this operation replaces its first-level ancestor. + + To update an attribute without pushing the entire record, you can use + these built-in operations. + + These operations can be helpful if you don't have access to your initial + data. + + + - Increment: increment a numeric attribute + + - Decrement: decrement a numeric attribute + + - Add: append a number or string element to an array attribute + + - Remove: remove all matching number or string elements from an array + attribute made of numbers or strings + + - AddUnique: add a number or string element to an array attribute made + of numbers or strings only if it's not already present + + - IncrementFrom: increment a numeric integer attribute only if the + provided value matches the current value, and otherwise ignore the whole + object update. For example, if you pass an IncrementFrom value of 2 for + the version attribute, but the current value of the attribute is 1, the + engine ignores the update. If the object doesn't exist, the engine only + creates it if you pass an IncrementFrom value of 0. + + - IncrementSet: increment a numeric integer attribute only if the + provided value is greater than the current value, and otherwise ignore + the whole object update. For example, if you pass an IncrementSet value + of 2 for the version attribute, and the current value of the attribute + is 1, the engine updates the object. If the object doesn't exist yet, + the engine only creates it if you pass an IncrementSet value greater + than 0. + + + You can specify an operation by providing an object with the attribute + to update as the key and its value being an object with the following + properties: + + + - _operation: the operation to apply on the attribute + + - value: the right-hand side argument to the operation, for example, + increment or decrement step, value to add or remove. + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). parameters: - $ref: '#/components/parameters/IndexName' - $ref: '#/components/parameters/ObjectID' @@ -954,10 +1029,8 @@ paths: content: application/json: schema: - type: object description: Attributes to update. - additionalProperties: - $ref: '#/components/schemas/attributeToUpdate' + type: object responses: '200': $ref: '#/components/responses/UpdatedAtWithObjectId' @@ -987,54 +1060,19 @@ paths: - Actions are equivalent to the individual API requests of the same name. + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). parameters: - $ref: '#/components/parameters/IndexName' + x-codegen-request-body-name: batchWriteParams requestBody: - required: true content: application/json: schema: - title: batchWriteParams - description: Batch parameters. - type: object - additionalProperties: false - properties: - requests: - type: array - items: - title: batchRequest - type: object - additionalProperties: false - properties: - action: - $ref: '#/components/schemas/action' - body: - type: object - description: Operation arguments (varies with specified `action`). - example: - name: Betty Jane McCamey - company: Vita Foods Inc. - email: betty@mccamey.com - required: - - action - - body - required: - - requests - examples: - batch: - summary: Batch indexing request - value: - requests: - - action: addObject - body: - name: Betty Jane McCamey - company: Vita Foods Inc. - email: betty@mccamey.com - - action: addObject - body: - name: Gayla geimer - company: Ortman McCain Co. - email: gayla@geimer.com + $ref: '#/components/schemas/batchWriteParams' + required: true responses: '200': description: OK @@ -1064,6 +1102,10 @@ paths: - Actions are equivalent to the individual API requests of the same name. + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). summary: Batch indexing operations on multiple indices requestBody: required: true @@ -1091,7 +1133,6 @@ paths: $ref: '#/components/schemas/indexName' required: - action - - body - indexName required: - requests @@ -1168,8 +1209,8 @@ paths: requests: type: array items: - description: Request body for retrieving records. title: getObjectsRequest + description: Request body for retrieving records. type: object additionalProperties: false required: @@ -1209,6 +1250,10 @@ paths: type: object additionalProperties: false properties: + message: + type: string + description: An optional status message. + example: Index INDEX_NAME does not exist. results: type: array description: Retrieved records. @@ -1243,7 +1288,7 @@ paths: content: application/json: schema: - $ref: '#/components/schemas/indexSettings' + $ref: '#/components/schemas/settingsResponse' '400': $ref: '#/components/responses/BadRequest' '402': @@ -1299,7 +1344,7 @@ paths: - settings summary: Retrieve a synonym description: | - Retrieves a syonym by its ID. + Retrieves a synonym by its ID. To find the object IDs for your synonyms, use the [`search` operation](#tag/Synonyms/operation/searchSynonyms). parameters: @@ -1408,9 +1453,14 @@ paths: x-acl: - editSettings summary: Create or replace synonyms - description: | + description: > If a synonym with the `objectID` doesn't exist, Algolia adds a new one. + Otherwise, existing synonyms are replaced. + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). parameters: - $ref: '#/components/parameters/IndexName' - $ref: '#/components/parameters/ForwardToReplicas' @@ -1579,7 +1629,9 @@ paths: for any of your application's keys. When authenticating with other API keys, you can only retrieve - information for that key. + information for that key, + + with the description replaced by ``. parameters: - $ref: '#/components/parameters/KeyString' responses: @@ -1767,11 +1819,7 @@ paths: $ref: '#/components/schemas/rule' responses: '200': - description: OK - content: - application/json: - schema: - $ref: '#/components/schemas/updatedRuleResponse' + $ref: '#/components/responses/UpdatedAt' '400': $ref: '#/components/responses/BadRequest' '402': @@ -1822,6 +1870,10 @@ paths: new one. Otherwise, existing rules are replaced. + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). x-codegen-request-body-name: rules parameters: - $ref: '#/components/parameters/IndexName' @@ -1887,8 +1939,8 @@ paths: content: application/json: schema: - type: object title: searchRulesParams + type: object description: Rules search parameters. additionalProperties: false properties: @@ -2838,30 +2890,22 @@ paths: maxLength: 1000 description: Response body. example: > - 'n{\n \"results\": [\n {\n \"hits\": [\n - {\n \"name\": \"Amazon - Fire TV Stick\",\n - \"description\": \"Amazon Fire TV Stick connects to + 'n{\n "results": [\n {\n "hits": [\n {\n + "name": "Amazon - Fire TV Stick",\n + "description": "Amazon Fire TV Stick connects to your TV's HDMI port. Just grab and go to enjoy Netflix, Prime Instant Video, Hulu Plus, - YouTube.com, music, and much more.\",\n - \"brand\": \"Amazon\",\n \"categories\": - [\n \"TV & Home Theater\",\n \"Streaming - Media Players\"\n ],\n - \"hierarchicalCategories\": {\n \"lvl0\": \"TV - & Home Theater\",\n \"lvl1\": \"TV & Home - Theater > Streaming Media Players\"\n },\n - \"type\": \"Streaming media plyr\",\n \"price\": - 39.99,\n \"price_range\": \"1 - 50\",\n - \"image\": - \"https:\/\/cdn-demo.algolia.com\/bestbuy\/9999119_sb.jpg\",\n - \"url\": - \"http:\/\/www.bestbuy.com\/site\/amazon-fire-tv-stick\/9999119.p?id=1219460752591&skuId=9999119&cmp=RMX&ky=1uWSHMdQqBeVJB9cXgEke60s5EjfS6M1W\",\n - \"free_shipping\": false,\n \"popularity\": - 9843,\n \"rating\": 4,\n \"objectID\": - \"9999119\"\n' + YouTube.com, music, and much more.",\n "brand": + "Amazon",\n "categories": [\n "TV & Home + Theater",\n "Streaming Media Players"\n + ],\n "hierarchicalCategories": {\n "lvl0": + "TV & Home Theater",\n "lvl1": "TV & Home + Theater > Streaming Media Players"\n },\n + "type": "Streaming media player",\n "price": + 39.99,\n "price_range": "1 }\n ]\n }\n ]\n}' url: type: string - format: uri + format: uri-reference description: URL of the API endpoint. example: /1/indexes ip: @@ -2911,8 +2955,8 @@ paths: type: array description: Queries performed for the given request. items: - type: object title: logQuery + type: object properties: index_name: type: string @@ -2936,7 +2980,6 @@ paths: - ip - query_headers - sha1 - - nb_api_calls - processing_time_ms '400': $ref: '#/components/responses/BadRequest' @@ -2971,14 +3014,7 @@ paths: content: application/json: schema: - title: getTaskResponse - type: object - additionalProperties: false - properties: - status: - $ref: '#/components/schemas/taskStatus' - required: - - status + $ref: '#/components/schemas/GetTaskResponse' '400': $ref: '#/components/responses/BadRequest' '402': @@ -3023,14 +3059,7 @@ paths: content: application/json: schema: - title: getTaskResponse - type: object - additionalProperties: false - properties: - status: - $ref: '#/components/schemas/taskStatus' - required: - - status + $ref: '#/components/schemas/GetTaskResponse' '400': $ref: '#/components/responses/BadRequest' '402': @@ -3056,6 +3085,8 @@ paths: - If the destination index doesn't exist yet, it'll be created. + - This operation is resource-intensive. + **Copy** @@ -3083,7 +3114,7 @@ paths: - Moving a source index that doesn't exist is ignored without returning an error. - - When moving an index, the analytics data keep their original name and + - When moving an index, the analytics data keeps its original name, and a new set of analytics data is started for the new name. To access the original analytics in the dashboard, create an index with the original name. - If the destination index has replicas, moving will overwrite the @@ -3091,6 +3122,10 @@ paths: - Related guide: [Move indices](https://www.algolia.com/doc/guides/sending-and-managing-data/manage-indices-and-apps/manage-indices/how-to/move-indices/). + + + This operation is subject to [indexing rate + limits](https://support.algolia.com/hc/en-us/articles/4406975251089-Is-there-a-rate-limit-for-indexing-on-Algolia). parameters: - $ref: '#/components/parameters/IndexName' requestBody: @@ -3203,6 +3238,106 @@ paths: $ref: '#/components/schemas/getApiKeyResponse' '400': $ref: '#/components/responses/IndexNotFound' + /waitForTask: + get: + x-helper: true + tags: + - search + operationId: waitForTask + summary: Wait for operation to complete + description: > + Wait for a task to complete to ensure synchronized index updates. + + + All Algolia write operations are asynchronous. When you make a request + for a write operation, for example, to add or update records in your + index, Algolia creates a task on a queue and returns a taskID. The task + itself runs separately, depending on the server load. + parameters: + - in: query + name: indexName + description: The name of the index on which the operation was performed. + required: true + schema: + type: string + - in: query + name: taskID + description: The taskID returned by the operation. + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetTaskResponse' + '400': + $ref: '#/components/responses/IndexNotFound' + /waitForAppTask: + get: + x-helper: true + operationId: waitForAppTask + summary: Wait for application-level operation to complete + description: Wait for a application-level task to complete. + parameters: + - in: query + name: taskID + description: The taskID returned by the operation. + required: true + schema: + type: integer + format: int64 + responses: + '200': + description: OK + content: + application/json: + schema: + $ref: '#/components/schemas/GetTaskResponse' + '400': + $ref: '#/components/responses/IndexNotFound' + tags: + - search + /browseObjects: + get: + x-helper: true + tags: + - search + operationId: browseObjects + summary: Get all records from an index + description: > + You can use the browse method to get records from an index—for example, + to export your index as a backup. To export all records, use an empty + query. + + + Use browse instead of search when exporting records from your index, + when ranking, or analytics, isn't important. The Analytics API doesn't + collect data when using browse. + + + Don't use this method for building a search UI. Use search instead. + parameters: + - in: query + name: indexName + description: The name of the index on which the operation was performed. + required: true + schema: + type: string + - in: query + name: browseParams + description: Browse parameters. + required: true + schema: + $ref: '#/components/schemas/browseParamsObject' + responses: + '204': + description: No content. + '400': + $ref: '#/components/responses/IndexNotFound' /generateSecuredApiKey: get: x-helper: true @@ -3324,6 +3459,17 @@ paths: required: false schema: type: integer + default: 1000 + - in: query + name: scopes + description: >- + List of scopes to kepp in the index. Defaults to `settings`, + `synonyms`, and `rules`. + required: false + schema: + type: array + items: + $ref: '#/components/schemas/scopeType' responses: '200': description: OK @@ -3420,6 +3566,25 @@ paths: type: array items: type: object + - in: query + name: waitForTasks + description: >- + 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. + required: false + schema: + type: boolean + default: false + - in: query + name: batchSize + description: >- + The size of the chunk of `objects`. The number of `batch` calls will + be equal to `length(objects) / batchSize`. Defaults to 1000. + required: false + schema: + type: integer + default: 1000 - in: query name: requestOptions description: The request options to pass to the `batch` method. @@ -3463,6 +3628,23 @@ paths: type: array items: type: string + - in: query + name: waitForTasks + description: >- + 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. + required: false + schema: + type: boolean + - in: query + name: batchSize + description: >- + The size of the chunk of `objects`. The number of `batch` calls will + be equal to `length(objects) / batchSize`. Defaults to 1000. + required: false + schema: + type: integer - in: query name: requestOptions description: The request options to pass to the `batch` method. @@ -3517,6 +3699,26 @@ paths: required: false schema: type: boolean + default: false + - in: query + name: waitForTasks + description: >- + 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. + required: false + schema: + type: boolean + default: false + - in: query + name: batchSize + description: >- + The size of the chunk of `objects`. The number of `batch` calls will + be equal to `length(objects) / batchSize`. Defaults to 1000. + required: false + schema: + type: integer + default: 1000 - in: query name: requestOptions description: The request options to pass to the `batch` method. @@ -3534,147 +3736,83 @@ paths: $ref: '#/components/schemas/batchResponse' '400': $ref: '#/components/responses/IndexNotFound' -components: - securitySchemes: - appId: - type: apiKey - in: header - name: x-algolia-application-id - description: Your Algolia application ID. - apiKey: - type: apiKey - in: header - name: x-algolia-api-key + /indexExists: + get: + x-helper: true + tags: + - search + operationId: indexExists + summary: Check if an index exists or not description: > - Your Algolia API key with the necessary permissions to make the request. - - Permissions are controlled through access control lists (ACL) and access - restrictions. + You can initialize an index with any name. The index is created on + Algolia's servers when you add objects or set settings. To prevent + accidentally creating new indices, or changing existing indices, you can + use the exists method. The exists method returns a boolean that + indicates whether an initialized index has been created. + parameters: + - in: query + name: indexName + description: The name of the index to check. + required: true + schema: + type: string + responses: + '200': + description: Index exists. + content: + application/json: + schema: + type: boolean + /setClientApiKey: + get: + x-helper: true + x-asynchronous-helper: false + tags: + - search + operationId: setClientApiKey + summary: Switch the API key used to authenticate requests + description: | + Switch the API key used to authenticate requests. + parameters: + - in: query + name: apiKey + description: API key to be used from now on. + required: true + schema: + type: string + responses: + '204': + description: No content. +components: + securitySchemes: + appId: + type: apiKey + in: header + name: x-algolia-application-id + description: Your Algolia application ID. + apiKey: + type: apiKey + in: header + name: x-algolia-api-key + description: > + Your Algolia API key with the necessary permissions to make the request. + + Permissions are controlled through access control lists (ACL) and access + restrictions. The required ACL to make a request is listed in each endpoint's reference. - parameters: - PathInPath: - name: path - in: path - description: Path of the endpoint, anything after "/1" must be specified. - required: true - schema: - type: string - example: /keys - Parameters: - name: parameters - in: query - description: Query parameters to apply to the current query. - schema: - type: object - additionalProperties: true - IndexName: - name: indexName - in: path - description: Name of the index on which to perform the operation. - required: true - schema: - type: string - example: YourIndexName - ObjectID: - name: objectID - in: path - description: Unique record identifier. - required: true - schema: - $ref: '#/components/schemas/objectID' - ForwardToReplicas: - in: query - name: forwardToReplicas - required: false - description: Whether changes are applied to replica indices. - schema: - type: boolean - parameters_ObjectID: - name: objectID - in: path - description: Unique identifier of a synonym object. - required: true - schema: - type: string - example: synonymID - ReplaceExistingSynonyms: - in: query - name: replaceExistingSynonyms - schema: - type: boolean - description: >- - Whether to replace all synonyms in the index with the ones sent with - this request. - KeyString: - in: path - name: key - required: true - schema: - type: string - example: YourAPIKey - description: API key. - ObjectIDRule: - in: path - name: objectID - description: Unique identifier of a rule object. - required: true - schema: - $ref: '#/components/schemas/ruleID' - ClearExistingRules: - in: query - name: clearExistingRules - required: false - schema: - type: boolean - description: Whether existing rules should be deleted before adding this batch. - DictionaryName: - in: path - name: dictionaryName - description: Dictionary type in which to search. - required: true - schema: - $ref: '#/components/schemas/dictionaryType' - Page: - in: query - name: page - description: | - Requested page of the API response. - If `null`, the API response is not paginated. - required: false - schema: - oneOf: - - type: integer - minimum: 0 - - type: 'null' - default: null - HitsPerPage: - in: query - name: hitsPerPage - description: Number of hits per page. - required: false - schema: - type: integer - default: 100 - UserIDInHeader: - name: X-Algolia-User-ID - description: Unique identifier of the user who makes the search request. - in: header - required: true - schema: - $ref: '#/components/schemas/userID' - UserIDInPath: - name: userID - description: Unique identifier of the user who makes the search request. - in: path - required: true - schema: - $ref: '#/components/schemas/userID' schemas: + attributeToUpdate: + x-keep-model: true + deprecated: true + oneOf: + - type: string + - $ref: '#/components/schemas/builtInOperation' ErrorBase: description: Error. type: object + x-keep-model: true additionalProperties: true properties: message: @@ -3687,7 +3825,8 @@ components: default: '' searchParamsString: type: object - title: Search parameters as query string + title: Search parameters as query string. + description: Search parameters as query string. additionalProperties: false x-discriminator-fields: - params @@ -3806,6 +3945,10 @@ components: - Optional filters are applied _after_ sort-by attributes. + - Optional filters are applied _before_ custom ranking attributes (in + the default + [ranking](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/)). + - Optional filters don't work with numeric attributes. example: - category:Book @@ -3827,7 +3970,9 @@ components: You can use numeric comparison operators: `<`, `<=`, `=`, `!=`, `>`, - `>=`. Comparsions are precise up to 3 decimals. + `>=`. + + Comparisons are precise up to 3 decimals. You can also provide ranges: `facet: TO `. The range includes the lower and upper boundaries. @@ -3886,7 +4031,7 @@ components: string of latitude and longitude. - Only records included within circle around this central location are + Only records included within a circle around this central location are included in the results. The radius of the circle is determined by the `aroundRadius` and @@ -3936,6 +4081,7 @@ components: title: range objects type: array items: + title: range type: object description: >- Range object with lower and upper values in meters to define custom @@ -3975,7 +4121,7 @@ components: - $ref: '#/components/schemas/aroundPrecisionFromValue' x-categories: - Geo-Search - insideBoundingBox: + insideBoundingBoxArray: type: array items: type: array @@ -4010,6 +4156,11 @@ components: - 1.9916 x-categories: - Geo-Search + insideBoundingBox: + oneOf: + - type: string + - type: 'null' + - $ref: '#/components/schemas/insideBoundingBoxArray' insidePolygon: type: array items: @@ -4249,7 +4400,7 @@ components: length: type: integer description: Number of hits to retrieve (used in combination with `offset`). - minimum: 1 + minimum: 0 maximum: 1000 x-categories: - Pagination @@ -4415,8 +4566,8 @@ components: If typo tolerance is true, `min`, or `strict`, [word splitting and - concetenation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/splitting-and-concatenation/) - is also active. + concatenation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/splitting-and-concatenation/) + are also active. oneOf: - type: boolean default: true @@ -4426,6 +4577,11 @@ components: - $ref: '#/components/schemas/typoToleranceEnum' x-categories: - Typos + booleanString: + type: string + enum: + - 'true' + - 'false' ignorePlurals: description: | Treat singular, plurals, and other forms of declensions as equivalent. @@ -4440,6 +4596,7 @@ components: This overrides languages you set with `queryLanguages`. items: $ref: '#/components/schemas/supportedLanguage' + - $ref: '#/components/schemas/booleanString' - type: boolean description: > If true, `ignorePlurals` is active for all languages included in @@ -4493,7 +4650,7 @@ components: Determines if and how query words are interpreted as prefixes. - By default, only the last query word is treated as prefix + By default, only the last query word is treated as a prefix (`prefixLast`). To turn off prefix search, use `prefixNone`. @@ -4573,6 +4730,61 @@ components: items: type: string - type: 'null' + optionalWordsArray: + type: array + items: + type: string + example: + - blue + - iphone case + description: >- + List of [optional + words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words). + default: [] + x-categories: + - Query strategy + optionalWords: + description: > + Words that should be considered optional when found in the query. + + + By default, records must match all words in the search query to be + included in the search results. + + Adding optional words can help to increase the number of search results + by running an additional search query that doesn't include the optional + words. + + For example, if the search query is "action video" and "video" is an + optional word, + + the search engine runs two queries. One for "action video" and one for + "action". + + Records that match all words are ranked higher. + + + For a search query with 4 or more words **and** all its words are + optional, + + the number of matched words required for a record to be included in the + search results increases for every 1,000 records: + + + - If `optionalWords` has less than 10 words, the required number of + matched words increases by 1: + results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 2 matched words. + - If `optionalWords` has 10 or more words, the number of required + matched words increases by the number of optional words divided by 5 + (rounded down). + For example, with 18 optional words: results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 4 matched words. + + For more information, see [Optional + words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words). + oneOf: + - type: string + - type: 'null' + - $ref: '#/components/schemas/optionalWordsArray' exactOnSingleWordQuery: type: string enum: @@ -4606,6 +4818,7 @@ components: - ignorePlurals - singleWordSynonym - multiWordsSynonym + - ignoreConjugations x-categories: - Query strategy advancedSyntaxFeatures: @@ -4656,15 +4869,6 @@ components: default: 0 x-categories: - Advanced - maxFacetHits: - type: integer - description: >- - Maximum number of facet values to return when [searching for facet - values](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#search-for-facet-values). - maximum: 100 - default: 10 - x-categories: - - Advanced order: description: > Explicit order of facets or facet values. @@ -4739,13 +4943,59 @@ components: properties: url: type: string + bannerImageUrl: + description: URL for an image to show inside a banner. + type: object + additionalProperties: false + properties: + url: + type: string + bannerImage: + description: Image to show inside a banner. + type: object + additionalProperties: false + properties: + urls: + type: array + items: + $ref: '#/components/schemas/bannerImageUrl' + title: + type: string + bannerLink: + description: Link for a banner defined in the Merchandising Studio. + type: object + additionalProperties: false + properties: + url: + type: string + banner: + description: Banner with image and link to redirect users. + type: object + additionalProperties: false + properties: + image: + $ref: '#/components/schemas/bannerImage' + link: + $ref: '#/components/schemas/bannerLink' + banners: + description: Banners defined in the Merchandising Studio for a given search. + type: array + items: + $ref: '#/components/schemas/banner' + widgets: + description: Widgets returned from any rules that are applied to the current search. + type: object + additionalProperties: false + properties: + banners: + $ref: '#/components/schemas/banners' renderingContent: description: > Extra data that can be used in the search UI. - You can use this to control aspects of your search UI, such as, the - order of facet names and values + You can use this to control aspects of your search UI, such as the order + of facet names and values without changing your frontend code. type: object @@ -4755,6 +5005,8 @@ components: $ref: '#/components/schemas/facetOrdering' redirect: $ref: '#/components/schemas/redirectURL' + widgets: + $ref: '#/components/schemas/widgets' x-categories: - Advanced reRankingApplyFilter: @@ -4848,79 +5100,36 @@ components: - custom x-categories: - Ranking - customRanking: + relevancyStrictness: + type: integer + example: 90 + description: > + Relevancy threshold below which less relevant results aren't + included in the results. + + + You can only set `relevancyStrictness` on [virtual replica + indices](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/#what-are-virtual-replicas). + + Use this setting to strike a balance between the relevance and + number of returned results. + default: 100 + x-categories: + - Ranking + attributesToHighlight: type: array items: type: string example: - - desc(popularity) - - asc(price) + - author + - title + - conten + - content description: > - Attributes to use as [custom - ranking](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/). + Attributes to highlight. - Attribute names are case-sensitive. - - The custom ranking attributes decide which items are shown first if - the other ranking criteria are equal. - - - Records with missing values for your selected custom ranking - attributes are always sorted last. - - Boolean attributes are sorted based on their alphabetical order. - - - **Modifiers** - - - - `asc("ATTRIBUTE")`. - Sort the index by the values of an attribute, in ascending order. - - - `desc("ATTRIBUTE")`. - Sort the index by the values of an attribute, in descending order. - - If you use two or more custom ranking attributes, - - [reduce the - precision](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision/) - of your first attributes, - - or the other attributes will never be applied. - default: [] - x-categories: - - Ranking - relevancyStrictness: - type: integer - example: 90 - description: > - Relevancy threshold below which less relevant results aren't - included in the results. - - - You can only set `relevancyStrictness` on [virtual replica - indices](https://www.algolia.com/doc/guides/managing-results/refine-results/sorting/in-depth/replicas/#what-are-virtual-replicas). - - Use this setting to strike a balance between the relevance and - number of returned results. - default: 100 - x-categories: - - Ranking - attributesToHighlight: - type: array - items: - type: string - example: - - author - - title - - conten - - content - description: > - Attributes to highlight. - - - By default, all searchable attributes are highlighted. + By default, all searchable attributes are highlighted. Use `*` to highlight all attributes or use an empty array `[]` to turn off highlighting. @@ -5063,18 +5272,6 @@ components: $ref: '#/components/schemas/ignorePlurals' removeStopWords: $ref: '#/components/schemas/removeStopWords' - keepDiacriticsOnCharacters: - type: string - example: øé - description: | - Characters for which diacritics should be preserved. - - By default, Algolia removes diacritics from letters. - For example, `é` becomes `e`. If this causes issues in your search, - you can specify characters that should keep their diacritics. - default: '' - x-categories: - - Languages queryLanguages: type: array items: @@ -5116,7 +5313,8 @@ components: decompoundQuery: type: boolean description: > - Whether to split compound words into their building blocks. + Whether to split compound words in the query into their building + blocks. For more information, see [Word @@ -5124,6 +5322,12 @@ components: Word segmentation is supported for these languages: German, Dutch, Finnish, Swedish, and Norwegian. + + Decompounding doesn't work for words with [non-spacing mark Unicode + characters](https://www.charactercodes.net/category/non-spacing_mark). + + For example, `Gartenstühle` won't be decompounded if the `ü` + consists of `u` (U+0075) and `◌̈` (U+0308). default: true x-categories: - Languages @@ -5160,52 +5364,7 @@ components: x-categories: - Query strategy optionalWords: - type: array - items: - type: string - example: - - blue - - iphone case - description: > - Words that should be considered optional when found in the query. - - - By default, records must match all words in the search query to be - included in the search results. - - Adding optional words can help to increase the number of search - results by running an additional search query that doesn't include - the optional words. - - For example, if the search query is "action video" and "video" is an - optional word, - - the search engine runs two queries. One for "action video" and one - for "action". - - Records that match all words are ranked higher. - - - For a search query with 4 or more words **and** all its words are - optional, - - the number of matched words required for a record to be included in - the search results increases for every 1,000 records: - - - - If `optionalWords` has less than 10 words, the required number of - matched words increases by 1: - results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 2 matched words. - - If `optionalWords` has 10 or more words, the number of required - matched words increases by the number of optional words dividied by - 5 (rounded down). - For example, with 18 optional words: results 1 to 1,000 require 1 matched word, results 1,001 to 2000 need 4 matched words. - - For more information, see [Optional - words](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/empty-or-insufficient-results/#creating-a-list-of-optional-words). - default: [] - x-categories: - - Query strategy + $ref: '#/components/schemas/optionalWords' disableExactOnAttributes: type: array items: @@ -5221,7 +5380,7 @@ components: This can be useful for attributes with long values, where the - likelyhood of an exact match is high, + likelihood of an exact match is high, such as product descriptions. @@ -5240,18 +5399,32 @@ components: items: $ref: '#/components/schemas/alternativesAsExact' description: > - Alternatives of query words that should be considered as exact - matches by the Exact ranking criterion. + Determine which plurals and synonyms should be considered an exact + matches. + + + By default, Algolia treats singular and plural forms of a word, and + single-word synonyms, as + [exact](https://www.algolia.com/doc/guides/managing-results/relevance-overview/in-depth/ranking-criteria/#exact) + matches when searching. + + For example: + + + - "swimsuit" and "swimsuits" are treated the same + + - "swimsuit" and "swimwear" are treated the same (if they are + [synonyms](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/adding-synonyms/#regular-synonyms)). - `ignorePlurals`. Plurals and similar declensions added by the `ignorePlurals` setting are considered exact matches. - `singleWordSynonym`. - Single-word synonyms, such as "NY/NYC" are considered exact matches. + Single-word synonyms, such as "NY" = "NYC", are considered exact matches. - `multiWordsSynonym`. - Multi-word synonyms, such as "NY/New York" are considered exact matches. + Multi-word synonyms, such as "NY" = "New York", are considered exact matches. default: - ignorePlurals - singleWordSynonym @@ -5301,7 +5474,7 @@ components: With `replaceSynonymsInHighlight` set to `true`, a search for `home` still matches the same records, - but all occurences of "house" are replaced by "home" in the + but all occurrences of "house" are replaced by "home" in the highlighted response. default: false x-categories: @@ -5330,31 +5503,39 @@ components: items: type: string description: > - Properties to include in the API response of `search` and `browse` + Properties to include in the API response of search and browse requests. By default, all response properties are included. - To reduce the response size, you can select, which attributes should + To reduce the response size, you can select which properties should be included. + An empty list may lead to an empty API response (except properties + you can't exclude). + + You can't exclude these properties: - `message`, `warning`, `cursor`, `serverUsed`, `indexUsed`, + `message`, `warning`, `cursor`, `abTestVariantID`, - `abTestVariantID`, `parsedQuery`, or any property triggered by the - `getRankingInfo` parameter. + or any property added by setting `getRankingInfo` to true. - Don't exclude properties that you might need in your search UI. + Your search depends on the `hits` field. If you omit this field, + searches won't return any results. + + Your UI might also depend on other properties, for example, for + pagination. + + Before restricting the response size, check the impact on your + search experience. default: - '*' x-categories: - Advanced - maxFacetHits: - $ref: '#/components/schemas/maxFacetHits' maxValuesPerFacet: type: integer description: Maximum number of facet values to return for each facet. @@ -5451,6 +5632,7 @@ components: type: boolean description: Redirect rule status. data: + title: redirectRuleIndexData type: object description: Redirect rule data. required: @@ -5502,8 +5684,8 @@ components: type: string description: Distance from a central coordinate provided by `aroundLatLng`. exhaustive: - type: object title: exhaustive + type: object description: >- Whether certain properties of the search response are calculated exhaustive (exact) or approximated. @@ -5550,6 +5732,12 @@ components: default) to be processed (this can happen when a lot of typo alternatives exist for the query). This field will not be included when typo-tolerance is entirely disabled. + appliedRules: + description: Rules applied to the query. + title: appliedRules + type: array + items: + type: object exhaustiveFacetsCount: type: boolean description: >- @@ -5579,12 +5767,11 @@ components: food: 1 tech: 42 facets_stats: - title: facetsStats type: object description: Statistics for numerical facets. additionalProperties: - type: object title: facetStats + type: object properties: min: type: number @@ -5668,6 +5855,9 @@ components: Unique identifier for the query. This is used for [click analytics](https://www.algolia.com/doc/guides/analytics/click-analytics/). example: a00dbc80a8d13c4565a442e7e2dca80a + _automaticInsights: + type: boolean + description: Whether automatic events collection is enabled for the application. nbHits: type: integer description: Number of results (hits). @@ -5688,11 +5878,6 @@ components: $ref: '#/components/schemas/nbPages' hitsPerPage: $ref: '#/components/schemas/hitsPerPage' - required: - - page - - nbHits - - nbPages - - hitsPerPage objectID: type: string description: Unique record identifier. @@ -5731,27 +5916,25 @@ components: - value - matchLevel - matchedWords - highlightResultOptionMap: + x-discriminator-fields: + - matchLevel + - matchedWords + highlightResultMap: type: object description: Surround words that match the query with HTML tags for highlighting. + x-is-free-form: false additionalProperties: x-additionalPropertiesName: attribute - $ref: '#/components/schemas/highlightResultOption' - highlightResultOptionArray: - type: array - description: Surround words that match the query with HTML tags for highlighting. - items: - $ref: '#/components/schemas/highlightResultOption' + $ref: '#/components/schemas/highlightResult' highlightResult: oneOf: - $ref: '#/components/schemas/highlightResultOption' - - $ref: '#/components/schemas/highlightResultOptionMap' - - $ref: '#/components/schemas/highlightResultOptionArray' - highlightResultMap: - type: object + - $ref: '#/components/schemas/highlightResultMap' + - $ref: '#/components/schemas/highlightResultArray' + highlightResultArray: + type: array description: Surround words that match the query with HTML tags for highlighting. - additionalProperties: - x-additionalPropertiesName: attribute + items: $ref: '#/components/schemas/highlightResult' snippetResultOption: type: object @@ -5765,27 +5948,24 @@ components: required: - value - matchLevel - snippetResultOptionMap: + x-discriminator-fields: + - matchLevel + snippetResultMap: type: object description: Snippets that show the context around a matching search query. + x-is-free-form: false additionalProperties: x-additionalPropertiesName: attribute - $ref: '#/components/schemas/snippetResultOption' - snippetResultOptionArray: - type: array - description: Snippets that show the context around a matching search query. - items: - $ref: '#/components/schemas/snippetResultOption' + $ref: '#/components/schemas/snippetResult' snippetResult: oneOf: - $ref: '#/components/schemas/snippetResultOption' - - $ref: '#/components/schemas/snippetResultOptionMap' - - $ref: '#/components/schemas/snippetResultOptionArray' - snippetResultMap: - type: object + - $ref: '#/components/schemas/snippetResultMap' + - $ref: '#/components/schemas/snippetResultArray' + snippetResultArray: + type: array description: Snippets that show the context around a matching search query. - additionalProperties: - x-additionalPropertiesName: attribute + items: $ref: '#/components/schemas/snippetResult' matchedGeoLocation: type: object @@ -5880,7 +6060,7 @@ components: - geoDistance - nbExactWords - userScore - _distinctSeqID: + distinctSeqID: type: integer hit: type: object @@ -5904,7 +6084,7 @@ components: _rankingInfo: $ref: '#/components/schemas/rankingInfo' _distinctSeqID: - $ref: '#/components/schemas/_distinctSeqID' + $ref: '#/components/schemas/distinctSeqID' searchHits: type: object additionalProperties: true @@ -5968,6 +6148,15 @@ components: description: Text to search inside the facet's values. example: george default: '' + maxFacetHits: + type: integer + description: >- + Maximum number of facet values to return when [searching for facet + values](https://www.algolia.com/doc/guides/managing-results/refine-results/faceting/#search-for-facet-values). + maximum: 100 + default: 10 + x-categories: + - Advanced searchTypeFacet: type: string enum: @@ -6033,8 +6222,8 @@ components: type: array description: Matching facet values. items: - type: object title: facetHits + type: object additionalProperties: false required: - value @@ -6125,9 +6314,6 @@ components: type: string example: '2023-06-27T14:42:38.831Z' description: Date and time when the object was deleted, in RFC 3339 format. - attribute: - type: string - description: Value of the attribute to update. updatedAt: type: string example: '2023-07-04T12:49:15Z' @@ -6164,36 +6350,6 @@ components: $ref: '#/components/schemas/taskID' updatedAt: $ref: '#/components/schemas/updatedAt' - builtInOperationType: - type: string - enum: - - Increment - - Decrement - - Add - - Remove - - AddUnique - - IncrementFrom - - IncrementSet - description: How to change the attribute. - builtInOperation: - type: object - description: Update to perform on the attribute. - additionalProperties: false - properties: - _operation: - $ref: '#/components/schemas/builtInOperationType' - value: - type: string - description: >- - Value that corresponds to the operation, for example an `Increment` - or `Decrement` step, or an `Add` or `Remove` value. - required: - - _operation - - value - attributeToUpdate: - oneOf: - - $ref: '#/components/schemas/attribute' - - $ref: '#/components/schemas/builtInOperation' action: type: string enum: @@ -6205,6 +6361,45 @@ components: - delete - clear description: Type of indexing operation. + batchWriteParams: + title: batchWriteParams + description: Batch parameters. + type: object + additionalProperties: false + properties: + requests: + type: array + items: + title: batchRequest + type: object + additionalProperties: false + properties: + action: + $ref: '#/components/schemas/action' + body: + type: object + description: Operation arguments (varies with specified `action`). + example: + name: Betty Jane McCamey + company: Vita Foods Inc. + email: betty@mccamey.com + required: + - action + - body + required: + - requests + example: + requests: + - action: addObject + body: + name: Betty Jane McCamey + company: Vita Foods Inc. + email: betty@mccamey.com + - action: addObject + body: + name: Gayla geimer + company: Ortman McCain Co. + email: gayla@geimer.com objectIDs: type: array items: @@ -6226,7 +6421,6 @@ components: - objectIDs baseIndexSettings: type: object - title: Index settings. additionalProperties: false properties: attributesForFaceting: @@ -6257,7 +6451,7 @@ components: - `filterOnly("ATTRIBUTE")`. - Allows using this attribute as a filter, but doesn't evalue the facet values. + Allows the attribute to be used as a filter but doesn't evaluate the facet values. - `searchable("ATTRIBUTE")`. Allows searching for facet values. @@ -6294,7 +6488,7 @@ components: replicas to this parameter. If you omit a replica from this list, the replica turns into a - regular, standalone index that will no longer by synced with the + regular, standalone index that will no longer be synced with the primary index. @@ -6349,8 +6543,8 @@ components: - wheel - 1X2BCD description: > - Words for which you want to turn off [typo - tolerance](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/). + Creates a list of [words which require exact + matches](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/in-depth/configuring-typo-tolerance/#turn-off-typo-tolerance-for-certain-words). This also turns off [word splitting and concatenation](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/splitting-and-concatenation/) @@ -6421,6 +6615,12 @@ components: Dutch (`nl`), German (`de`), Finnish (`fi`), Danish (`da`), Swedish (`sv`), and Norwegian (`no`). + + Decompounding doesn't work for words with [non-spacing mark Unicode + characters](https://www.charactercodes.net/category/non-spacing_mark). + + For example, `Gartenstühle` won't be decompounded if the `ü` + consists of `u` (U+0075) and `◌̈` (U+0308). default: {} x-categories: - Languages @@ -6492,8 +6692,8 @@ components: For faster indexing, reduce the number of numeric attributes. - If you want to turn off filtering for all numeric attributes, - specifiy an attribute that doesn't exist in your index, such as + To turn off filtering for all numeric attributes, specify an + attribute that doesn't exist in your index, such as `NO_NUMERIC_FILTERING`. @@ -6512,18 +6712,25 @@ components: type: string example: +# description: > - Controls which separators are indexed. + Control which non-alphanumeric characters are indexed. + + + By default, Algolia ignores [non-alphanumeric + characters](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/typo-tolerance/how-to/how-to-search-in-hyphenated-attributes/#handling-non-alphanumeric-characters) + like hyphen (`-`), plus (`+`), and parentheses (`(`,`)`). + + To include such characters, define them with `separatorsToIndex`. Separators are all non-letter characters except spaces and currency characters, such as $€£¥. - By default, separator characters aren't indexed. With `separatorsToIndex`, Algolia treats separator characters as separate words. - For example, a search for `C#` would report two matches. + For example, in a search for "Disney+", Algolia considers "Disney" + and "+" as two separate words. default: '' x-categories: - Typos @@ -6567,8 +6774,8 @@ components: - `unordered("ATTRIBUTE")`. Ignore the position of a match within the attribute. - Without modifier, matches at the beginning of an attribute rank - higer than matches at the end. + Without a modifier, matches at the beginning of an attribute rank + higher than matches at the end. default: [] x-categories: - Attributes @@ -6614,11 +6821,81 @@ components: accurate facet counts. example: url type: string + maxFacetHits: + $ref: '#/components/schemas/maxFacetHits' + keepDiacriticsOnCharacters: + type: string + example: øé + description: | + Characters for which diacritics should be preserved. + + By default, Algolia removes diacritics from letters. + For example, `é` becomes `e`. If this causes issues in your search, + you can specify characters that should keep their diacritics. + default: '' + x-categories: + - Languages + customRanking: + type: array + items: + type: string + example: + - desc(popularity) + - asc(price) + description: > + Attributes to use as [custom + ranking](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/). + + Attribute names are case-sensitive. + + + The custom ranking attributes decide which items are shown first if + the other ranking criteria are equal. + + + Records with missing values for your selected custom ranking + attributes are always sorted last. + + Boolean attributes are sorted based on their alphabetical order. + + + **Modifiers** + + + - `asc("ATTRIBUTE")`. + Sort the index by the values of an attribute, in ascending order. + + - `desc("ATTRIBUTE")`. + Sort the index by the values of an attribute, in descending order. + + If you use two or more custom ranking attributes, + + [reduce the + precision](https://www.algolia.com/doc/guides/managing-results/must-do/custom-ranking/how-to/controlling-custom-ranking-metrics-precision/) + of your first attributes, + + or the other attributes will never be applied. + default: [] + x-categories: + - Ranking indexSettings: description: Index settings. allOf: - $ref: '#/components/schemas/baseIndexSettings' - $ref: '#/components/schemas/indexSettingsAsSearchParams' + WithPrimary: + type: object + additionalProperties: false + properties: + primary: + type: string + description: > + Replica indices only: the name of the primary index for this + replica. + settingsResponse: + allOf: + - $ref: '#/components/schemas/indexSettings' + - $ref: '#/components/schemas/WithPrimary' SynonymType: type: string description: Synonym type. @@ -6629,6 +6906,9 @@ components: - altcorrection1 - altcorrection2 - placeholder + - oneWaySynonym + - altCorrection1 + - altCorrection2 synonymHit: type: object description: Synonym object. @@ -6728,7 +7008,7 @@ components: createdAt: $ref: '#/components/schemas/createdAtTimestamp' required: - - key + - value - createdAt acl: description: Access control list permissions. @@ -6831,7 +7111,7 @@ components: Creating an API key fails if the request is made from an IP address - that's outside the restricted range. + outside the restricted range. example: typoTolerance=strict&restrictSources=192.168.1.0/24 default: '' referers: @@ -6896,12 +7176,12 @@ components: description: | Which part of the search query the pattern should match: - - `startsWith`. The pattern must match the begginning of the query. + - `startsWith`. The pattern must match the beginning of the query. - `endsWith`. The pattern must match the end of the query. - `is`. The pattern must match the query exactly. - `contains`. The pattern must match anywhere in the query. - Empty queries are only allowed as pattern with `anchoring: is`. + Empty queries are only allowed as patterns with `anchoring: is`. enum: - is - startsWith @@ -7106,6 +7386,8 @@ components: required: - position - objectIDs + x-discriminator-fields: + - objectIDs promoteObjectID: title: objectID description: Record to promote. @@ -7119,6 +7401,8 @@ components: required: - position - objectID + x-discriminator-fields: + - objectID promote: oneOf: - $ref: '#/components/schemas/promoteObjectIDs' @@ -7177,6 +7461,7 @@ components: required: - objectID userData: + type: object description: > A JSON object with custom data that will be appended to the `userData` array in the response. @@ -7192,9 +7477,11 @@ components: properties: from: type: integer + format: int64 description: When the rule should start to be active, in Unix epoch time. until: type: integer + format: int64 description: When the rule should stop to be active, in Unix epoch time. required: - from @@ -7240,20 +7527,7 @@ components: $ref: '#/components/schemas/timeRange' required: - objectID - updatedRuleResponse: - type: object - additionalProperties: false - properties: - objectID: - $ref: '#/components/schemas/ruleID' - updatedAt: - $ref: '#/components/schemas/updatedAt' - taskID: - $ref: '#/components/schemas/taskID' - required: - - objectID - - updatedAt - - taskID + - consequence parameters_query: type: string description: Search query for rules. @@ -7261,13 +7535,49 @@ components: parameters_page: type: integer minimum: 0 - description: Requested page of the API response. + description: > + Requested page of the API response. + + + Algolia uses `page` and `hitsPerPage` to control how search results are + displayed + ([paginated](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/js/)). + + + - `hitsPerPage`: sets the number of search results (_hits_) displayed + per page. + + - `page`: specifies the page number of the search results you want to + retrieve. Page numbering starts at 0, so the first page is `page=0`, the + second is `page=1`, and so on. + + + For example, to display 10 results per page starting from the third + page, set `hitsPerPage` to 10 and `page` to 2. parameters_hitsPerPage: type: integer default: 20 minimum: 1 maximum: 1000 - description: Maximum number of hits per page. + description: > + Maximum number of hits per page. + + + Algolia uses `page` and `hitsPerPage` to control how search results are + displayed + ([paginated](https://www.algolia.com/doc/guides/building-search-ui/ui-and-ux-patterns/pagination/js/)). + + + - `hitsPerPage`: sets the number of search results (_hits_) displayed + per page. + + - `page`: specifies the page number of the search results you want to + retrieve. Page numbering starts at 0, so the first page is `page=0`, the + second is `page=1`, and so on. + + + For example, to display 10 results per page starting from the third + page, set `hitsPerPage` to 10 and `page` to 2. dictionaryType: type: string enum: @@ -7287,13 +7597,20 @@ components: - disabled default: enabled description: Whether a dictionary entry is active. + dictionaryEntryType: + type: string + enum: + - custom + - standard + description: >- + Whether a dictionary entry is provided by Algolia (standard), or has + been added by you (custom). dictionaryEntry: type: object description: Dictionary entry. additionalProperties: true required: - objectID - - language properties: objectID: type: string @@ -7328,6 +7645,8 @@ components: type: string state: $ref: '#/components/schemas/dictionaryEntryState' + type: + $ref: '#/components/schemas/dictionaryEntryType' searchDictionaryEntriesResponse: type: object additionalProperties: false @@ -7359,6 +7678,7 @@ components: type: boolean - type: 'null' standardEntries: + type: object description: > Key-value pairs of [supported language ISO codes](https://www.algolia.com/doc/guides/managing-results/optimize-search-results/handling-natural-languages-nlp/in-depth/supported-languages/) @@ -7440,6 +7760,7 @@ components: description: Data size taken by all the users assigned to the cluster. example: 481 source: + type: object description: Source. required: - source @@ -7473,6 +7794,15 @@ components: description: >- Task status, `published` if the task is completed, `notPublished` otherwise. + GetTaskResponse: + title: getTaskResponse + type: object + additionalProperties: false + properties: + status: + $ref: '#/components/schemas/taskStatus' + required: + - status operationType: type: string enum: @@ -7672,6 +8002,155 @@ components: - copyOperationResponse - batchResponses - moveOperationResponse + builtInOperationType: + type: string + enum: + - Increment + - Decrement + - Add + - Remove + - AddUnique + - IncrementFrom + - IncrementSet + description: How to change the attribute. + builtInOperationValue: + oneOf: + - type: string + description: >- + A string to append or remove for the `Add`, `Remove`, and + `AddUnique` operations. + - type: integer + description: A number to add, remove, or append, depending on the operation. + builtInOperation: + type: object + description: Update to perform on the attribute. + additionalProperties: false + properties: + _operation: + $ref: '#/components/schemas/builtInOperationType' + value: + $ref: '#/components/schemas/builtInOperationValue' + required: + - _operation + - value + parameters: + PathInPath: + name: path + in: path + description: Path of the endpoint, anything after "/1" must be specified. + required: true + schema: + type: string + example: /keys + Parameters: + name: parameters + in: query + description: Query parameters to apply to the current query. + schema: + type: object + additionalProperties: true + IndexName: + name: indexName + in: path + description: Name of the index on which to perform the operation. + required: true + schema: + type: string + example: ALGOLIA_INDEX_NAME + ObjectID: + name: objectID + in: path + description: Unique record identifier. + required: true + schema: + $ref: '#/components/schemas/objectID' + ForwardToReplicas: + in: query + name: forwardToReplicas + required: false + description: Whether changes are applied to replica indices. + schema: + type: boolean + parameters_ObjectID: + name: objectID + in: path + description: Unique identifier of a synonym object. + required: true + schema: + type: string + example: synonymID + ReplaceExistingSynonyms: + in: query + name: replaceExistingSynonyms + schema: + type: boolean + description: >- + Whether to replace all synonyms in the index with the ones sent with + this request. + KeyString: + in: path + name: key + required: true + schema: + type: string + example: ALGOLIA_API_KEY + description: API key. + ObjectIDRule: + in: path + name: objectID + description: Unique identifier of a rule object. + required: true + schema: + $ref: '#/components/schemas/ruleID' + ClearExistingRules: + in: query + name: clearExistingRules + required: false + schema: + type: boolean + description: Whether existing rules should be deleted before adding this batch. + DictionaryName: + in: path + name: dictionaryName + description: Dictionary type in which to search. + required: true + schema: + $ref: '#/components/schemas/dictionaryType' + Page: + in: query + name: page + description: | + Requested page of the API response. + If `null`, the API response is not paginated. + required: false + schema: + oneOf: + - type: integer + minimum: 0 + - type: 'null' + default: null + HitsPerPage: + in: query + name: hitsPerPage + description: Number of hits per page. + required: false + schema: + type: integer + default: 100 + UserIDInHeader: + name: X-Algolia-User-ID + description: Unique identifier of the user who makes the search request. + in: header + required: true + schema: + $ref: '#/components/schemas/userID' + UserIDInPath: + name: userID + description: Unique identifier of the user who makes the search request. + in: path + required: true + schema: + $ref: '#/components/schemas/userID' responses: BadRequest: description: Bad request or request arguments. diff --git a/pkg/cmdutil/spec_flags.go b/pkg/cmdutil/spec_flags.go index 5ffe03ed..16fd8a80 100644 --- a/pkg/cmdutil/spec_flags.go +++ b/pkg/cmdutil/spec_flags.go @@ -24,7 +24,6 @@ var BrowseParamsObject = []string{ "attributesToSnippet", "clickAnalytics", "cursor", - "customRanking", "decompoundQuery", "disableExactOnAttributes", "disableTypoToleranceOnAttributes", @@ -45,9 +44,7 @@ var BrowseParamsObject = []string{ "ignorePlurals", "insideBoundingBox", "insidePolygon", - "keepDiacriticsOnCharacters", "length", - "maxFacetHits", "maxValuesPerFacet", "minProximity", "minWordSizefor1Typo", @@ -178,7 +175,6 @@ var SearchParamsObject = []string{ "attributesToRetrieve", "attributesToSnippet", "clickAnalytics", - "customRanking", "decompoundQuery", "disableExactOnAttributes", "disableTypoToleranceOnAttributes", @@ -199,9 +195,7 @@ var SearchParamsObject = []string{ "ignorePlurals", "insideBoundingBox", "insidePolygon", - "keepDiacriticsOnCharacters", "length", - "maxFacetHits", "maxValuesPerFacet", "minProximity", "minWordSizefor1Typo", @@ -251,7 +245,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntaxFeat cmd.Flags().Bool("allowTyposOnNumericTokens", true, heredoc.Doc(`Whether to allow typos on numbers in the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/`)) cmd.Flags().SetAnnotation("allowTyposOnNumericTokens", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered as exact matches. + cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered an exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether to include this query in Algolia's search analytics. @@ -289,11 +283,8 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/attributesToSnippe cmd.Flags().Bool("clickAnalytics", false, heredoc.Doc(`Whether to include a queryID attribute in the response. See: https://www.algolia.com/doc/api-reference/api-parameters/clickAnalytics/`)) cmd.Flags().SetAnnotation("clickAnalytics", "Categories", []string{"Analytics"}) - cmd.Flags().String("cursor", "", heredoc.Doc(`Cursor to get to the next page of the response.`)) - cmd.Flags().StringSlice("customRanking", []string{}, heredoc.Doc(`Attributes to use as custom ranking. -See: https://www.algolia.com/doc/api-reference/api-parameters/customRanking/`)) - cmd.Flags().SetAnnotation("customRanking", "Categories", []string{"Ranking"}) - cmd.Flags().Bool("decompoundQuery", true, heredoc.Doc(`Whether to split compound words into their building blocks. + cmd.Flags().String("cursor", "", heredoc.Doc(`Cursor to get the next page of the response.`)) + cmd.Flags().Bool("decompoundQuery", true, heredoc.Doc(`Whether to split compound words in the query into their building blocks. See: https://www.algolia.com/doc/api-reference/api-parameters/decompoundQuery/`)) cmd.Flags().SetAnnotation("decompoundQuery", "Categories", []string{"Languages"}) cmd.Flags().StringSlice("disableExactOnAttributes", []string{}, heredoc.Doc(`Searchable attributes for which you want to turn off the Exact ranking criterion. @@ -346,21 +337,17 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/highlightPreTag/`) cmd.Flags().Int("hitsPerPage", 20, heredoc.Doc(`Number of hits per page. See: https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/`)) cmd.Flags().SetAnnotation("hitsPerPage", "Categories", []string{"Pagination"}) - ignorePlurals := NewJSONVar([]string{"array", "boolean"}...) + ignorePlurals := NewJSONVar([]string{"array", "string", "boolean"}...) cmd.Flags().Var(ignorePlurals, "ignorePlurals", heredoc.Doc(`Treat singular, plurals, and other forms of declensions as equivalent. See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().SetAnnotation("ignorePlurals", "Categories", []string{"Languages"}) - cmd.Flags().SetAnnotation("insideBoundingBox", "Categories", []string{"Geo-Search"}) + insideBoundingBox := NewJSONVar([]string{"string", "null", "array"}...) + cmd.Flags().Var(insideBoundingBox, "insideBoundingBox", heredoc.Doc(`. +See: https://www.algolia.com/doc/api-reference/api-parameters/insideBoundingBox/`)) cmd.Flags().SetAnnotation("insidePolygon", "Categories", []string{"Geo-Search"}) - cmd.Flags().String("keepDiacriticsOnCharacters", "", heredoc.Doc(`Characters for which diacritics should be preserved. -See: https://www.algolia.com/doc/api-reference/api-parameters/keepDiacriticsOnCharacters/`)) - cmd.Flags().SetAnnotation("keepDiacriticsOnCharacters", "Categories", []string{"Languages"}) - cmd.Flags().Int("length", 0, heredoc.Doc(`If you've specified an offset, this determines the number of hits to retrieve. + cmd.Flags().Int("length", 0, heredoc.Doc(`Number of hits to retrieve (used in combination with offset). See: https://www.algolia.com/doc/api-reference/api-parameters/length/`)) cmd.Flags().SetAnnotation("length", "Categories", []string{"Pagination"}) - cmd.Flags().Int("maxFacetHits", 10, heredoc.Doc(`Maximum number of facet values to return when searching for facet values. -See: https://www.algolia.com/doc/api-reference/api-parameters/maxFacetHits/`)) - cmd.Flags().SetAnnotation("maxFacetHits", "Categories", []string{"Advanced"}) cmd.Flags().Int("maxValuesPerFacet", 100, heredoc.Doc(`Maximum number of facet values to return for each facet. See: https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/`)) cmd.Flags().SetAnnotation("maxValuesPerFacet", "Categories", []string{"Faceting"}) @@ -393,10 +380,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/offset/`)) cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Create filters for ranking purposes. Records that match the filter will rank higher (or lower for a negative filter). See: https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/`)) cmd.Flags().SetAnnotation("optionalFilters", "Categories", []string{"Filtering"}) - cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`If a search doesn't return enough results, you can increase the number of hits by setting these words as optional. + optionalWords := NewJSONVar([]string{"string", "null", "array"}...) + cmd.Flags().Var(optionalWords, "optionalWords", heredoc.Doc(`Words that should be considered optional when found in the query. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) - cmd.Flags().SetAnnotation("optionalWords", "Categories", []string{"Query strategy"}) - cmd.Flags().Int("page", 0, heredoc.Doc(`Requested page of search results. Algolia uses page and hitsPerPage to control how search results are displayed (paginated). + cmd.Flags().Int("page", 0, heredoc.Doc(`Page of search results to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/page/`)) cmd.Flags().SetAnnotation("page", "Categories", []string{"Pagination"}) cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this query in the processing-time percentile computation. @@ -493,7 +480,9 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) - cmd.Flags().SetAnnotation("insideBoundingBox", "Categories", []string{"Geo-Search"}) + insideBoundingBox := NewJSONVar([]string{"string", "null", "array"}...) + cmd.Flags().Var(insideBoundingBox, "insideBoundingBox", heredoc.Doc(`. +See: https://www.algolia.com/doc/api-reference/api-parameters/insideBoundingBox/`)) cmd.Flags().SetAnnotation("insidePolygon", "Categories", []string{"Geo-Search"}) numericFilters := NewJSONVar([]string{"array", "string"}...) cmd.Flags().Var(numericFilters, "numericFilters", heredoc.Doc(`Filter by numeric facets. @@ -518,7 +507,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/allowCompressionOf cmd.Flags().Bool("allowTyposOnNumericTokens", true, heredoc.Doc(`Whether to allow typos on numbers in the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/`)) cmd.Flags().SetAnnotation("allowTyposOnNumericTokens", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered as exact matches. + cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered an exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. @@ -551,7 +540,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/customNormalizatio cmd.Flags().StringSlice("customRanking", []string{}, heredoc.Doc(`Attributes to use as custom ranking. See: https://www.algolia.com/doc/api-reference/api-parameters/customRanking/`)) cmd.Flags().SetAnnotation("customRanking", "Categories", []string{"Ranking"}) - cmd.Flags().Bool("decompoundQuery", true, heredoc.Doc(`Whether to split compound words into their building blocks. + cmd.Flags().Bool("decompoundQuery", true, heredoc.Doc(`Whether to split compound words in the query into their building blocks. See: https://www.algolia.com/doc/api-reference/api-parameters/decompoundQuery/`)) cmd.Flags().SetAnnotation("decompoundQuery", "Categories", []string{"Languages"}) decompoundedAttributes := NewJSONVar([]string{}...) @@ -567,7 +556,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/disablePrefixOnAtt cmd.Flags().StringSlice("disableTypoToleranceOnAttributes", []string{}, heredoc.Doc(`Attributes for which you want to turn off typo tolerance. See: https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleranceOnAttributes/`)) cmd.Flags().SetAnnotation("disableTypoToleranceOnAttributes", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("disableTypoToleranceOnWords", []string{}, heredoc.Doc(`Words for which you want to turn off typo tolerance. + cmd.Flags().StringSlice("disableTypoToleranceOnWords", []string{}, heredoc.Doc(`Creates a list of words which require exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleranceOnWords/`)) cmd.Flags().SetAnnotation("disableTypoToleranceOnWords", "Categories", []string{"Typos"}) distinct := NewJSONVar([]string{"boolean", "integer"}...) @@ -595,7 +584,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/highlightPreTag/`) cmd.Flags().Int("hitsPerPage", 20, heredoc.Doc(`Number of hits per page. See: https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/`)) cmd.Flags().SetAnnotation("hitsPerPage", "Categories", []string{"Pagination"}) - ignorePlurals := NewJSONVar([]string{"array", "boolean"}...) + ignorePlurals := NewJSONVar([]string{"array", "string", "boolean"}...) cmd.Flags().Var(ignorePlurals, "ignorePlurals", heredoc.Doc(`Treat singular, plurals, and other forms of declensions as equivalent. See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().SetAnnotation("ignorePlurals", "Categories", []string{"Languages"}) @@ -626,9 +615,9 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/mode/`)) cmd.Flags().StringSlice("numericAttributesForFiltering", []string{}, heredoc.Doc(`Numeric attributes that can be used as numerical filters. See: https://www.algolia.com/doc/api-reference/api-parameters/numericAttributesForFiltering/`)) cmd.Flags().SetAnnotation("numericAttributesForFiltering", "Categories", []string{"Performance"}) - cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`If a search doesn't return enough results, you can increase the number of hits by setting these words as optional. + optionalWords := NewJSONVar([]string{"string", "null", "array"}...) + cmd.Flags().Var(optionalWords, "optionalWords", heredoc.Doc(`Words that should be considered optional when found in the query. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) - cmd.Flags().SetAnnotation("optionalWords", "Categories", []string{"Query strategy"}) cmd.Flags().Int("paginationLimitedTo", 1000, heredoc.Doc(`Maximum number of search results that can be obtained through pagination. See: https://www.algolia.com/doc/api-reference/api-parameters/paginationLimitedTo/`)) cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. @@ -673,7 +662,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/searchableAttribut cmd.Flags().SetAnnotation("searchableAttributes", "Categories", []string{"Attributes"}) semanticSearch := NewJSONVar([]string{}...) cmd.Flags().Var(semanticSearch, "semanticSearch", heredoc.Doc(`Settings for the semantic search part of NeuralSearch.`)) - cmd.Flags().String("separatorsToIndex", "", heredoc.Doc(`Controls which separators are indexed. + cmd.Flags().String("separatorsToIndex", "", heredoc.Doc(`Control which non-alphanumeric characters are indexed. See: https://www.algolia.com/doc/api-reference/api-parameters/separatorsToIndex/`)) cmd.Flags().SetAnnotation("separatorsToIndex", "Categories", []string{"Typos"}) cmd.Flags().String("snippetEllipsisText", "…", heredoc.Doc(`String used as an ellipsis indicator when a snippet is truncated. @@ -705,7 +694,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/advancedSyntaxFeat cmd.Flags().Bool("allowTyposOnNumericTokens", true, heredoc.Doc(`Whether to allow typos on numbers in the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumericTokens/`)) cmd.Flags().SetAnnotation("allowTyposOnNumericTokens", "Categories", []string{"Typos"}) - cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered as exact matches. + cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered an exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether to include this query in Algolia's search analytics. @@ -743,10 +732,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/attributesToSnippe cmd.Flags().Bool("clickAnalytics", false, heredoc.Doc(`Whether to include a queryID attribute in the response. See: https://www.algolia.com/doc/api-reference/api-parameters/clickAnalytics/`)) cmd.Flags().SetAnnotation("clickAnalytics", "Categories", []string{"Analytics"}) - cmd.Flags().StringSlice("customRanking", []string{}, heredoc.Doc(`Attributes to use as custom ranking. -See: https://www.algolia.com/doc/api-reference/api-parameters/customRanking/`)) - cmd.Flags().SetAnnotation("customRanking", "Categories", []string{"Ranking"}) - cmd.Flags().Bool("decompoundQuery", true, heredoc.Doc(`Whether to split compound words into their building blocks. + cmd.Flags().Bool("decompoundQuery", true, heredoc.Doc(`Whether to split compound words in the query into their building blocks. See: https://www.algolia.com/doc/api-reference/api-parameters/decompoundQuery/`)) cmd.Flags().SetAnnotation("decompoundQuery", "Categories", []string{"Languages"}) cmd.Flags().StringSlice("disableExactOnAttributes", []string{}, heredoc.Doc(`Searchable attributes for which you want to turn off the Exact ranking criterion. @@ -799,21 +785,17 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/highlightPreTag/`) cmd.Flags().Int("hitsPerPage", 20, heredoc.Doc(`Number of hits per page. See: https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/`)) cmd.Flags().SetAnnotation("hitsPerPage", "Categories", []string{"Pagination"}) - ignorePlurals := NewJSONVar([]string{"array", "boolean"}...) + ignorePlurals := NewJSONVar([]string{"array", "string", "boolean"}...) cmd.Flags().Var(ignorePlurals, "ignorePlurals", heredoc.Doc(`Treat singular, plurals, and other forms of declensions as equivalent. See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().SetAnnotation("ignorePlurals", "Categories", []string{"Languages"}) - cmd.Flags().SetAnnotation("insideBoundingBox", "Categories", []string{"Geo-Search"}) + insideBoundingBox := NewJSONVar([]string{"string", "null", "array"}...) + cmd.Flags().Var(insideBoundingBox, "insideBoundingBox", heredoc.Doc(`. +See: https://www.algolia.com/doc/api-reference/api-parameters/insideBoundingBox/`)) cmd.Flags().SetAnnotation("insidePolygon", "Categories", []string{"Geo-Search"}) - cmd.Flags().String("keepDiacriticsOnCharacters", "", heredoc.Doc(`Characters for which diacritics should be preserved. -See: https://www.algolia.com/doc/api-reference/api-parameters/keepDiacriticsOnCharacters/`)) - cmd.Flags().SetAnnotation("keepDiacriticsOnCharacters", "Categories", []string{"Languages"}) - cmd.Flags().Int("length", 0, heredoc.Doc(`If you've specified an offset, this determines the number of hits to retrieve. + cmd.Flags().Int("length", 0, heredoc.Doc(`Number of hits to retrieve (used in combination with offset). See: https://www.algolia.com/doc/api-reference/api-parameters/length/`)) cmd.Flags().SetAnnotation("length", "Categories", []string{"Pagination"}) - cmd.Flags().Int("maxFacetHits", 10, heredoc.Doc(`Maximum number of facet values to return when searching for facet values. -See: https://www.algolia.com/doc/api-reference/api-parameters/maxFacetHits/`)) - cmd.Flags().SetAnnotation("maxFacetHits", "Categories", []string{"Advanced"}) cmd.Flags().Int("maxValuesPerFacet", 100, heredoc.Doc(`Maximum number of facet values to return for each facet. See: https://www.algolia.com/doc/api-reference/api-parameters/maxValuesPerFacet/`)) cmd.Flags().SetAnnotation("maxValuesPerFacet", "Categories", []string{"Faceting"}) @@ -846,10 +828,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/offset/`)) cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Create filters for ranking purposes. Records that match the filter will rank higher (or lower for a negative filter). See: https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/`)) cmd.Flags().SetAnnotation("optionalFilters", "Categories", []string{"Filtering"}) - cmd.Flags().StringSlice("optionalWords", []string{}, heredoc.Doc(`If a search doesn't return enough results, you can increase the number of hits by setting these words as optional. + optionalWords := NewJSONVar([]string{"string", "null", "array"}...) + cmd.Flags().Var(optionalWords, "optionalWords", heredoc.Doc(`Words that should be considered optional when found in the query. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) - cmd.Flags().SetAnnotation("optionalWords", "Categories", []string{"Query strategy"}) - cmd.Flags().Int("page", 0, heredoc.Doc(`Requested page of search results. Algolia uses page and hitsPerPage to control how search results are displayed (paginated). + cmd.Flags().Int("page", 0, heredoc.Doc(`Page of search results to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/page/`)) cmd.Flags().SetAnnotation("page", "Categories", []string{"Pagination"}) cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this query in the processing-time percentile computation. From c96d3e07500c326895aae4acc53c4d249ee4680e Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Tue, 18 Mar 2025 14:40:21 +0100 Subject: [PATCH 11/12] feat: update CLI code base to use latest major version of the Go API client (#178) --- .envrc | 9 + .github/workflows/api-specs-pr.yml | 16 +- .github/workflows/go.yml | 11 +- .github/workflows/golangci-lint.yml | 30 +- .github/workflows/releases.yml | 18 +- .gitignore | 4 +- .golangci.yml | 5 + Dockerfile | 19 +- README.md | 60 +- Taskfile.yml | 114 ++++ api/crawler/client.go | 23 +- api/crawler/types.go | 6 +- api/insights/client.go | 108 +-- api/insights/types.go | 1 + api/insights/utils.go | 19 - cmd/docs/main.go | 10 +- devbox.json | 12 + devbox.lock | 420 ++++++++++++ go.mod | 97 +-- go.sum | 623 ++++-------------- internal/analyze/analyze.go | 40 +- internal/docs/docs.go | 1 - internal/docs/yaml.go | 4 +- internal/update/update.go | 9 +- pkg/ask/ask.go | 45 +- pkg/auth/auth_check.go | 56 +- pkg/auth/auth_check_test.go | 33 +- pkg/cmd/apikeys/create/create.go | 60 +- pkg/cmd/apikeys/create/create_test.go | 7 +- pkg/cmd/apikeys/delete/delete.go | 27 +- pkg/cmd/apikeys/delete/delete_test.go | 12 +- pkg/cmd/apikeys/get/get.go | 23 +- pkg/cmd/apikeys/get/get_test.go | 15 +- pkg/cmd/apikeys/list/list.go | 66 +- pkg/cmd/apikeys/list/list_test.go | 30 +- pkg/cmd/apikeys/shared/shared.go | 21 - pkg/cmd/art/art.go | 60 -- pkg/cmd/crawler/crawl/crawl.go | 16 +- pkg/cmd/crawler/crawl/crawl_test.go | 5 +- pkg/cmd/crawler/create/create.go | 18 +- pkg/cmd/crawler/create/create_test.go | 4 +- pkg/cmd/crawler/get/get.go | 3 +- pkg/cmd/crawler/list/list.go | 7 +- pkg/cmd/crawler/pause/pause.go | 11 +- pkg/cmd/crawler/reindex/reindex.go | 14 +- pkg/cmd/crawler/run/run.go | 7 +- pkg/cmd/crawler/test/test.go | 7 +- pkg/cmd/crawler/unblock/unblock.go | 13 +- pkg/cmd/dictionary/dictionary.go | 2 +- pkg/cmd/dictionary/entries/browse/browse.go | 68 +- .../dictionary/entries/browse/browse_test.go | 65 +- pkg/cmd/dictionary/entries/clear/clear.go | 82 ++- .../dictionary/entries/clear/clear_test.go | 80 +-- pkg/cmd/dictionary/entries/delete/delete.go | 58 +- .../dictionary/entries/delete/delete_test.go | 55 +- pkg/cmd/dictionary/entries/import/import.go | 138 ++-- .../dictionary/entries/import/import_test.go | 81 +-- pkg/cmd/dictionary/settings/get/get.go | 4 +- pkg/cmd/dictionary/settings/set/languages.go | 1 + pkg/cmd/dictionary/settings/set/set.go | 74 ++- pkg/cmd/dictionary/shared/constants.go | 43 +- pkg/cmd/events/tail/tail.go | 50 +- pkg/cmd/factory/default.go | 70 +- pkg/cmd/indices/analyze/analyze.go | 127 ++-- pkg/cmd/indices/clear/clear.go | 35 +- pkg/cmd/indices/clear/clear_test.go | 7 +- pkg/cmd/indices/config/config.go | 2 +- pkg/cmd/indices/config/export/export.go | 50 +- pkg/cmd/indices/config/import/confirm.go | 23 +- pkg/cmd/indices/config/import/confirm_test.go | 6 +- pkg/cmd/indices/config/import/import.go | 84 ++- pkg/cmd/indices/config/import/synonyms.go | 59 -- pkg/cmd/indices/copy/copy.go | 68 +- pkg/cmd/indices/copy/copy_test.go | 48 +- pkg/cmd/indices/delete/delete.go | 256 +++---- pkg/cmd/indices/delete/delete_test.go | 63 +- pkg/cmd/indices/list/list.go | 60 +- pkg/cmd/indices/move/move.go | 37 +- pkg/cmd/indices/move/move_test.go | 7 +- pkg/cmd/objects/browse/browse.go | 67 +- pkg/cmd/objects/browse/browse_test.go | 17 +- pkg/cmd/objects/delete/delete.go | 161 ++--- pkg/cmd/objects/delete/delete_test.go | 73 +- pkg/cmd/objects/import/import.go | 104 +-- pkg/cmd/objects/import/import_test.go | 29 +- pkg/cmd/objects/objects.go | 5 +- pkg/cmd/objects/operations/operations.go | 109 ++- pkg/cmd/objects/operations/operations_test.go | 40 +- pkg/cmd/objects/update/object.go | 81 --- pkg/cmd/objects/update/object_test.go | 125 ---- pkg/cmd/objects/update/update.go | 109 ++- pkg/cmd/objects/update/update_test.go | 9 +- pkg/cmd/open/open.go | 49 +- pkg/cmd/profile/add/add.go | 54 +- pkg/cmd/profile/list/list.go | 16 +- pkg/cmd/profile/remove/remove.go | 12 +- pkg/cmd/profile/remove/remove_test.go | 31 +- pkg/cmd/root/help.go | 40 +- pkg/cmd/root/root.go | 41 +- pkg/cmd/root/{root_help.go => root_test.go} | 2 +- pkg/cmd/rules/browse/browse.go | 44 +- pkg/cmd/rules/browse/browse_test.go | 21 +- pkg/cmd/rules/delete/delete.go | 62 +- pkg/cmd/rules/delete/delete_test.go | 42 +- pkg/cmd/rules/import/import.go | 114 +++- pkg/cmd/rules/import/import_test.go | 37 +- pkg/cmd/search/search.go | 46 +- pkg/cmd/settings/get/list.go | 6 +- pkg/cmd/settings/import/import.go | 35 +- pkg/cmd/settings/import/import_test.go | 10 +- pkg/cmd/settings/set/set.go | 41 +- pkg/cmd/settings/set/set_test.go | 7 +- pkg/cmd/shared/config/config.go | 112 ++-- pkg/cmd/shared/handler/indices/config.go | 82 ++- pkg/cmd/shared/handler/indices/config_test.go | 22 +- pkg/cmd/shared/handler/synonyms/synonyms.go | 45 +- .../shared/handler/synonyms/synonyms_test.go | 9 +- pkg/cmd/synonyms/browse/browse.go | 42 +- pkg/cmd/synonyms/browse/browse_test.go | 33 +- pkg/cmd/synonyms/delete/delete.go | 63 +- pkg/cmd/synonyms/delete/delete_test.go | 45 +- pkg/cmd/synonyms/import/import.go | 182 ++--- pkg/cmd/synonyms/import/import_test.go | 229 ++++++- pkg/cmd/synonyms/save/messages.go | 38 +- pkg/cmd/synonyms/save/messages_test.go | 57 +- pkg/cmd/synonyms/save/save.go | 69 +- pkg/cmd/synonyms/save/save_test.go | 83 ++- pkg/cmd/synonyms/shared/flags_to_synonym.go | 96 +-- .../synonyms/shared/flags_to_synonym_test.go | 34 +- pkg/cmdutil/factory.go | 4 +- pkg/cmdutil/file_input.go | 5 +- pkg/cmdutil/flags_completion.go | 38 +- pkg/cmdutil/flags_completion_test.go | 7 +- pkg/cmdutil/json_flags.go | 5 +- pkg/cmdutil/jsonpath_flags.go | 15 +- pkg/cmdutil/print_flags.go | 18 +- pkg/cmdutil/spec_flags.go | 112 ++-- pkg/cmdutil/usage.go | 52 +- pkg/cmdutil/valid_args.go | 12 +- pkg/config/config.go | 4 +- pkg/config/profile.go | 6 +- pkg/gen/gen_flags.go | 2 +- pkg/httpmock/registry.go | 7 +- pkg/httpmock/stub.go | 6 +- pkg/iostreams/iostreams.go | 12 +- pkg/iostreams/tty_size.go | 3 +- pkg/iostreams/tty_size_windows.go | 1 + pkg/open/open.go | 1 - pkg/printers/table_printer.go | 6 +- pkg/printers/template.go | 6 +- pkg/telemetry/telemetry.go | 14 +- pkg/telemetry/telemetry_test.go | 4 +- pkg/text/truncate.go | 8 + pkg/utils/utils.go | 8 +- pkg/validators/cmd.go | 9 +- staticcheck.conf | 1 - test/helpers.go | 23 +- 157 files changed, 4265 insertions(+), 3026 deletions(-) create mode 100644 .envrc create mode 100644 .golangci.yml create mode 100644 Taskfile.yml delete mode 100644 api/insights/utils.go create mode 100644 devbox.json create mode 100644 devbox.lock delete mode 100644 pkg/cmd/apikeys/shared/shared.go delete mode 100644 pkg/cmd/art/art.go delete mode 100644 pkg/cmd/indices/config/import/synonyms.go delete mode 100644 pkg/cmd/objects/update/object.go delete mode 100644 pkg/cmd/objects/update/object_test.go rename pkg/cmd/root/{root_help.go => root_test.go} (97%) delete mode 100644 staticcheck.conf diff --git a/.envrc b/.envrc new file mode 100644 index 00000000..2f05af98 --- /dev/null +++ b/.envrc @@ -0,0 +1,9 @@ +#!/bin/bash + +# Automatically sets up your devbox environment whenever you cd into this +# directory via our direnv integration: + +eval "$(devbox generate direnv --print-envrc)" + +# check out https://www.jetpack.io/devbox/docs/ide_configuration/direnv/ +# for more details diff --git a/.github/workflows/api-specs-pr.yml b/.github/workflows/api-specs-pr.yml index 442f4637..46a00633 100644 --- a/.github/workflows/api-specs-pr.yml +++ b/.github/workflows/api-specs-pr.yml @@ -2,19 +2,19 @@ name: Scheduled API Specs Pull Request on: schedule: - cron: '0 */12 * * *' - jobs: api-specs-pr: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.19 - - run: | - git config --global user.name "algolia-ci" - git config --global user.email "noreply@algolia.com" + go-version: 1.23 - run: make api-specs-pr env: - GH_TOKEN: ${{ secrets.GH_SECRET }} \ No newline at end of file + GH_TOKEN: ${{ secrets.GH_SECRET }} + GIT_COMMITTER_NAME: algolia-ci + GIT_AUTHOR_NAME: algolia-ci + GIT_COMMITTER_EMAIL: noreply@algolia.com + GIT_AUTHOR_EMAIL: noreply@algolia.com diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index 9c40c6f8..be2eb8c1 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -1,24 +1,19 @@ name: Go - on: push: branches: [main] pull_request: branches: [main] - jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 - + - uses: actions/checkout@v4 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.19 - + go-version: 1.23 - name: Build run: go build -v ./... - - name: Test run: go test -v ./... diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml index 20de0822..5a99bcc3 100644 --- a/.github/workflows/golangci-lint.yml +++ b/.github/workflows/golangci-lint.yml @@ -14,31 +14,11 @@ jobs: name: lint runs-on: ubuntu-latest steps: - - uses: actions/setup-go@v3 + - uses: actions/setup-go@v5 with: - go-version: 1.18 - - uses: actions/checkout@v3 + go-version: 1.23 + - uses: actions/checkout@v4 - name: golangci-lint - uses: golangci/golangci-lint-action@v3 + uses: golangci/golangci-lint-action@v6 with: - # Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version - version: v1.47.3 - - # Optional: working directory, useful for monorepos - # working-directory: somedir - - # Optional: golangci-lint command line arguments. - # args: --issues-exit-code=0 - - # Optional: show only new issues if it's a pull request. The default value is `false`. - # only-new-issues: true - - # Optional: if set to true then the all caching functionality will be complete disabled, - # takes precedence over all other caching options. - # skip-cache: true - - # Optional: if set to true then the action don't cache or restore ~/go/pkg. - # skip-pkg-cache: true - - # Optional: if set to true then the action don't cache or restore ~/.cache/go-build. - # skip-build-cache: true \ No newline at end of file + version: v1.63.4 diff --git a/.github/workflows/releases.yml b/.github/workflows/releases.yml index 2745a68b..dfee9b52 100644 --- a/.github/workflows/releases.yml +++ b/.github/workflows/releases.yml @@ -1,25 +1,22 @@ name: goreleaser - on: push: tags: - "v*" - permissions: - contents: write # publishing releases - + contents: write # publishing releases jobs: release: runs-on: ubuntu-latest steps: - name: Code checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Go - uses: actions/setup-go@v2 + uses: actions/setup-go@v5 with: - go-version: 1.19 + go-version: 1.23 - name: Install chocolatey run: | mkdir -p /opt/chocolatey @@ -30,15 +27,15 @@ jobs: env: CHOCOLATEY_VERSION: 1.2.0 - name: Run GoReleaser - uses: goreleaser/goreleaser-action@v2.8.0 + uses: goreleaser/goreleaser-action@v6 with: - version: "~1.13.1" + version: "~> v2" args: release env: GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} CHOCOLATEY_API_KEY: ${{ secrets.CHOCOLATEY_API_KEY }} - name: Docs checkout - uses: actions/checkout@v3 + uses: actions/checkout@v4 with: repository: algolia/doc path: docs @@ -53,4 +50,3 @@ jobs: GITHUB_TOKEN: ${{ secrets.GORELEASER_GITHUB_TOKEN }} run: | make docs-pr - make VARIATON=new docs-pr diff --git a/.gitignore b/.gitignore index 1f042521..cb9a173e 100644 --- a/.gitignore +++ b/.gitignore @@ -18,5 +18,5 @@ vendor/ # local build algolia -# Ignore docs directory -docs/ \ No newline at end of file +# Environment variables +*.env \ No newline at end of file diff --git a/.golangci.yml b/.golangci.yml new file mode 100644 index 00000000..6beda9ee --- /dev/null +++ b/.golangci.yml @@ -0,0 +1,5 @@ +linters: + enable: + - gosec + - gofumpt + - stylecheck diff --git a/Dockerfile b/Dockerfile index 872fd2aa..cd949e09 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,14 @@ -FROM alpine -RUN apk update && apk upgrade && \ - apk add --no-cache ca-certificates -COPY algolia /bin/algolia -ENTRYPOINT ["/bin/algolia"] \ No newline at end of file +# Build the binary +FROM golang:1.23-alpine AS builder +WORKDIR /app +COPY . . +ARG VERSION=docker +RUN apk update && apk add --no-cache curl +RUN go mod download +RUN go install github.com/go-task/task/v3/cmd/task@latest +RUN task download-spec-file && VERSION=${VERSION} task build + +FROM alpine:3 +RUN apk update && apk upgrade && apk add --no-cache ca-certificates +COPY --from=builder /app/algolia /bin/algolia +ENTRYPOINT ["/bin/algolia"] diff --git a/README.md b/README.md index d639cd66..9c9e31ae 100644 --- a/README.md +++ b/README.md @@ -1,49 +1,69 @@ # Algolia CLI -A command line interface to enable Algolia developers to interact with and configure their Algolia applications straight from a command line or terminal window. Automate common workloads, create snapshots, revert to backups, or quickly modify applications as needed! This is a lightweight tool, providing a text-only interface, that is easy to install and use! +The Algolia CLI lets you work with your Algolia resources, +such as indices, records, API keys, and synonyms, +and from the command line. ![cli](https://user-images.githubusercontent.com/5702266/153008646-1fd8fbf2-4a4d-4421-b2f2-0886487f3e27.png) ## Documentation -See the [documentation](https://algolia.com/doc/tools/cli/) for setup and usage instructions. +See [Algolia CLI](https://algolia.com/doc/tools/cli/) in the Algolia documentation for setup and usage instructions. ## Installation -### Build from Source +### macOS -A `Makefile` is available to help installing and building the CLI. +The Algolia CLI is available on [Homebrew](https://brew.sh/) and as a downloadable binary from the [releases page](https://github.com/algolia/cli/releases). -```bash -git clone git@github.com:algolia/cli.git && make install +```sh +brew install algolia/algolia-cli/algolia ``` -### MacOS +### Linux -`algolia` is available on Homebrew and as a downloadable binary from the [releases page](https://github.com/algolia/cli/releases). +The Algolia CLI is available as a `.deb` package: -```bash -brew tap algolia/algolia-cli && brew install algolia +```sh +# Select the package appropriate for your platform: +sudo dpkg -i algolia_*.deb +``` + +as a `.rpm` package: + +```sh +# Select the package appropriate for your platform +sudo rpm -i algolia_*.rpm +``` + +or as a tarball from the [releases page](https://github.com/algolia/cli/releases): + +```sh +# Select the archive appropriate for your platform +tar xvf algolia_*_linux_*.tar.gz ``` ### Windows -`algolia` is available via [Chocolatey](https://community.chocolatey.org/packages/algolia/) and as downloadable .exe files. +The Algolia CLI is available via [Chocolatey](https://community.chocolatey.org/packages/algolia/) and as a downloadable binary from the [releases page](https://github.com/algolia/cli/releases) -#### Chocolatey +### Community packages -| Install: | Upgrade: | -| ------------------ | ------------------ | -| `choco install algolia` | `choco upgrade algolia` | +Other packages are maintained by the community, not by Algolia. +If you distribute a package for the Algolia CLI, create a pull request so that we can list it here! -#### Executable +### Build from source -Executable files are available on the [releases page](https://github.com/algolia/cli/releases). +To build the Algolia CLI from source, you'll need: -### Other platforms +- Go version 1.23 or later +- [Go task](https://taskfile.dev/) -Download packaged binaries from the [releases page](https://github.com/algolia/cli/releases). +1. Clone the repo: `git clone https://github.com/kai687/cli.git algolia-cli && cd algolia-cli` +1. Run: `task build` ## Support -Found a bug on the CLI? [Open a new issue](https://github.com/algolia/cli/issues/new) or [contact the support](https://www.algolia.com/support/) to get help with Algolia! +If you found an issue with the Algolia CLI, +[open a new GitHub issue](https://github.com/algolia/cli/issues/new), +or join the Algolia community on [Discord](https://alg.li/discord). diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 00000000..ff07aa38 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,114 @@ +version: 3 +output: prefixed +vars: + # The URL to the "old" Algolia docs repo + docs_remote: https://github.com/algolia/doc.git + # The temporary folder for the generated YML files + docs_local: docs + cli_ref_path: app_data/cli/commands + yml_folder: tmp +tasks: + build: + desc: Build the binary + deps: [generate] + cmd: go build -ldflags "-s -w -X=github.com/algolia/cli/pkg/version.Version={{ .VERSION }}" -o algolia cmd/algolia/main.go + vars: + VERSION: '{{ .VERSION | default "main" }}' + test: + desc: Run unit tests + run: always + cmd: go test ./... + lint: + desc: Lint code + cmd: golangci-lint run + format: + desc: Format code + cmds: + - gofumpt -w pkg cmd test internal api + - golines -w pkg cmd test internal api + ci: + desc: Test, lint, and format + aliases: + - default + deps: + - build + - test + - lint + - format + api-specs-pr: + desc: Update the flags for search and settings from the latest Search API spec + summary: | + This task downloads the latest Search API OpenAPI spec from the api-clients-automation repo, + generates the flags, and makes a new PR to the CLI GitHub repo. + deps: [download-spec-file, generate] + preconditions: + - git status --porcelain + cmds: + - | + original="$(git branch --show-current)" + git checkout -B {{ .branch }} + git add . + git commit --message "chore: update search api spec" + git push --force --set-upstream origin {{ .branch }} + gh pr list --base main --head {{ .branch }} | grep -q . || gh pr create --title '{{ .pr-title }}' --description '{{ .pr-description }}' + git switch "${original}" + vars: + branch: feat/api-specs + pr-title: "chore: Update Search API spec" + pr-description: "Update Search API spec" + env: + GIT_COMMITTER_NAME: algolia-ci + GIT_AUTHOR_NAME: algolia-ci + GIT_COMMITTER_EMAIL: noreply@algolia.com + GIT_AUTHOR_EMAIL: noreply@algolia.com + download-spec-file: + desc: Download the latest Search API spec from GitHub + cmd: curl -fsSL -o {{ .destination }} {{ .source }} + vars: + source: https://raw.githubusercontent.com/algolia/api-clients-automation/main/specs/bundled/search.yml + destination: ./api/specs/search.yml + generate: + desc: Generate command flags + internal: true + cmds: + - go generate ./... + update-docs: + desc: Update the CLI command reference in the Algolia docs + deps: + - clone-docs + - generate-command-reference + cmds: + - task: update-command-reference + - task: cleanup + clone-docs: + desc: Clone the Algolia docs + internal: true + cmd: git clone --depth=1 {{ .docs_remote }} {{ .docs_local }} + generate-command-reference: + desc: Generate updated YML files for the CLI command reference + internal: true + cmd: go run ./cmd/docs --app_data-path {{ .yml_folder }} + update-command-reference: + desc: Add the updated YML files to the docs + summary: | + This task clones the Algolia docs repo, + adds the updated CLI reference yml files to it, + and pushes a new PR to the GitHub repo. + internal: true + cmds: + - | + git -C {{ .docs_local }} checkout -B chore/cli-$(git rev-parse --short HEAD) + git -C {{ .docs_local }} rm "{{ .cli_ref_path }}/*.yml" + mkdir -p {{ .docs_local }}/{{ .cli_ref_path }} + mv {{ .yml_folder }}/*.yml {{ .docs_local }}/{{ .cli_ref_path }}/ + git -C {{ .docs_local }} add "{{ .cli_ref_path }}/*.yml" + git -C {{ .docs_local }} commit --message 'chore: Update CLI command reference' + env: + GIT_COMMITTER_NAME: algolia-ci + GIT_AUTHOR_NAME: algolia-ci + GIT_COMMITTER_EMAIL: noreply@algolia.com + GIT_AUTHOR_EMAIL: noreply@algolia.com + cleanup: + desc: Cleanup the docs files + internal: true + cmd: rm -rf {{ .docs_local }} {{ .yml_folder }} || true diff --git a/api/crawler/client.go b/api/crawler/client.go index 8ac9a101..44924c54 100644 --- a/api/crawler/client.go +++ b/api/crawler/client.go @@ -6,7 +6,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "net/http" ) @@ -43,7 +42,13 @@ func NewClientWithHTTPClient(userID, apiKey string, client *http.Client) *Client // Request sends an HTTP request and returns an HTTP response. // It unmarshals the response body to the given interface. -func (c *Client) request(res interface{}, method string, path string, body interface{}, urlParams map[string]string) error { +func (c *Client) request( + res interface{}, + method string, + path string, + body interface{}, + urlParams map[string]string, +) error { r, err := c.buildRequest(method, path, body, urlParams) if err != nil { return err @@ -99,14 +104,18 @@ func (c *Client) buildRequestWithBody(method, url string, body interface{}) (*ht return nil, err } - r = ioutil.NopCloser(bytes.NewReader(b)) + r = io.NopCloser(bytes.NewReader(b)) } return http.NewRequest(method, url, r) } // buildRequest builds an HTTP request. -func (c *Client) buildRequest(method, path string, body interface{}, urlParams map[string]string) (req *http.Request, err error) { +func (c *Client) buildRequest( + method, path string, + body interface{}, + urlParams map[string]string, +) (req *http.Request, err error) { url := DefaultBaseURL + path if body == nil { @@ -275,7 +284,11 @@ func (c *Client) Stats(crawlerID string) (*StatsResponse, error) { // CrawlURLs crawls the specified URLs on the specified Crawler. // It returns the Task ID if successful. -func (c *Client) CrawlURLs(crawlerID string, URLs []string, save, saveSpecified bool) (string, error) { +func (c *Client) CrawlURLs( + crawlerID string, + URLs []string, + save, saveSpecified bool, +) (string, error) { var res TaskIDResponse path := fmt.Sprintf("crawlers/%s/urls/crawl", crawlerID) diff --git a/api/crawler/types.go b/api/crawler/types.go index 415b65c1..a55d1e60 100644 --- a/api/crawler/types.go +++ b/api/crawler/types.go @@ -3,7 +3,7 @@ package crawler import ( "time" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" ) // ErrResponse is a Crawler API error response. @@ -67,8 +67,8 @@ type Config struct { IgnoreNoFollowTo bool `json:"ignoreNoFollowTo,omitempty"` IgnoreCanonicalTo bool `json:"ignoreCanonicalTo,omitempty"` - SaveBackup bool `json:"saveBackup,omitempty"` - InitialIndexSettings map[string]*search.Settings `json:"initialIndexSettings,omitempty"` + SaveBackup bool `json:"saveBackup,omitempty"` + InitialIndexSettings map[string]*search.IndexSettings `json:"initialIndexSettings,omitempty"` Actions []*Action `json:"actions,omitempty"` } diff --git a/api/insights/client.go b/api/insights/client.go index 1b7446b8..f74f48b9 100644 --- a/api/insights/client.go +++ b/api/insights/client.go @@ -1,66 +1,80 @@ package insights import ( + "encoding/json" "fmt" - "net/http" "time" - "github.com/algolia/algoliasearch-client-go/v3/algolia/call" - "github.com/algolia/algoliasearch-client-go/v3/algolia/compression" - _insights "github.com/algolia/algoliasearch-client-go/v3/algolia/insights" - "github.com/algolia/algoliasearch-client-go/v3/algolia/transport" + algoliaInsights "github.com/algolia/algoliasearch-client-go/v4/algolia/insights" + "github.com/algolia/algoliasearch-client-go/v4/algolia/transport" + "github.com/algolia/cli/pkg/version" ) -// Client provides methods to interact with the Algolia Insights API. +// Client wraps the default Insights API client so that we can declare methods on it type Client struct { - appID string - transport *transport.Transport + *algoliaInsights.APIClient } -// NewClient instantiates a new client able to interact with the Algolia -// Insights API. -func NewClient(appID, apiKey string) *Client { - return NewClientWithConfig( - _insights.Configuration{ - AppID: appID, - APIKey: apiKey, +// NewClient instantiates a new Insights API client +func NewClient(appID, apiKey string, region algoliaInsights.Region) (*Client, error) { + // Get the default user agent + userAgent, err := getUserAgentInfo(appID, apiKey, region, version.Version) + if err != nil { + return nil, err + } + if userAgent == "" { + return nil, fmt.Errorf("user agent info must not be empty") + } + clientConfig := algoliaInsights.InsightsConfiguration{ + Configuration: transport.Configuration{ + AppID: appID, + ApiKey: apiKey, + UserAgent: userAgent, }, - ) + } + client, err := algoliaInsights.NewClientWithConfig(clientConfig) + if err != nil { + return nil, err + } + return &Client{client}, nil } -// NewClientWithConfig instantiates a new client able to interact with the -// Algolia Insights API. -func NewClientWithConfig(config _insights.Configuration) *Client { - var hosts []*transport.StatefulHost - - if config.Hosts == nil { - hosts = defaultHosts(config.Region) - } else { - for _, h := range config.Hosts { - hosts = append(hosts, transport.NewStatefulHost(h, call.IsReadWrite)) - } +// GetEvents retrieves a number of events from the Algolia Insights API. +func (c *Client) GetEvents(startDate, endDate time.Time, limit int) (*EventsRes, error) { + layout := "2006-01-02T15:04:05.000Z" + params := map[string]any{ + "startDate": startDate.Format(layout), + "endDate": endDate.Format(layout), + "limit": limit, } - - return &Client{ - appID: config.AppID, - transport: transport.New( - hosts, - config.Requester, - config.AppID, - config.APIKey, - config.ReadTimeout, - config.WriteTimeout, - config.Headers, - config.ExtraUserAgent, - compression.None, - ), + res, err := c.CustomGet(c.NewApiCustomGetRequest("1/events").WithParameters(params)) + if err != nil { + return nil, err } + tmp, err := json.Marshal(res) + if err != nil { + return nil, err + } + var eventsRes EventsRes + err = json.Unmarshal(tmp, &eventsRes) + if err != nil { + return nil, err + } + + return &eventsRes, err } -// FetchEvents retrieves events from the Algolia Insights API. -func (c *Client) FetchEvents(startDate, endDate time.Time, limit int) (EventsRes, error) { - var res EventsRes - path := fmt.Sprintf("/1/events?startDate=%s&endDate=%s&limit=%d", startDate.Format("2006-01-02T15:04:05.000Z"), endDate.Format("2006-01-02T15:04:05.000Z"), limit) - err := c.transport.Request(&res, http.MethodGet, path, nil, call.Read, nil) - return res, err +// getUserAgentInfo returns the user agent string for the Insights client in the CLI +func getUserAgentInfo( + appID string, + apiKey string, + region algoliaInsights.Region, + appVersion string, +) (string, error) { + client, err := algoliaInsights.NewClient(appID, apiKey, region) + if err != nil { + return "", err + } + + return client.GetConfiguration().UserAgent + fmt.Sprintf("; Algolia CLI (%s)", appVersion), nil } diff --git a/api/insights/types.go b/api/insights/types.go index 58b06120..0d65974a 100644 --- a/api/insights/types.go +++ b/api/insights/types.go @@ -26,6 +26,7 @@ func (t *Timestamp) UnmarshalJSON(data []byte) error { return nil } +// TODO: Replace this with a type from the API. type Event struct { EventType string `json:"eventType"` EventName string `json:"eventName"` diff --git a/api/insights/utils.go b/api/insights/utils.go deleted file mode 100644 index fc132f2e..00000000 --- a/api/insights/utils.go +++ /dev/null @@ -1,19 +0,0 @@ -package insights - -import ( - "fmt" - - "github.com/algolia/algoliasearch-client-go/v3/algolia/call" - "github.com/algolia/algoliasearch-client-go/v3/algolia/region" - "github.com/algolia/algoliasearch-client-go/v3/algolia/transport" -) - -func defaultHosts(r region.Region) (hosts []*transport.StatefulHost) { - switch r { - case region.DE, region.US: - hosts = append(hosts, transport.NewStatefulHost(fmt.Sprintf("insights.%s.algolia.io", r), call.IsReadWrite)) - default: - hosts = append(hosts, transport.NewStatefulHost("insights.algolia.io", call.IsReadWrite)) - } - return -} diff --git a/cmd/docs/main.go b/cmd/docs/main.go index c06c8b6e..d9476351 100644 --- a/cmd/docs/main.go +++ b/cmd/docs/main.go @@ -23,7 +23,12 @@ func main() { func run(args []string) error { flags := pflag.NewFlagSet("", pflag.ContinueOnError) - dir := flags.StringP("app_data-path", "", "", "Path directory where you want generate documentation data files") + dir := flags.StringP( + "app_data-path", + "", + "", + "Path directory where you want generate documentation data files", + ) help := flags.BoolP("help", "h", false, "Help about any command") target := flags.StringP("target", "T", "old", "target old or new documentation website") @@ -51,7 +56,7 @@ func run(args []string) error { }) rootCmd.InitDefaultHelpCmd() - if err := os.MkdirAll(*dir, 0755); err != nil { + if err := os.MkdirAll(*dir, 0o755); err != nil { return err } @@ -66,5 +71,4 @@ func run(args []string) error { } return nil - } diff --git a/devbox.json b/devbox.json new file mode 100644 index 00000000..4717bfef --- /dev/null +++ b/devbox.json @@ -0,0 +1,12 @@ +{ + "$schema": "https://raw.githubusercontent.com/jetify-com/devbox/0.13.7/.schema/devbox.schema.json", + "packages": [ + "go-task@latest", + "go@1.23.4", + "golangci-lint@1.63.4", + "gofumpt@latest", + "golines@latest", + "gh@latest", + "curl@latest" + ] +} diff --git a/devbox.lock b/devbox.lock new file mode 100644 index 00000000..d6b9c6a5 --- /dev/null +++ b/devbox.lock @@ -0,0 +1,420 @@ +{ + "lockfile_version": "1", + "packages": { + "curl@latest": { + "last_modified": "2025-03-01T01:09:10Z", + "resolved": "github:NixOS/nixpkgs/199169a2135e6b864a888e89a2ace345703c025d#curl", + "source": "devbox-search", + "version": "8.12.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/9myjd1zdkkax5jc3f3229j442miaq7kj-curl-8.12.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/p28rf5q9r38zjmrri672y5simy393wsj-curl-8.12.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/csqw5yhj3zpg03vqxlncw3c28kf8x1zs-curl-8.12.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/qwgrhvxfpipy422d60k8k5w56cmgqb9x-curl-8.12.0-devdoc" + }, + { + "name": "out", + "path": "/nix/store/wb9nfblv2jnil545kphk5lvslfq1g3b8-curl-8.12.0" + } + ], + "store_path": "/nix/store/9myjd1zdkkax5jc3f3229j442miaq7kj-curl-8.12.0-bin" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/fl6kcwnyls1vqcnbb87n021dfad7n7sc-curl-8.12.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/38mms36gmk6gwjanhd6kpfxh1lkjhqpa-curl-8.12.0-man", + "default": true + }, + { + "name": "out", + "path": "/nix/store/hifg644pihcpysmyhng1szck9ml5zily-curl-8.12.0" + }, + { + "name": "debug", + "path": "/nix/store/w5ialygvxmmj65zrlzdj68rsc6zbxr9g-curl-8.12.0-debug" + }, + { + "name": "dev", + "path": "/nix/store/hklf3asvj79ydqqdfpmax1ppcdm5ikj8-curl-8.12.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/1cjg5qd88ia6fv8nb9s1x0fppmz01dvy-curl-8.12.0-devdoc" + } + ], + "store_path": "/nix/store/fl6kcwnyls1vqcnbb87n021dfad7n7sc-curl-8.12.0-bin" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/lj6gw1acr0669krygpccm1dphsh1mj71-curl-8.12.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/vwqh8i5iv2n0g55pbkkqf77af6lydiv3-curl-8.12.0-man", + "default": true + }, + { + "name": "out", + "path": "/nix/store/0fa623i9pja3mvqd1vid5dhc4daz0r45-curl-8.12.0" + }, + { + "name": "dev", + "path": "/nix/store/gczdlrihh9l93a1hwc7zsw3pydffaw9b-curl-8.12.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/pwjzxpsk1w87hfbysjb2zvvx5gkklmlh-curl-8.12.0-devdoc" + } + ], + "store_path": "/nix/store/lj6gw1acr0669krygpccm1dphsh1mj71-curl-8.12.0-bin" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "bin", + "path": "/nix/store/ms1vcpk06m5szgr1lyraasjimml78v6z-curl-8.12.0-bin", + "default": true + }, + { + "name": "man", + "path": "/nix/store/wx5qzp42x0fsqwyvf178yk9w4wfx2shc-curl-8.12.0-man", + "default": true + }, + { + "name": "dev", + "path": "/nix/store/qj3l9cxfj8hwib1496af23aclj52xabk-curl-8.12.0-dev" + }, + { + "name": "devdoc", + "path": "/nix/store/avav4ics90xrg1fw8gi5xkir762qz1dk-curl-8.12.0-devdoc" + }, + { + "name": "out", + "path": "/nix/store/simdgzgxrcqkvak7zpczr176rgbar3s5-curl-8.12.0" + }, + { + "name": "debug", + "path": "/nix/store/6zy2wa0g1vjk9h7pzypg2648rzdmmsi4-curl-8.12.0-debug" + } + ], + "store_path": "/nix/store/ms1vcpk06m5szgr1lyraasjimml78v6z-curl-8.12.0-bin" + } + } + }, + "gh@latest": { + "last_modified": "2025-02-13T04:03:32Z", + "resolved": "github:NixOS/nixpkgs/2d55b4c1531187926c2a423f6940b3b1301399b5#gh", + "source": "devbox-search", + "version": "2.67.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/9n2f576hng76zzhx308qxjzw6g4npgnh-gh-2.67.0", + "default": true + } + ], + "store_path": "/nix/store/9n2f576hng76zzhx308qxjzw6g4npgnh-gh-2.67.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/wbb60p6ijhrajrnaz45v66gjp5wsb503-gh-2.67.0", + "default": true + } + ], + "store_path": "/nix/store/wbb60p6ijhrajrnaz45v66gjp5wsb503-gh-2.67.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/psbp4wp18fmnynd24yklwv30465f7wwk-gh-2.67.0", + "default": true + } + ], + "store_path": "/nix/store/psbp4wp18fmnynd24yklwv30465f7wwk-gh-2.67.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6zxliiicnw366ma37132lqd9hcwxs1ic-gh-2.67.0", + "default": true + } + ], + "store_path": "/nix/store/6zxliiicnw366ma37132lqd9hcwxs1ic-gh-2.67.0" + } + } + }, + "github:NixOS/nixpkgs/nixpkgs-unstable": { + "resolved": "github:NixOS/nixpkgs/573c650e8a14b2faa0041645ab18aed7e60f0c9a?lastModified=1741865919&narHash=sha256-4thdbnP6dlbdq%2BqZWTsm4ffAwoS8Tiq1YResB%2BRP6WE%3D" + }, + "go-task@latest": { + "last_modified": "2025-02-07T11:26:36Z", + "resolved": "github:NixOS/nixpkgs/d98abf5cf5914e5e4e9d57205e3af55ca90ffc1d#go-task", + "source": "devbox-search", + "version": "3.41.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/m0zc229zl4nfi9800dvxikyp31davmgl-go-task-3.41.0", + "default": true + } + ], + "store_path": "/nix/store/m0zc229zl4nfi9800dvxikyp31davmgl-go-task-3.41.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/3yg6irq9bdr34krjp0nbrrpd8gzfr4ag-go-task-3.41.0", + "default": true + } + ], + "store_path": "/nix/store/3yg6irq9bdr34krjp0nbrrpd8gzfr4ag-go-task-3.41.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/slgvld5bfxl4caqi5jwwd583ay12cikj-go-task-3.41.0", + "default": true + } + ], + "store_path": "/nix/store/slgvld5bfxl4caqi5jwwd583ay12cikj-go-task-3.41.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jn35gb9isnqzr9mfwhmig40c6iflmadz-go-task-3.41.0", + "default": true + } + ], + "store_path": "/nix/store/jn35gb9isnqzr9mfwhmig40c6iflmadz-go-task-3.41.0" + } + } + }, + "go@1.23.4": { + "last_modified": "2025-01-19T08:16:51Z", + "resolved": "github:NixOS/nixpkgs/50165c4f7eb48ce82bd063e1fb8047a0f515f8ce#go", + "source": "devbox-search", + "version": "1.23.4", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/lg7l1czmd5hqwwhjszprdz342q98zmkw-go-1.23.4", + "default": true + } + ], + "store_path": "/nix/store/lg7l1czmd5hqwwhjszprdz342q98zmkw-go-1.23.4" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/2x7k6yj9z37s3d3x5j3kkz6bcak8k9qr-go-1.23.4", + "default": true + } + ], + "store_path": "/nix/store/2x7k6yj9z37s3d3x5j3kkz6bcak8k9qr-go-1.23.4" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/8x0ag0v37rfy9z58a8yy3hakj3dzdr9c-go-1.23.4", + "default": true + } + ], + "store_path": "/nix/store/8x0ag0v37rfy9z58a8yy3hakj3dzdr9c-go-1.23.4" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0b6vsy4fa4i4qpk1011hi6251nwdg5y8-go-1.23.4", + "default": true + } + ], + "store_path": "/nix/store/0b6vsy4fa4i4qpk1011hi6251nwdg5y8-go-1.23.4" + } + } + }, + "gofumpt@latest": { + "last_modified": "2025-02-23T09:42:26Z", + "resolved": "github:NixOS/nixpkgs/2d068ae5c6516b2d04562de50a58c682540de9bf#gofumpt", + "source": "devbox-search", + "version": "0.7.0", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/q6xv4w3fxp0zgrgzahlgbppzhrc9zgs5-gofumpt-0.7.0", + "default": true + } + ], + "store_path": "/nix/store/q6xv4w3fxp0zgrgzahlgbppzhrc9zgs5-gofumpt-0.7.0" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/6k117ys4vrd1699a6168smbz1i2ka8s0-gofumpt-0.7.0", + "default": true + } + ], + "store_path": "/nix/store/6k117ys4vrd1699a6168smbz1i2ka8s0-gofumpt-0.7.0" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/08k254mr11ffryvv42n29nvhmkcxmnql-gofumpt-0.7.0", + "default": true + } + ], + "store_path": "/nix/store/08k254mr11ffryvv42n29nvhmkcxmnql-gofumpt-0.7.0" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/m64p14qm2xr45a36g6c4p1isw340am77-gofumpt-0.7.0", + "default": true + } + ], + "store_path": "/nix/store/m64p14qm2xr45a36g6c4p1isw340am77-gofumpt-0.7.0" + } + } + }, + "golangci-lint@1.63.4": { + "last_modified": "2025-02-07T11:26:36Z", + "resolved": "github:NixOS/nixpkgs/d98abf5cf5914e5e4e9d57205e3af55ca90ffc1d#golangci-lint", + "source": "devbox-search", + "version": "1.63.4", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/mhd101l8p4cim0ac7w6g5q0vd44zjl6b-golangci-lint-1.63.4", + "default": true + } + ], + "store_path": "/nix/store/mhd101l8p4cim0ac7w6g5q0vd44zjl6b-golangci-lint-1.63.4" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/nnkfa1x72m7g0z6anjqyzlich6x58ywj-golangci-lint-1.63.4", + "default": true + } + ], + "store_path": "/nix/store/nnkfa1x72m7g0z6anjqyzlich6x58ywj-golangci-lint-1.63.4" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/gpf1pmzzkr12ibzqz3hqrw2hbzqkggcl-golangci-lint-1.63.4", + "default": true + } + ], + "store_path": "/nix/store/gpf1pmzzkr12ibzqz3hqrw2hbzqkggcl-golangci-lint-1.63.4" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/jdqir4ijb6msmlwxzg0zbv8aiiizj6fv-golangci-lint-1.63.4", + "default": true + } + ], + "store_path": "/nix/store/jdqir4ijb6msmlwxzg0zbv8aiiizj6fv-golangci-lint-1.63.4" + } + } + }, + "golines@latest": { + "last_modified": "2025-02-07T11:26:36Z", + "resolved": "github:NixOS/nixpkgs/d98abf5cf5914e5e4e9d57205e3af55ca90ffc1d#golines", + "source": "devbox-search", + "version": "0.12.2", + "systems": { + "aarch64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/0183j0kva6250dx975s7qsxz4990m525-golines-0.12.2", + "default": true + } + ], + "store_path": "/nix/store/0183j0kva6250dx975s7qsxz4990m525-golines-0.12.2" + }, + "aarch64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/kb36pm6l9zm8ib36yjx9hijs68pl3d2i-golines-0.12.2", + "default": true + } + ], + "store_path": "/nix/store/kb36pm6l9zm8ib36yjx9hijs68pl3d2i-golines-0.12.2" + }, + "x86_64-darwin": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/k3wyarhab4jsndak4mvr1x8ksxciadpm-golines-0.12.2", + "default": true + } + ], + "store_path": "/nix/store/k3wyarhab4jsndak4mvr1x8ksxciadpm-golines-0.12.2" + }, + "x86_64-linux": { + "outputs": [ + { + "name": "out", + "path": "/nix/store/92k5729rwjphdq51rclschpdkrh4j0rz-golines-0.12.2", + "default": true + } + ], + "store_path": "/nix/store/92k5729rwjphdq51rclschpdkrh4j0rz-golines-0.12.2" + } + } + } + } +} diff --git a/go.mod b/go.mod index 549a4987..d2ddafd3 100644 --- a/go.mod +++ b/go.mod @@ -1,67 +1,80 @@ module github.com/algolia/cli -go 1.19 +go 1.23.0 + +toolchain go1.23.4 require ( - github.com/AlecAivazis/survey/v2 v2.3.5 - github.com/BurntSushi/toml v1.2.0 + github.com/AlecAivazis/survey/v2 v2.3.7 + github.com/BurntSushi/toml v1.4.0 github.com/MakeNowJust/heredoc v1.0.0 - github.com/algolia/algoliasearch-client-go/v3 v3.30.0 + github.com/algolia/algoliasearch-client-go/v4 v4.13.0 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 - github.com/briandowns/spinner v1.19.0 - github.com/cli/safeexec v1.0.0 - github.com/dustin/go-humanize v1.0.0 + github.com/briandowns/spinner v1.23.2 + github.com/cli/safeexec v1.0.1 + github.com/dustin/go-humanize v1.0.1 github.com/getkin/kin-openapi v0.100.0 github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hashicorp/go-version v1.6.0 - github.com/mattn/go-colorable v0.1.13 - github.com/mattn/go-isatty v0.0.16 + github.com/hashicorp/go-version v1.7.0 + github.com/mattn/go-colorable v0.1.14 + github.com/mattn/go-isatty v0.0.20 github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d github.com/mitchellh/go-homedir v1.1.0 - github.com/mitchellh/mapstructure v1.5.0 github.com/muesli/reflow v0.3.0 - github.com/muesli/termenv v0.12.0 - github.com/sirupsen/logrus v1.9.0 - github.com/spf13/cobra v1.5.0 - github.com/spf13/pflag v1.0.5 - github.com/spf13/viper v1.13.0 - github.com/stretchr/testify v1.8.0 + github.com/muesli/termenv v0.16.0 + github.com/segmentio/analytics-go/v3 v3.3.0 + github.com/sirupsen/logrus v1.9.3 + github.com/spf13/cobra v1.9.1 + github.com/spf13/pflag v1.0.6 + github.com/spf13/viper v1.19.0 + github.com/stretchr/testify v1.10.0 github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c - golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 - gopkg.in/segmentio/analytics-go.v3 v3.1.0 + golang.org/x/term v0.30.0 gopkg.in/yaml.v3 v3.0.1 - k8s.io/client-go v0.25.0 + k8s.io/client-go v0.32.3 ) require ( - github.com/davecgh/go-spew v1.1.1 // indirect - github.com/fatih/color v1.13.0 // indirect - github.com/fsnotify/fsnotify v1.5.4 // indirect - github.com/go-openapi/jsonpointer v0.19.5 // indirect - github.com/go-openapi/swag v0.22.3 // indirect + github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect + github.com/fatih/color v1.18.0 // indirect + github.com/fsnotify/fsnotify v1.8.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.8 // indirect + github.com/go-openapi/jsonpointer v0.21.0 // indirect + github.com/go-openapi/swag v0.23.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.25.0 // indirect + github.com/google/uuid v1.6.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.0.1 // indirect - github.com/invopop/yaml v0.2.0 // indirect + github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/invopop/yaml v0.1.0 // indirect github.com/josharian/intern v1.0.0 // indirect github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect - github.com/kr/pretty v0.3.0 // indirect + github.com/kr/pretty v0.3.1 // indirect github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.2.0 // indirect - github.com/magiconair/properties v1.8.6 // indirect + github.com/magiconair/properties v1.8.9 // indirect github.com/mailru/easyjson v0.7.7 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect - github.com/pelletier/go-toml v1.9.5 // indirect - github.com/pelletier/go-toml/v2 v2.0.5 // indirect - github.com/pmezard/go-difflib v1.0.0 // indirect - github.com/rivo/uniseg v0.3.4 // indirect + github.com/mattn/go-runewidth v0.0.16 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/pelletier/go-toml/v2 v2.2.3 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/rivo/uniseg v0.4.7 // indirect github.com/rogpeppe/go-internal v1.9.0 // indirect - github.com/segmentio/backo-go v1.0.1 // indirect - github.com/spf13/afero v1.9.2 // indirect - github.com/spf13/cast v1.5.0 // indirect - github.com/spf13/jwalterweatherman v1.1.0 // indirect - github.com/subosito/gotenv v1.4.1 // indirect - golang.org/x/sys v0.0.0-20220907062415-87db552b00fd // indirect - golang.org/x/text v0.3.8 // indirect + github.com/sagikazarmark/locafero v0.7.0 // indirect + github.com/sagikazarmark/slog-shim v0.1.0 // indirect + github.com/segmentio/backo-go v1.1.0 // indirect + github.com/sourcegraph/conc v0.3.0 // indirect + github.com/spf13/afero v1.12.0 // indirect + github.com/spf13/cast v1.7.1 // indirect + github.com/subosito/gotenv v1.6.0 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.36.0 // indirect + golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 // indirect + golang.org/x/net v0.37.0 // indirect + golang.org/x/sys v0.31.0 // indirect + golang.org/x/text v0.23.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect - gopkg.in/yaml.v2 v2.4.0 // indirect ) diff --git a/go.sum b/go.sum index e8916e06..ec369a71 100644 --- a/go.sum +++ b/go.sum @@ -1,207 +1,102 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/AlecAivazis/survey/v2 v2.3.5 h1:A8cYupsAZkjaUmhtTYv3sSqc7LO5mp1XDfqe5E/9wRQ= -github.com/AlecAivazis/survey/v2 v2.3.5/go.mod h1:4AuI9b7RjAR+G7v9+C4YSlX/YL3K3cWNXgWXOhllqvI= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.2.0 h1:Rt8g24XnyGTyglgET/PRUNlrUeu9F5L+7FilkXfZgs0= -github.com/BurntSushi/toml v1.2.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/AlecAivazis/survey/v2 v2.3.7 h1:6I/u8FvytdGsgonrYsVn2t8t4QiRnh6QSTqkkhIiSjQ= +github.com/AlecAivazis/survey/v2 v2.3.7/go.mod h1:xUTIdE4KCOIjsBAE1JYsUPoCqYdZ1reCfTwbto0Fduo= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/MakeNowJust/heredoc v1.0.0 h1:cXCdzVdstXyiTqTvfqk9SDHpKNjxuom+DOlyEeQ4pzQ= github.com/MakeNowJust/heredoc v1.0.0/go.mod h1:mG5amYoWBHf8vpLOuehzbGGw0EHxpZZ6lCpQ4fNJ8LE= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2 h1:+vx7roKuyA63nhn5WAunQHLTznkw5W8b1Xc0dNjp83s= github.com/Netflix/go-expect v0.0.0-20220104043353-73e0943537d2/go.mod h1:HBCaDeC1lPdgDeDbhX8XFpy1jqjK0IBG8W5K+xYqA0w= -github.com/algolia/algoliasearch-client-go/v3 v3.30.0 h1:sZvynLYzLrDok2uqQoN3JmUkRUbs4HvLmdf1uvGFXL8= -github.com/algolia/algoliasearch-client-go/v3 v3.30.0/go.mod h1:i7tLoP7TYDmHX3Q7vkIOL4syVse/k5VJ+k0i8WqFiJk= +github.com/algolia/algoliasearch-client-go/v4 v4.13.0 h1:rgThwsQWVAePnYkBmXAXfG5jB8JMbuWUV9Oj8N08SR8= +github.com/algolia/algoliasearch-client-go/v4 v4.13.0/go.mod h1:Vq4V9gK/ncGu8msftKUBMjgjny4Zaw3J3+lCUfM/xng= +github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= +github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4YnC6+E63dPcxHo2sUxDIu8g3QgEJdRY= github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= -github.com/briandowns/spinner v1.19.0 h1:s8aq38H+Qju89yhp89b4iIiMzMm8YN3p6vGpwyh/a8E= -github.com/briandowns/spinner v1.19.0/go.mod h1:mQak9GHqbspjC/5iUx3qMlIho8xBS/ppAL/hX5SmPJU= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/cli/safeexec v1.0.0 h1:0VngyaIyqACHdcMNWfo6+KdUYnqEr2Sg+bSP1pdF+dI= -github.com/cli/safeexec v1.0.0/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= +github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= +github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= +github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.17 h1:QeVUsEDNrLBW4tMgZHvxy18sKtr6VI492kBhUfhDJNI= github.com/creack/pty v1.1.17/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/dustin/go-humanize v1.0.0 h1:VSnTsYCnlFHaM2/igO1h6X3HA71jcobQuxemgkq4zYo= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= -github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= -github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= -github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= +github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM= +github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU= +github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8= +github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= +github.com/fsnotify/fsnotify v1.8.0 h1:dAwr6QBTBZIkG8roQaJjGof0pp0EeF+tNV7YBP3F/8M= +github.com/fsnotify/fsnotify v1.8.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= +github.com/gabriel-vasile/mimetype v1.4.8 h1:FfZ3gj38NjllZIeJAmMhr+qKL8Wu+nOoI3GqacKw1NM= +github.com/gabriel-vasile/mimetype v1.4.8/go.mod h1:ByKUIKGjh1ODkGM1asKUbQZOLGrPjydw3hYPU2YU9t8= github.com/getkin/kin-openapi v0.100.0 h1:8L9xNFNJFDqIRjZwwFjWhTTmTAxPRn/BVTzPn+hOA2s= github.com/getkin/kin-openapi v0.100.0/go.mod h1:w4lRPHiyOdwGbOkLIyk+P0qCwlu7TXPCHD/64nSXzgE= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-openapi/jsonpointer v0.19.5 h1:gZr+CIYByUqjcgeLXnQu2gHYQC9o73G2XUeOFYEICuY= github.com/go-openapi/jsonpointer v0.19.5/go.mod h1:Pl9vOtqEWErmShwVjC8pYs9cog34VGT37dQOVbmoatg= +github.com/go-openapi/jsonpointer v0.21.0 h1:YgdVicSA9vH5RiHs9TZW5oyafXZFc6+2Vc1rr/O9oNQ= +github.com/go-openapi/jsonpointer v0.21.0/go.mod h1:IUyH9l/+uyhIYQ/PXVA41Rexl+kOkAPDdXEYns6fzUY= github.com/go-openapi/swag v0.19.5/go.mod h1:POnQmlKehdgb5mhVOsnJFsivZCEZ/vjK9gh66Z9tfKk= -github.com/go-openapi/swag v0.22.3 h1:yMBqmnQ0gyZvEb/+KzuWZOXgllrXT4SADYbvDaXHv/g= -github.com/go-openapi/swag v0.22.3/go.mod h1:UzaqsxGiab7freDnrUUra0MwWfN/q7tE4j+VcZ0yl14= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/go-openapi/swag v0.23.0 h1:vsEVJDUo2hPJ2tu0/Xc+4noaxyEffXNIs3cOULZ+GrE= +github.com/go-openapi/swag v0.23.0/go.mod h1:esZ8ITTYEsH1V2trKHjAN8Ai7xHb8RV+YSZ577vPjgQ= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.25.0 h1:5Dh7cjvzR7BRZadnsVOzPhWsrwUr0nmsZJxEAnFLNO8= +github.com/go-playground/validator/v10 v10.25.0/go.mod h1:GGzBIJMuE98Ic/kJsBXbz1x/7cByt++cQ+YOuDM5wus= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= -github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= +github.com/hashicorp/go-version v1.7.0 h1:5tqGy27NaOTB8yJKUZELlFAS/LTKJkrmONwQKeRZfjY= +github.com/hashicorp/go-version v1.7.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec h1:qv2VnGeEQHchGaZ/u7lxST/RaJw+cv273q79D81Xbog= github.com/hinshun/vt10x v0.0.0-20220119200601-820417d04eec/go.mod h1:Q48J4R4DvxnHolD5P8pOtXigYlRuPLGl6moFx3ulM68= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= -github.com/inconshreveable/mousetrap v1.0.1 h1:U3uMjPSQEBMNp1lFxmllqCPM6P5u/Xq7Pgzkat/bFNc= -github.com/inconshreveable/mousetrap v1.0.1/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= +github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/invopop/yaml v0.1.0 h1:YW3WGUoJEXYfzWBjn00zIlrw7brGVD0fUKRYDPAPhrc= github.com/invopop/yaml v0.1.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= -github.com/invopop/yaml v0.2.0 h1:7zky/qH+O0DwAyoobXUqvVBwgBFRxKoQ/3FjcVpjTMY= -github.com/invopop/yaml v0.2.0/go.mod h1:2XuRLgs/ouIrW3XNzuNj7J3Nvu/Dig5MXvbCEdiBN3Q= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs= github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= -github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= +github.com/magiconair/properties v1.8.9 h1:nWcCbLq1N2v/cpNsy5WvQ37Fb+YElfq20WJ/a8RkpQM= +github.com/magiconair/properties v1.8.9/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= github.com/mailru/easyjson v0.0.0-20190614124828-94de47d64c63/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.0.0-20190626092158-b2ccc519800e/go.mod h1:C1wdFJiN94OJF2b5HbByQZoLdCWB1Yqtg26g4irojpc= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= -github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= -github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-runewidth v0.0.12/go.mod h1:RAqKPSqVFrSLVXbA8x7dzmKdmGzieGRCM46jaSJTDAk= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc= +github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d h1:5PJl274Y63IEHC+7izoQE9x6ikvDFZS2mDVS3drnohI= github.com/mgutz/ansi v0.0.0-20200706080929-d51e80ef957d/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= @@ -211,380 +106,106 @@ github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyua github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/muesli/reflow v0.3.0 h1:IFsN6K9NfGtjeggFP+68I4chLZV2yIKsXJFNZ+eWh6s= github.com/muesli/reflow v0.3.0/go.mod h1:pbwTDkVPibjO2kyvBQRBxTWEEGDGq0FlB1BIKtnHY/8= -github.com/muesli/termenv v0.12.0 h1:KuQRUE3PgxRFWhq4gHvZtPSLCGDqM5q/cYr1pZ39ytc= -github.com/muesli/termenv v0.12.0/go.mod h1:WCCv32tusQ/EEZ5S8oUIIrC/nIuBcxCVqlN4Xfkv+7A= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.0.5 h1:ipoSadvV8oGUjnUbMub59IDPPwfxF694nG/jwbMiyQg= -github.com/pelletier/go-toml/v2 v2.0.5/go.mod h1:OMHamSCAODeSsVrwwvcJOaoN0LIUIaFVNZzmWyNfXas= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc= +github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= +github.com/pelletier/go-toml/v2 v2.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M= +github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc= +github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= +github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/rivo/uniseg v0.3.4 h1:3Z3Eu6FGHZWSfNKJTOUiPatWwfc7DzJRU04jFUqJODw= -github.com/rivo/uniseg v0.3.4/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= +github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8= github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/segmentio/backo-go v1.0.1 h1:68RQccglxZeyURy93ASB/2kc9QudzgIDexJ927N++y4= -github.com/segmentio/backo-go v1.0.1/go.mod h1:9/Rh6yILuLysoQnZ2oNooD2g7aBnvM7r/fNVxRNWfBc= -github.com/sirupsen/logrus v1.9.0 h1:trlNQbNUG3OdDrDil03MCb1H2o9nJ1x4/5LYw7byDE0= -github.com/sirupsen/logrus v1.9.0/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= -github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= -github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= -github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= -github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= -github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.13.0 h1:BWSJ/M+f+3nmdz9bxB+bWX28kkALN2ok11D0rSo8EJU= -github.com/spf13/viper v1.13.0/go.mod h1:Icm2xNL3/8uyh/wFuB1jI7TiTNKp8632Nwegu+zgdYw= +github.com/sagikazarmark/locafero v0.7.0 h1:5MqpDsTGNDhY8sGp0Aowyf0qKsPrhewaLSsFaodPcyo= +github.com/sagikazarmark/locafero v0.7.0/go.mod h1:2za3Cg5rMaTMoG/2Ulr9AwtFaIppKXTRYnozin4aB5k= +github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= +github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= +github.com/segmentio/analytics-go/v3 v3.3.0 h1:8VOMaVGBW03pdBrj1CMFfY9o/rnjJC+1wyQHlVxjw5o= +github.com/segmentio/analytics-go/v3 v3.3.0/go.mod h1:p8owAF8X+5o27jmvUognuXxdtqvSGtD0ZrfY2kcS9bE= +github.com/segmentio/backo-go v1.1.0 h1:cJIfHQUdmLsd8t9IXqf5J8SdrOMn9vMa7cIvOavHAhc= +github.com/segmentio/backo-go v1.1.0/go.mod h1:ckenwdf+v/qbyhVdNPWHnqh2YdJBED1O9cidYyM5J18= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= +github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= +github.com/spf13/afero v1.12.0 h1:UcOPyRBYczmFn6yvphxkn9ZEOY65cpwGKb5mL36mrqs= +github.com/spf13/afero v1.12.0/go.mod h1:ZTlWwG4/ahT8W7T0WQ5uYmjI9duaLQGy3Q2OAl4sk/4= +github.com/spf13/cast v1.7.1 h1:cuNEagBQEHWN1FnbGEjCXL2szYEXqfJPbP2HNUaca9Y= +github.com/spf13/cast v1.7.1/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo= +github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= +github.com/spf13/cobra v1.9.1/go.mod h1:nDyEzZ8ogv936Cinf6g1RU9MRY64Ir93oCnqb9wxYW0= +github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= +github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI= +github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/subosito/gotenv v1.4.1 h1:jyEFiXpy21Wm81FBN71l9VoMMV8H8jG+qIK3GCpY6Qs= -github.com/subosito/gotenv v1.4.1/go.mod h1:ayKnFf/c6rvx/2iiLrJUk1e6plDbT3edrFNGqEflhK0= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= +github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c h1:3lbZUMbMiGUW/LMkfsEABsc5zNT9+b1CvsJx47JzJ8g= github.com/xtgo/uuid v0.0.0-20140804021211-a0b114877d4c/go.mod h1:UrdRz5enIKZ63MEE3IF9l2/ebyx59GyGgPi+tICQdmM= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.36.0 h1:AnAEvhDddvBdpY+uR+MyHmuZzzNqXSe/GvuDeob5L34= +golang.org/x/crypto v0.36.0/go.mod h1:Y4J0ReaxCR1IMaabaSMugxJES1EpwhBHhv2bDHklZvc= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394 h1:nDVHiLt8aIbd/VzvPWN6kSOPE7+F/fNFDSXLVYkE/Iw= +golang.org/x/exp v0.0.0-20250305212735-054e65f0b394/go.mod h1:sIifuuw/Yco/y6yb6+bDNfyeQ/MdPUy/hKEMYQV17cM= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.37.0 h1:1zLorHbz+LYj7MQlSf1+2tPIIgibq2eL5xkrGk6f+2c= +golang.org/x/net v0.37.0/go.mod h1:ivrbrMbzFq5J41QOQh0siUuly180yBYtLp+CKbEaFx8= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220422013727-9388b58f7150/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd h1:AZeIEzg+8RCELJYq8w+ODLVxFgLMMigSwO/ffKPEd9U= -golang.org/x/sys v0.0.0-20220907062415-87db552b00fd/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= +golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210503060354-a79de5458b56/go.mod h1:tfny5GFUkzUvx4ps4ajbZsCe5lw1metzhBm9T3x7oIY= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035 h1:Q5284mrmYTpACcm+eAKjKJH48BBwSyfJqmmGDTtT8Vc= -golang.org/x/term v0.0.0-20220722155259-a9ba230a4035/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= +golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.23.0 h1:D71I7dUrlY+VX0gQShAThNGHFxZ13dGLBHQLVl1mJlY= +golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/segmentio/analytics-go.v3 v3.1.0 h1:UzxH1uaGZRpMKDhJyBz0pexz6yUoBU3x8bJsRk/HV6U= -gopkg.in/segmentio/analytics-go.v3 v3.1.0/go.mod h1:4QqqlTlSSpVlWA9/9nDcPw+FkM2yv1NQoYjUbL9/JAw= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -k8s.io/client-go v0.25.0 h1:CVWIaCETLMBNiTUta3d5nzRbXvY5Hy9Dpl+VvREpu5E= -k8s.io/client-go v0.25.0/go.mod h1:lxykvypVfKilxhTklov0wz1FoaUZ8X4EwbhS6rpRfN8= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +k8s.io/client-go v0.32.3 h1:RKPVltzopkSgHS7aS98QdscAgtgah/+zmpAogooIqVU= +k8s.io/client-go v0.32.3/go.mod h1:3v0+3k4IcT9bXTc4V2rt+d2ZPPG700Xy6Oi0Gdl2PaY= diff --git a/internal/analyze/analyze.go b/internal/analyze/analyze.go index 8d83796a..1340a152 100644 --- a/internal/analyze/analyze.go +++ b/internal/analyze/analyze.go @@ -3,11 +3,9 @@ package analyze import ( "encoding/json" "fmt" - "io" "strings" - "github.com/algolia/algoliasearch-client-go/v3/algolia/iterator" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" ) // AttributeType is an enum for the different types of attributes. @@ -47,33 +45,19 @@ type Stats struct { } // ComputeStats computes the stats for the given index. -func ComputeStats(i iterator.Iterator, s search.Settings, limit int, only string, counter chan int) (*Stats, error) { - settingsMap := settingsAsMap(s) +func ComputeStats( + records []search.Hit, + settings search.SettingsResponse, + only string, +) (*Stats, error) { + settingsMap := settingsAsMap(settings) stats := &Stats{ - Attributes: make(map[string]*AttributeStats), + Attributes: make(map[string]*AttributeStats), + TotalRecords: len(records), } - for { - iObject, err := i.Next() - if err != nil { - if err == io.EOF { - break - } - return nil, err - } - - object, ok := iObject.(map[string]interface{}) - if !ok { - continue - } - - if limit > 0 && stats.TotalRecords >= limit { - break - } - - stats.TotalRecords++ - counter <- 1 - stats = computeObjectStats(stats, "", object, only) + for _, record := range records { + stats = computeObjectStats(stats, "", record.AdditionalProperties, only) } for key, value := range stats.Attributes { @@ -175,7 +159,7 @@ func getType(value interface{}) AttributeType { // settingsAsMap converts the given settings to a map. // We marshal and unmarshal the settings to avoid having to write the conversion code ourselves. -func settingsAsMap(s search.Settings) map[string]interface{} { +func settingsAsMap(s search.SettingsResponse) map[string]interface{} { var settingsMap map[string]interface{} var settingsBytes []byte settingsBytes, err := s.MarshalJSON() diff --git a/internal/docs/docs.go b/internal/docs/docs.go index 6b0cb22d..e228f8a9 100644 --- a/internal/docs/docs.go +++ b/internal/docs/docs.go @@ -83,7 +83,6 @@ func newCommand(cmd *cobra.Command) Command { if categoryFlagSet.Print.HasAvailableFlags() { flags["Output formatting flags"] = newFlags(categoryFlagSet.Print) - } command.Flags = flags diff --git a/internal/docs/yaml.go b/internal/docs/yaml.go index 7e0c86a4..c58a00a3 100644 --- a/internal/docs/yaml.go +++ b/internal/docs/yaml.go @@ -14,8 +14,8 @@ func GenYamlTree(cmd *cobra.Command, dir string) error { commands := getCommands(cmd) for _, c := range commands { - command_path := strings.ReplaceAll(c.Name, " ", "_") - filename := filepath.Join(dir, fmt.Sprintf("%s.yml", command_path)) + commandPath := strings.ReplaceAll(c.Name, " ", "_") + filename := filepath.Join(dir, fmt.Sprintf("%s.yml", commandPath)) f, err := os.Create(filename) if err != nil { return err diff --git a/internal/update/update.go b/internal/update/update.go index 3a6ff661..be3814d6 100644 --- a/internal/update/update.go +++ b/internal/update/update.go @@ -33,7 +33,10 @@ type StateEntry struct { } // CheckForUpdate checks whether this software has had a newer release on GitHub -func CheckForUpdate(client *http.Client, stateFilePath, currentVersion string) (*ReleaseInfo, error) { +func CheckForUpdate( + client *http.Client, + stateFilePath, currentVersion string, +) (*ReleaseInfo, error) { stateEntry, _ := getStateEntry(stateFilePath) if stateEntry != nil && time.Since(stateEntry.CheckedForUpdateAt).Hours() < 24 { return nil, nil @@ -97,12 +100,12 @@ func setStateEntry(stateFilePath string, t time.Time, r ReleaseInfo) error { return err } - err = os.MkdirAll(filepath.Dir(stateFilePath), 0755) + err = os.MkdirAll(filepath.Dir(stateFilePath), 0o755) if err != nil { return err } - err = os.WriteFile(stateFilePath, content, 0600) + err = os.WriteFile(stateFilePath, content, 0o600) return err } diff --git a/pkg/ask/ask.go b/pkg/ask/ask.go index db1c989f..b72eb662 100644 --- a/pkg/ask/ask.go +++ b/pkg/ask/ask.go @@ -16,7 +16,12 @@ func (my *StringSlice) WriteAnswer(name string, value interface{}) error { return nil } -func AskCommaSeparatedInputQuestion(message string, storage *[]string, defaultValues []string, opts ...survey.AskOpt) error { +func AskCommaSeparatedInputQuestion( + message string, + storage *[]string, + defaultValues []string, + opts ...survey.AskOpt, +) error { stringSlice := StringSlice{} err := survey.AskOne( &survey.Input{ @@ -30,7 +35,13 @@ func AskCommaSeparatedInputQuestion(message string, storage *[]string, defaultVa return err } -func AskMultiSelectQuestion(message string, defaultValues []string, storage *[]string, options []string, opts ...survey.AskOpt) error { +func AskMultiSelectQuestion( + message string, + defaultValues []string, + storage *[]string, + options []string, + opts ...survey.AskOpt, +) error { err := survey.AskOne( &survey.MultiSelect{ Message: message, @@ -43,7 +54,13 @@ func AskMultiSelectQuestion(message string, defaultValues []string, storage *[]s return err } -func AskSelectQuestion(message string, storage *string, options []string, defaultValue string, opts ...survey.AskOpt) error { +func AskSelectQuestion( + message string, + storage *string, + options []string, + defaultValue string, + opts ...survey.AskOpt, +) error { return survey.AskOne(&survey.Select{ Message: message, Options: options, @@ -51,14 +68,25 @@ func AskSelectQuestion(message string, storage *string, options []string, defaul }, storage, opts...) } -func AskInputQuestion(message string, storage *string, defaultValue string, opts ...survey.AskOpt) error { +func AskInputQuestion( + message string, + storage *string, + defaultValue string, + opts ...survey.AskOpt, +) error { return survey.AskOne(&survey.Input{ Message: message, Default: defaultValue, }, storage, opts...) } -func AskInputQuestionWithSuggestion(message string, storage *string, defaultValue string, suggest func(toComplete string) []string, opts ...survey.AskOpt) error { +func AskInputQuestionWithSuggestion( + message string, + storage *string, + defaultValue string, + suggest func(toComplete string) []string, + opts ...survey.AskOpt, +) error { return survey.AskOne(&survey.Input{ Message: message, Default: defaultValue, @@ -66,7 +94,12 @@ func AskInputQuestionWithSuggestion(message string, storage *string, defaultValu }, storage, opts...) } -func AskBooleanQuestion(message string, storage *bool, defaultValue bool, opts ...survey.AskOpt) error { +func AskBooleanQuestion( + message string, + storage *bool, + defaultValue bool, + opts ...survey.AskOpt, +) error { return survey.AskOne(&survey.Confirm{ Message: message, Default: defaultValue, diff --git a/pkg/auth/auth_check.go b/pkg/auth/auth_check.go index defd6da4..5186eec9 100644 --- a/pkg/auth/auth_check.go +++ b/pkg/auth/auth_check.go @@ -12,33 +12,34 @@ import ( "github.com/algolia/cli/pkg/utils" ) -var ( - WriteAPIKeyDefaultACLs = []string{ - "search", - "browse", - "seeUnretrievableAttributes", - "listIndexes", - "analytics", - "logs", - "addObject", - "deleteObject", - "deleteIndex", - "settings", - "editSettings", - "recommendation", - } -) +var WriteAPIKeyDefaultACLs = []string{ + "search", + "browse", + "seeUnretrievableAttributes", + "listIndexes", + "analytics", + "logs", + "addObject", + "deleteObject", + "deleteIndex", + "settings", + "editSettings", + "recommendation", +} // errMissingACLs return an error with the missing ACLs func errMissingACLs(missing []string) error { - err := fmt.Sprintf("Missing API Key ACL(s): %s\n", strings.Join(missing, ", ")) - err += "Either edit your profile or use the `--api-key` flag to provide an API Key with the missing ACLs.\n" - err += "See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restrictions for more information.\n" + err := fmt.Sprintf("Missing API key ACL(s): %s\n", strings.Join(missing, ", ")) + err += "Edit your profile or use the `--api-key` flag to provide an API key with the missing ACLs.\n" + err += "See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restrictions for more information" + return errors.New(err) } // errAdminAPIKeyRequired is returned when the command requires an admin API Key -var errAdminAPIKeyRequired = errors.New("This command requires an admin API Key. Please use the `--api-key` flag to provide a valid admin API Key.\n") +var errAdminAPIKeyRequired = errors.New( + "this command requires an admin API key. Use the `--api-key` flag with a valid admin API key", +) func DisableAuthCheck(cmd *cobra.Command) { if cmd.Annotations == nil { @@ -81,7 +82,7 @@ func CheckACLs(cmd *cobra.Command, f *cmdutil.Factory) error { if err != nil { return err } - _, err = client.ListAPIKeys() + _, err = client.ListApiKeys() if err == nil { return nil // Admin API Key, no need to check ACLs } @@ -92,12 +93,21 @@ func CheckACLs(cmd *cobra.Command, f *cmdutil.Factory) error { } // Check the ACLs of the provided API Key - apiKey, err := client.GetAPIKey(f.Config.Profile().GetAPIKey()) + key, err := f.Config.Profile().GetAPIKey() if err != nil { return err } + apiKey, err := client.GetApiKey(client.NewApiGetApiKeyRequest(key)) + if err != nil { + return err + } + + var hasAcls []string + for _, acl := range apiKey.Acl { + hasAcls = append(hasAcls, string(acl)) + } - missingACLs := utils.Differences(neededACLs, apiKey.ACL) + missingACLs := utils.Differences(neededACLs, hasAcls) if len(missingACLs) > 0 { return errMissingACLs(missingACLs) } diff --git a/pkg/auth/auth_check_test.go b/pkg/auth/auth_check_test.go index b3887fbe..90b584f3 100644 --- a/pkg/auth/auth_check_test.go +++ b/pkg/auth/auth_check_test.go @@ -1,9 +1,10 @@ package auth import ( + "os" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/pkg/httpmock" "github.com/algolia/cli/test" @@ -12,11 +13,15 @@ import ( ) func Test_CheckACLs(t *testing.T) { + // Remove these environment variables before the tests + os.Unsetenv("ALGOLIA_APPLICATION_ID") + os.Unsetenv("ALGOLIA_API_KEY") + tests := []struct { name string cmd *cobra.Command adminKey bool - ACLs []string + ACLs []search.Acl wantErr bool wantErrMessage string }{ @@ -26,7 +31,7 @@ func Test_CheckACLs(t *testing.T) { Annotations: map[string]string{}, }, adminKey: false, - ACLs: []string{}, + ACLs: []search.Acl{}, wantErr: false, }, { @@ -37,9 +42,9 @@ func Test_CheckACLs(t *testing.T) { }, }, adminKey: false, - ACLs: []string{}, + ACLs: []search.Acl{}, wantErr: true, - wantErrMessage: "This command requires an admin API Key. Please use the `--api-key` flag to provide a valid admin API Key.\n", + wantErrMessage: "this command requires an admin API key. Use the `--api-key` flag with a valid admin API key", }, { name: "need admin key, admin key", @@ -49,7 +54,7 @@ func Test_CheckACLs(t *testing.T) { }, }, adminKey: true, - ACLs: []string{}, + ACLs: []search.Acl{}, wantErr: false, wantErrMessage: "", }, @@ -61,12 +66,11 @@ func Test_CheckACLs(t *testing.T) { }, }, adminKey: false, - ACLs: []string{}, + ACLs: []search.Acl{}, wantErr: true, - wantErrMessage: `Missing API Key ACL(s): search -Either edit your profile or use the ` + "`--api-key`" + ` flag to provide an API Key with the missing ACLs. -See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restrictions for more information. -`, + wantErrMessage: `Missing API key ACL(s): search +Edit your profile or use the ` + "`--api-key`" + ` flag to provide an API key with the missing ACLs. +See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restrictions for more information`, }, { name: "need ACLs, has ACLs", @@ -76,7 +80,7 @@ See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restriction }, }, adminKey: false, - ACLs: []string{"search"}, + ACLs: []search.Acl{search.ACL_SEARCH}, wantErr: false, }, } @@ -87,7 +91,7 @@ See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restriction if tt.adminKey { r.Register( httpmock.REST("GET", "1/keys"), - httpmock.JSONResponse(search.ListAPIKeysRes{}), + httpmock.JSONResponse(search.ListApiKeysResponse{}), ) } else { r.Register( @@ -99,7 +103,7 @@ See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restriction if tt.ACLs != nil && !tt.adminKey { r.Register( httpmock.REST("GET", "1/keys/test"), - httpmock.JSONResponse(search.Key{ACL: tt.ACLs}), + httpmock.JSONResponse(search.ApiKey{Acl: tt.ACLs}), ) } @@ -114,5 +118,4 @@ See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restriction } }) } - } diff --git a/pkg/cmd/apikeys/create/create.go b/pkg/cmd/apikeys/create/create.go index b9f55ccc..1a56eda3 100644 --- a/pkg/cmd/apikeys/create/create.go +++ b/pkg/cmd/apikeys/create/create.go @@ -5,7 +5,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/dustin/go-humanize" "github.com/spf13/cobra" @@ -20,7 +20,7 @@ type CreateOptions struct { config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) ACL []string Description string @@ -37,8 +37,9 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co SearchClient: f.SearchClient, } cmd := &cobra.Command{ - Use: "create", - Args: validators.NoArgs(), + Use: "create", + Aliases: []string{"new", "n", "c"}, + Args: validators.NoArgs(), Annotations: map[string]string{ "acls": "admin", }, @@ -98,21 +99,27 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co Describe an API key to help you identify its uses.`, )) - _ = cmd.RegisterFlagCompletionFunc("indices", func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - client, err := f.SearchClient() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - indicesRes, err := client.ListIndices() - if err != nil { - return nil, cobra.ShellCompDirectiveError - } - allowedIndices := make([]string, 0, len(indicesRes.Items)) - for _, index := range indicesRes.Items { - allowedIndices = append(allowedIndices, fmt.Sprintf("%s\t%s records", index.Name, humanize.Comma(index.Entries))) - } - return allowedIndices, cobra.ShellCompDirectiveNoFileComp - }) + _ = cmd.RegisterFlagCompletionFunc( + "indices", + func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { + client, err := f.SearchClient() + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + indicesRes, err := client.ListIndices(client.NewApiListIndicesRequest()) + if err != nil { + return nil, cobra.ShellCompDirectiveError + } + allowedIndices := make([]string, 0, len(indicesRes.Items)) + for _, index := range indicesRes.Items { + allowedIndices = append( + allowedIndices, + fmt.Sprintf("%s\t%s records", index.Name, humanize.Comma(int64(index.Entries))), + ) + } + return allowedIndices, cobra.ShellCompDirectiveNoFileComp + }, + ) _ = cmd.RegisterFlagCompletionFunc("acl", cmdutil.StringSliceCompletionFunc(map[string]string{ @@ -136,19 +143,24 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co // runCreateCmd executes the create command func runCreateCmd(opts *CreateOptions) error { - key := search.Key{ - ACL: opts.ACL, + var acls []search.Acl + for _, a := range opts.ACL { + acls = append(acls, search.Acl(a)) + } + validity := int32(opts.Validity.Seconds()) + key := search.ApiKey{ + Acl: acls, Indexes: opts.Indices, - Validity: opts.Validity, + Validity: &validity, Referers: opts.Referers, - Description: opts.Description, + Description: &opts.Description, } client, err := opts.SearchClient() if err != nil { return err } - res, err := client.AddAPIKey(key) + res, err := client.AddApiKey(client.NewApiAddApiKeyRequest(&key)) if err != nil { return err } diff --git a/pkg/cmd/apikeys/create/create_test.go b/pkg/cmd/apikeys/create/create_test.go index cb8fd1ca..b5d00b21 100644 --- a/pkg/cmd/apikeys/create/create_test.go +++ b/pkg/cmd/apikeys/create/create_test.go @@ -4,7 +4,7 @@ import ( "testing" "time" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -105,7 +105,10 @@ func Test_runCreateCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", "1/keys"), httpmock.JSONResponse(search.CreateKeyRes{Key: "foo"})) + r.Register( + httpmock.REST("POST", "1/keys"), + httpmock.JSONResponse(search.AddApiKeyResponse{Key: "foo"}), + ) f, out := test.NewFactory(tt.isTTY, &r, nil, "") cmd := NewCreateCmd(f, nil) diff --git a/pkg/cmd/apikeys/delete/delete.go b/pkg/cmd/apikeys/delete/delete.go index 2406b96d..35c67301 100644 --- a/pkg/cmd/apikeys/delete/delete.go +++ b/pkg/cmd/apikeys/delete/delete.go @@ -3,7 +3,7 @@ package delete import ( "fmt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -18,7 +18,7 @@ type DeleteOptions struct { config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) APIKey string DoConfirm bool @@ -45,7 +45,9 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co opts.APIKey = args[0] if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -58,7 +60,8 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete API key confirmation prompt") + cmd.Flags(). + BoolVarP(&confirm, "confirm", "y", false, "Skip the delete API key confirmation prompt") return cmd } @@ -70,14 +73,17 @@ func runDeleteCmd(opts *DeleteOptions) error { return err } - _, err = client.GetAPIKey(opts.APIKey) + _, err = client.GetApiKey(client.NewApiGetApiKeyRequest(opts.APIKey)) if err != nil { return fmt.Errorf("API key %q does not exist", opts.APIKey) } if opts.DoConfirm { var confirmed bool - err = prompt.Confirm(fmt.Sprintf("Delete the following API key: %s?", opts.APIKey), &confirmed) + err = prompt.Confirm( + fmt.Sprintf("Delete the following API key: %s?", opts.APIKey), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -86,14 +92,19 @@ func runDeleteCmd(opts *DeleteOptions) error { } } - _, err = client.DeleteAPIKey(opts.APIKey) + _, err = client.DeleteApiKey(client.NewApiDeleteApiKeyRequest(opts.APIKey)) if err != nil { return err } cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s API key successfully deleted: %s\n", cs.SuccessIcon(), opts.APIKey) + fmt.Fprintf( + opts.IO.Out, + "%s API key successfully deleted: %s\n", + cs.SuccessIcon(), + opts.APIKey, + ) } return nil } diff --git a/pkg/cmd/apikeys/delete/delete_test.go b/pkg/cmd/apikeys/delete/delete_test.go index a5fa6e57..91871bdd 100644 --- a/pkg/cmd/apikeys/delete/delete_test.go +++ b/pkg/cmd/apikeys/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -103,8 +103,14 @@ func Test_runDeleteCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("GET", fmt.Sprintf("1/keys/%s", tt.key)), httpmock.JSONResponse(search.Key{Value: "foo"})) - r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/keys/%s", tt.key)), httpmock.JSONResponse(search.DeleteKeyRes{})) + r.Register( + httpmock.REST("GET", fmt.Sprintf("1/keys/%s", tt.key)), + httpmock.JSONResponse(search.GetApiKeyResponse{Value: "foo"}), + ) + r.Register( + httpmock.REST("DELETE", fmt.Sprintf("1/keys/%s", tt.key)), + httpmock.JSONResponse(search.DeletedAtResponse{}), + ) f, out := test.NewFactory(tt.isTTY, &r, nil, "") cmd := NewDeleteCmd(f, nil) diff --git a/pkg/cmd/apikeys/get/get.go b/pkg/cmd/apikeys/get/get.go index 3f89f9e1..220e3967 100644 --- a/pkg/cmd/apikeys/get/get.go +++ b/pkg/cmd/apikeys/get/get.go @@ -4,10 +4,9 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" - "github.com/algolia/cli/pkg/cmd/apikeys/shared" "github.com/algolia/cli/pkg/cmdutil" "github.com/algolia/cli/pkg/config" "github.com/algolia/cli/pkg/iostreams" @@ -19,14 +18,14 @@ type GetOptions struct { config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) APIKey string PrintFlags *cmdutil.PrintFlags } -// NewGetCmd returns a new instance of DeleteCmd +// NewGetCmd returns a new instance of the command func NewGetCmd(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command { opts := &GetOptions{ IO: f.IOStreams, @@ -68,7 +67,7 @@ func runGetCmd(opts *GetOptions) error { return err } - key, err := client.GetAPIKey(opts.APIKey) + key, err := client.GetApiKey(client.NewApiGetApiKeyRequest(opts.APIKey)) if err != nil { return fmt.Errorf("API key %q does not exist", opts.APIKey) } @@ -77,20 +76,8 @@ func runGetCmd(opts *GetOptions) error { if err != nil { return err } - keyResult := shared.JSONKey{ - ACL: key.ACL, - CreatedAt: key.CreatedAt, - Description: key.Description, - Indexes: key.Indexes, - MaxQueriesPerIPPerHour: key.MaxQueriesPerIPPerHour, - MaxHitsPerQuery: key.MaxHitsPerQuery, - Referers: key.Referers, - QueryParameters: key.QueryParameters, - Validity: key.Validity, - Value: key.Value, - } - if err := p.Print(opts.IO, keyResult); err != nil { + if err := p.Print(opts.IO, key); err != nil { return err } diff --git a/pkg/cmd/apikeys/get/get_test.go b/pkg/cmd/apikeys/get/get_test.go index 6dd56c0c..ebd2affd 100644 --- a/pkg/cmd/apikeys/get/get_test.go +++ b/pkg/cmd/apikeys/get/get_test.go @@ -3,7 +3,7 @@ package get import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -31,16 +31,13 @@ func Test_runGetCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} if tt.key == "foo" { + name := "test" r.Register( httpmock.REST("GET", "1/keys/foo"), - httpmock.JSONResponse(search.Key{ - Value: "foo", - Description: "test", - ACL: []string{"*"}, - Validity: 0, - MaxHitsPerQuery: 0, - MaxQueriesPerIPPerHour: 0, - Referers: []string{}, + httpmock.JSONResponse(search.GetApiKeyResponse{ + Value: "foo", + Description: &name, + Acl: []search.Acl{search.ACL_SEARCH}, }), ) } else { diff --git a/pkg/cmd/apikeys/list/list.go b/pkg/cmd/apikeys/list/list.go index c9f0e2ee..ec7c3f28 100644 --- a/pkg/cmd/apikeys/list/list.go +++ b/pkg/cmd/apikeys/list/list.go @@ -5,11 +5,10 @@ import ( "sort" "time" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/dustin/go-humanize" "github.com/spf13/cobra" - "github.com/algolia/cli/pkg/cmd/apikeys/shared" "github.com/algolia/cli/pkg/cmdutil" "github.com/algolia/cli/pkg/config" "github.com/algolia/cli/pkg/iostreams" @@ -21,7 +20,7 @@ type ListOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) PrintFlags *cmdutil.PrintFlags } @@ -35,8 +34,9 @@ func NewListCmd(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman PrintFlags: cmdutil.NewPrintFlags(), } cmd := &cobra.Command{ - Use: "list", - Args: validators.NoArgs(), + Use: "list", + Aliases: []string{"l"}, + Args: validators.NoArgs(), Annotations: map[string]string{ "acls": "admin", }, @@ -63,38 +63,12 @@ func runListCmd(opts *ListOptions) error { } opts.IO.StartProgressIndicatorWithLabel("Fetching API Keys") - res, err := client.ListAPIKeys() + res, err := client.ListApiKeys() opts.IO.StopProgressIndicator() if err != nil { return err } - if opts.PrintFlags.OutputFlagSpecified() && opts.PrintFlags.OutputFormat != nil { - p, err := opts.PrintFlags.ToPrinter() - if err != nil { - return err - } - for _, key := range res.Keys { - keyResult := shared.JSONKey{ - ACL: key.ACL, - CreatedAt: key.CreatedAt, - Description: key.Description, - Indexes: key.Indexes, - MaxQueriesPerIPPerHour: key.MaxQueriesPerIPPerHour, - MaxHitsPerQuery: key.MaxHitsPerQuery, - Referers: key.Referers, - QueryParameters: key.QueryParameters, - Validity: key.Validity, - Value: key.Value, - } - - if err := p.Print(opts.IO, keyResult); err != nil { - return err - } - } - return nil - } - table := printers.NewTablePrinter(opts.IO) if table.IsTTY() { table.AddField("KEY", nil, nil) @@ -111,25 +85,37 @@ func runListCmd(opts *ListOptions) error { // Sort API Keys by createdAt sort.Slice(res.Keys, func(i, j int) bool { - return res.Keys[i].CreatedAt.After(res.Keys[j].CreatedAt) + return res.Keys[i].CreatedAt > res.Keys[j].CreatedAt }) for _, key := range res.Keys { table.AddField(key.Value, nil, nil) - table.AddField(key.Description, nil, nil) - table.AddField(fmt.Sprintf("%v", key.ACL), nil, nil) + if key.Description != nil { + table.AddField(*key.Description, nil, nil) + } + table.AddField(fmt.Sprintf("%v", key.Acl), nil, nil) table.AddField(fmt.Sprintf("%v", key.Indexes), nil, nil) table.AddField(func() string { - if key.Validity == 0 { + if key.Validity == nil || *key.Validity == 0 { return "Never expire" } else { - return humanize.Time(time.Now().Add(key.Validity)) + validity := time.Duration(*key.Validity) * time.Second + return humanize.Time(time.Now().Add(validity)) } }(), nil, nil) - table.AddField(humanize.Comma(int64(key.MaxHitsPerQuery)), nil, nil) - table.AddField(humanize.Comma(int64(key.MaxQueriesPerIPPerHour)), nil, nil) + if key.MaxHitsPerQuery == nil || *key.MaxHitsPerQuery == 0 { + table.AddField("0", nil, nil) + } else { + table.AddField(humanize.Comma(int64(*key.MaxHitsPerQuery)), nil, nil) + } + if key.MaxQueriesPerIPPerHour == nil || *key.MaxQueriesPerIPPerHour == 0 { + table.AddField("0", nil, nil) + } else { + table.AddField(humanize.Comma(int64(*key.MaxQueriesPerIPPerHour)), nil, nil) + } table.AddField(fmt.Sprintf("%v", key.Referers), nil, nil) - table.AddField(humanize.Time(key.CreatedAt), nil, nil) + createdAt := time.Unix(key.CreatedAt, 0) + table.AddField(humanize.Time(createdAt), nil, nil) table.EndRow() } return table.Render() diff --git a/pkg/cmd/apikeys/list/list_test.go b/pkg/cmd/apikeys/list/list_test.go index 884b2a5d..287e9a22 100644 --- a/pkg/cmd/apikeys/list/list_test.go +++ b/pkg/cmd/apikeys/list/list_test.go @@ -2,9 +2,8 @@ package list import ( "testing" - "time" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -20,33 +19,28 @@ func Test_runListCmd(t *testing.T) { { name: "list", isTTY: false, - wantOut: "\ttest\t[*]\t[]\tNever expire\t0\t0\t[]\ta long while ago\n", + wantOut: "foo\ttest\t[search]\t[]\tNever expire\t0\t0\t[]\t5 years ago\n", }, { - name: "list_tty", - isTTY: true, - wantOut: `KEY DESCRIPTION ACL INDICES VALIDITY MAX H... MAX Q... REFERERS CREAT... - test [*] [] Never... 0 0 [] a lon... -`, + name: "list_tty", + isTTY: true, + wantOut: "KEY DESCRIPTION ACL INDICES VALI... MAX ... MAX ... REFE... CREA...\nfoo test [sea... [] Neve... 0 0 [] 5 ye...\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + name := "test" r := httpmock.Registry{} r.Register( httpmock.REST("GET", "1/keys"), - httpmock.JSONResponse(search.ListAPIKeysRes{ - Keys: []search.Key{ + httpmock.JSONResponse(search.ListApiKeysResponse{ + Keys: []search.GetApiKeyResponse{ { - Value: "foo", - Description: "test", - ACL: []string{"*"}, - Validity: 0, - MaxHitsPerQuery: 0, - MaxQueriesPerIPPerHour: 0, - Referers: []string{}, - CreatedAt: time.Date(2020, 1, 1, 0, 0, 0, 0, time.UTC), + Value: "foo", + Description: &name, + Acl: []search.Acl{search.ACL_SEARCH}, + CreatedAt: 1577836800, }, }, }), diff --git a/pkg/cmd/apikeys/shared/shared.go b/pkg/cmd/apikeys/shared/shared.go deleted file mode 100644 index adc3dc4b..00000000 --- a/pkg/cmd/apikeys/shared/shared.go +++ /dev/null @@ -1,21 +0,0 @@ -package shared - -import ( - "time" - - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" -) - -// JSONKey is the same as search.Key without omitting values -type JSONKey struct { - ACL []string `json:"acl"` - CreatedAt time.Time `json:"createdAt"` - Description string `json:"description"` - Indexes []string `json:"indexes"` - MaxQueriesPerIPPerHour int `json:"maxQueriesPerIPPerHour"` - MaxHitsPerQuery int `json:"maxHitsPerQuery"` - Referers []string `json:"referers"` - QueryParameters search.KeyQueryParams `json:"queryParameters"` - Validity time.Duration `json:"validity"` - Value string `json:"value"` -} diff --git a/pkg/cmd/art/art.go b/pkg/cmd/art/art.go deleted file mode 100644 index b27270bd..00000000 --- a/pkg/cmd/art/art.go +++ /dev/null @@ -1,60 +0,0 @@ -package art - -import ( - "fmt" - "time" - - "github.com/algolia/cli/pkg/auth" - "github.com/algolia/cli/pkg/cmdutil" - "github.com/spf13/cobra" -) - -var art = ` - ________________________ - / \ - | _____ | - | _. XXXXX | - | X/ .<&%%Y%%&>. | - | .%#/ |##+\#%. | - | %#` + "`" + ` |#/ ` + "`" + `#% | - | %# + #% | - | ` + "`" + `%\ /%` + "`" + ` | - | \#%b.___.d%#/ | - | ` + "`" + `` + "`" + `+===+` + "`" + `` + "`" + ` | - | | - \ ________________________ / - - Congratulations on your epic search! - You found the ☼art☼! -` - -func NewArtCmd(f *cmdutil.Factory) *cobra.Command { - loadingMessages := []string{ - "The legends speak of an Algolia developer so strong...", - "so capable...", - "so powerful at search and discovery...", - "that they could find the hidden art at the root of all things...", - "So spake the legends...", - } - - cmd := &cobra.Command{ - Use: "art", - Short: "We've been searching for the art for so long...!", - Run: func(cmd *cobra.Command, args []string) { - io := f.IOStreams - io.StartProgressIndicatorWithLabel("LEGENDARY QUEST ACCEPTED") - time.Sleep(2 * time.Second) - for i := 0; i < len(loadingMessages); i++ { - io.UpdateProgressIndicatorLabel(loadingMessages[i]) - time.Sleep(2 * time.Second) - } - io.StopProgressIndicator() - fmt.Println(art) - }, - Hidden: true, - } - - auth.DisableAuthCheck(cmd) - - return cmd -} diff --git a/pkg/cmd/crawler/crawl/crawl.go b/pkg/cmd/crawler/crawl/crawl.go index 7ac8331a..51a0a513 100644 --- a/pkg/cmd/crawler/crawl/crawl.go +++ b/pkg/cmd/crawler/crawl/crawl.go @@ -86,7 +86,13 @@ func runCrawlCmd(opts *CrawlOptions) error { } cs := opts.IO.ColorScheme() - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Requesting crawl for %s on crawler %s", utils.Pluralize(len(opts.URLs), "URL"), opts.ID)) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf( + "Requesting crawl for %s on crawler %s", + utils.Pluralize(len(opts.URLs), "URL"), + opts.ID, + ), + ) _, err = client.CrawlURLs(opts.ID, opts.URLs, opts.Save, opts.SaveSpecified) opts.IO.StopProgressIndicator() if err != nil { @@ -94,7 +100,13 @@ func runCrawlCmd(opts *CrawlOptions) error { } if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully requested crawl for %s on crawler %s\n", cs.SuccessIconWithColor(cs.Green), utils.Pluralize(len(opts.URLs), "URL"), opts.ID) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully requested crawl for %s on crawler %s\n", + cs.SuccessIconWithColor(cs.Green), + utils.Pluralize(len(opts.URLs), "URL"), + opts.ID, + ) } return nil diff --git a/pkg/cmd/crawler/crawl/crawl_test.go b/pkg/cmd/crawler/crawl/crawl_test.go index d45c248d..1fb0e655 100644 --- a/pkg/cmd/crawler/crawl/crawl_test.go +++ b/pkg/cmd/crawler/crawl/crawl_test.go @@ -154,7 +154,10 @@ func Test_runCrawlCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} if tt.wantErr == "" { - r.Register(httpmock.REST("POST", "api/1/crawlers/"+tt.id+"/urls/crawl"), httpmock.JSONResponse(crawler.TaskIDResponse{TaskID: "taskID"})) + r.Register( + httpmock.REST("POST", "api/1/crawlers/"+tt.id+"/urls/crawl"), + httpmock.JSONResponse(crawler.TaskIDResponse{TaskID: "taskID"}), + ) } else { r.Register(httpmock.REST("POST", "api/1/crawlers/"+tt.id+"/urls/crawl"), httpmock.ErrorResponseWithBody(crawler.ErrResponse{Err: crawler.Err{Code: "not-found", Message: "Crawler not-found not found"}})) } diff --git a/pkg/cmd/crawler/create/create.go b/pkg/cmd/crawler/create/create.go index cd55c41c..1f013467 100644 --- a/pkg/cmd/crawler/create/create.go +++ b/pkg/cmd/crawler/create/create.go @@ -34,9 +34,10 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co var configFile string cmd := &cobra.Command{ - Use: "create -F ", - Args: cobra.ExactArgs(1), - Short: "Create a crawler", + Use: "create -F ", + Aliases: []string{"new", "n", "c"}, + Args: cobra.ExactArgs(1), + Short: "Create a crawler", Long: heredoc.Doc(` Create a new crawler from the given configuration. `), @@ -67,7 +68,8 @@ func NewCreateCmd(f *cmdutil.Factory, runF func(*CreateOptions) error) *cobra.Co }, } - cmd.Flags().StringVarP(&configFile, "file", "F", "", "Path to the configuration file (use \"-\" to read from standard input)") + cmd.Flags(). + StringVarP(&configFile, "file", "F", "", "Path to the configuration file (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") return cmd @@ -88,7 +90,13 @@ func runCreateCmd(opts *CreateOptions) error { } if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Crawler %s created: %s\n", cs.SuccessIconWithColor(cs.Green), cs.Bold(opts.Name), cs.Bold(id)) + fmt.Fprintf( + opts.IO.Out, + "%s Crawler %s created: %s\n", + cs.SuccessIconWithColor(cs.Green), + cs.Bold(opts.Name), + cs.Bold(id), + ) } return nil diff --git a/pkg/cmd/crawler/create/create_test.go b/pkg/cmd/crawler/create/create_test.go index a4d6d357..97f318cf 100644 --- a/pkg/cmd/crawler/create/create_test.go +++ b/pkg/cmd/crawler/create/create_test.go @@ -19,7 +19,7 @@ import ( func TestNewCreateCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "config.json") - err := os.WriteFile(tmpFile, []byte("{\"enableReRanking\":false}"), 0600) + err := os.WriteFile(tmpFile, []byte("{\"enableReRanking\":false}"), 0o600) require.NoError(t, err) tests := []struct { @@ -81,7 +81,7 @@ func TestNewCreateCmd(t *testing.T) { func Test_runCreateCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "config.json") - err := os.WriteFile(tmpFile, []byte("{\"enableReRanking\":false}"), 0600) + err := os.WriteFile(tmpFile, []byte("{\"enableReRanking\":false}"), 0o600) require.NoError(t, err) tests := []struct { diff --git a/pkg/cmd/crawler/get/get.go b/pkg/cmd/crawler/get/get.go index 27cc3eb1..fb8037f7 100644 --- a/pkg/cmd/crawler/get/get.go +++ b/pkg/cmd/crawler/get/get.go @@ -57,7 +57,8 @@ func NewGetCmd(f *cmdutil.Factory, runF func(*GetOptions) error) *cobra.Command }, } - cmd.Flags().BoolVarP(&opts.ConfigOnly, "config-only", "c", false, "Display only the crawler configuration") + cmd.Flags(). + BoolVarP(&opts.ConfigOnly, "config-only", "c", false, "Display only the crawler configuration") return cmd } diff --git a/pkg/cmd/crawler/list/list.go b/pkg/cmd/crawler/list/list.go index 138a9f90..baa78c22 100644 --- a/pkg/cmd/crawler/list/list.go +++ b/pkg/cmd/crawler/list/list.go @@ -36,9 +36,10 @@ func NewListCmd(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman PrintFlags: cmdutil.NewPrintFlags(), } cmd := &cobra.Command{ - Use: "list", - Args: validators.NoArgs(), - Short: "List crawlers", + Use: "list", + Aliases: []string{"l"}, + Args: validators.NoArgs(), + Short: "List crawlers", Long: heredoc.Doc(` List crawlers, optionally filtered by name or appID. `), diff --git a/pkg/cmd/crawler/pause/pause.go b/pkg/cmd/crawler/pause/pause.go index b604e3bd..5e40aa4a 100644 --- a/pkg/cmd/crawler/pause/pause.go +++ b/pkg/cmd/crawler/pause/pause.go @@ -64,7 +64,9 @@ func runPauseCmd(opts *PauseOptions) error { } cs := opts.IO.ColorScheme() - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Pausing %s", utils.Pluralize(len(opts.IDs), "crawler"))) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Pausing %s", utils.Pluralize(len(opts.IDs), "crawler")), + ) for _, id := range opts.IDs { if _, err := client.Reindex(id); err != nil { opts.IO.StopProgressIndicator() @@ -74,7 +76,12 @@ func runPauseCmd(opts *PauseOptions) error { opts.IO.StopProgressIndicator() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s %s\n", cs.SuccessIconWithColor(cs.Green), fmt.Sprintf("Successfully paused %s", utils.Pluralize(len(opts.IDs), "crawler"))) + fmt.Fprintf( + opts.IO.Out, + "%s %s\n", + cs.SuccessIconWithColor(cs.Green), + fmt.Sprintf("Successfully paused %s", utils.Pluralize(len(opts.IDs), "crawler")), + ) } return nil diff --git a/pkg/cmd/crawler/reindex/reindex.go b/pkg/cmd/crawler/reindex/reindex.go index 543ed7c6..35b51d95 100644 --- a/pkg/cmd/crawler/reindex/reindex.go +++ b/pkg/cmd/crawler/reindex/reindex.go @@ -64,7 +64,9 @@ func runReindexCmd(opts *ReindexOptions) error { } cs := opts.IO.ColorScheme() - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Reindexing %s", utils.Pluralize(len(opts.IDs), "crawler"))) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Reindexing %s", utils.Pluralize(len(opts.IDs), "crawler")), + ) for _, id := range opts.IDs { if _, err := client.Reindex(id); err != nil { opts.IO.StopProgressIndicator() @@ -74,7 +76,15 @@ func runReindexCmd(opts *ReindexOptions) error { opts.IO.StopProgressIndicator() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s %s\n", cs.SuccessIconWithColor(cs.Green), fmt.Sprintf("Successfully requested reindexing for %s", utils.Pluralize(len(opts.IDs), "crawler"))) + fmt.Fprintf( + opts.IO.Out, + "%s %s\n", + cs.SuccessIconWithColor(cs.Green), + fmt.Sprintf( + "Successfully requested reindexing for %s", + utils.Pluralize(len(opts.IDs), "crawler"), + ), + ) } return nil diff --git a/pkg/cmd/crawler/run/run.go b/pkg/cmd/crawler/run/run.go index 2bd81e4b..507a8510 100644 --- a/pkg/cmd/crawler/run/run.go +++ b/pkg/cmd/crawler/run/run.go @@ -68,7 +68,12 @@ func runRunCmd(opts *RunOptions) error { } if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Crawler %s started\n", cs.SuccessIconWithColor(cs.Green), opts.ID) + fmt.Fprintf( + opts.IO.Out, + "%s Crawler %s started\n", + cs.SuccessIconWithColor(cs.Green), + opts.ID, + ) } return nil diff --git a/pkg/cmd/crawler/test/test.go b/pkg/cmd/crawler/test/test.go index 91544ada..30f12227 100644 --- a/pkg/cmd/crawler/test/test.go +++ b/pkg/cmd/crawler/test/test.go @@ -78,7 +78,8 @@ func NewTestCmd(f *cmdutil.Factory, runF func(*TestOptions) error) *cobra.Comman cmd.Flags().StringVarP(&opts.URL, "url", "u", "", "The URL to test.") _ = cmd.MarkFlagRequired("url") - cmd.Flags().StringVarP(&configFile, "config", "F", "", "The configuration file to use to override the crawler's configuration. (use \"-\" to read from standard input)") + cmd.Flags(). + StringVarP(&configFile, "config", "F", "", "The configuration file to use to override the crawler's configuration. (use \"-\" to read from standard input)") return cmd } @@ -90,7 +91,9 @@ func runTestCmd(opts *TestOptions) error { } cs := opts.IO.ColorScheme() - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Testing URL %s on crawler %s", cs.Bold(opts.URL), cs.Bold(opts.ID))) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Testing URL %s on crawler %s", cs.Bold(opts.URL), cs.Bold(opts.ID)), + ) res, err := client.Test(opts.ID, opts.URL, opts.config) if err != nil { opts.IO.StopProgressIndicator() diff --git a/pkg/cmd/crawler/unblock/unblock.go b/pkg/cmd/crawler/unblock/unblock.go index b64bfb1b..387eac95 100644 --- a/pkg/cmd/crawler/unblock/unblock.go +++ b/pkg/cmd/crawler/unblock/unblock.go @@ -56,7 +56,9 @@ func NewUnblockCmd(f *cmdutil.Factory, runF func(*UnblockOptions) error) *cobra. if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -86,7 +88,14 @@ func runUnblockCmd(opts *UnblockOptions) error { if opts.DoConfirm { var confirmed bool - err := prompt.Confirm(fmt.Sprintf("Are you sure you want to unblock the crawler %q? \nBlocking error is: %s", opts.ID, crawler.BlockingError), &confirmed) + err := prompt.Confirm( + fmt.Sprintf( + "Are you sure you want to unblock the crawler %q? \nBlocking error is: %s", + opts.ID, + crawler.BlockingError, + ), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } diff --git a/pkg/cmd/dictionary/dictionary.go b/pkg/cmd/dictionary/dictionary.go index 6fd4889b..e3784fca 100644 --- a/pkg/cmd/dictionary/dictionary.go +++ b/pkg/cmd/dictionary/dictionary.go @@ -13,7 +13,7 @@ import ( func NewDictionaryCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "dictionary", - Aliases: []string{"dictionaries"}, + Aliases: []string{"dictionaries", "dict"}, Short: "Manage your Algolia dictionaries", Annotations: map[string]string{ "help:see-also": heredoc.Doc(` diff --git a/pkg/cmd/dictionary/entries/browse/browse.go b/pkg/cmd/dictionary/entries/browse/browse.go index 05b19a80..34eed4e2 100644 --- a/pkg/cmd/dictionary/entries/browse/browse.go +++ b/pkg/cmd/dictionary/entries/browse/browse.go @@ -5,8 +5,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmd/dictionary/shared" @@ -19,25 +18,15 @@ type BrowseOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Dictionaries []search.DictionaryName + Dictionaries []search.DictionaryType All bool IncludeDefaultStopwords bool PrintFlags *cmdutil.PrintFlags } -// DictionaryEntry can be plural, compound, or stop word entry. -type DictionaryEntry struct { - Type shared.EntryType - Word string `json:"word,omitempty"` - Words []string `json:"words,omitempty"` - Decomposition string `json:"decomposition,omitempty"` - ObjectID string - Language string -} - // NewBrowseCmd creates and returns a browse command for dictionaries' entries. func NewBrowseCmd(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Command { cs := f.IOStreams.ColorScheme() @@ -48,12 +37,14 @@ func NewBrowseCmd(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co SearchClient: f.SearchClient, PrintFlags: cmdutil.NewPrintFlags().WithDefaultOutput("json"), } + cmd := &cobra.Command{ Use: "browse {... | --all} [--include-defaults]", Args: cobra.OnlyValidArgs, - ValidArgs: shared.DictionaryNames(), + Aliases: []string{"list", "l"}, + ValidArgs: shared.DictionaryTypes(), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shared.DictionaryNames(), cobra.ShellCompDirectiveNoFileComp + return shared.DictionaryTypes(), cobra.ShellCompDirectiveNoFileComp }, Annotations: map[string]string{ "acls": "settings", @@ -77,15 +68,17 @@ func NewBrowseCmd(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co `), RunE: func(cmd *cobra.Command, args []string) error { if opts.All && len(args) > 0 || !opts.All && len(args) == 0 { - return cmdutil.FlagErrorf("Either specify dictionaries' names or use --all to browse all dictionaries") + return cmdutil.FlagErrorf( + "Either specify dictionaries' names or use --all to browse all dictionaries", + ) } if opts.All { - opts.Dictionaries = []search.DictionaryName{search.Stopwords, search.Plurals, search.Compounds} + opts.Dictionaries = search.AllowedDictionaryTypeEnumValues } else { - opts.Dictionaries = make([]search.DictionaryName, len(args)) - for i, dictionary := range args { - opts.Dictionaries[i] = search.DictionaryName(dictionary) + opts.Dictionaries = make([]search.DictionaryType, len(args)) + for i, dict := range args { + opts.Dictionaries[i] = search.DictionaryType(dict) } } @@ -94,7 +87,8 @@ func NewBrowseCmd(f *cmdutil.Factory, runF func(*BrowseOptions) error) *cobra.Co } cmd.Flags().BoolVarP(&opts.All, "all", "a", false, "browse all dictionaries") - cmd.Flags().BoolVarP(&opts.IncludeDefaultStopwords, "include-defaults", "d", false, "include default stopwords") + cmd.Flags(). + BoolVarP(&opts.IncludeDefaultStopwords, "include-defaults", "d", false, "include default stopwords") opts.PrintFlags.AddFlags(cmd) @@ -117,12 +111,20 @@ func runBrowseCmd(opts *BrowseOptions) error { hasNoEntries := true for _, dictionary := range opts.Dictionaries { - pageCount := 0 - maxPages := 1 + var pageCount int32 = 0 + var maxPages int32 = 1 // Infinite pagination for pageCount < maxPages { - res, err := client.SearchDictionaryEntries(dictionary, "", opt.HitsPerPage(1000), opt.Page(pageCount)) + res, err := client.SearchDictionaryEntries( + client.NewApiSearchDictionaryEntriesRequest( + dictionary, + search.NewEmptySearchDictionaryEntriesParams(). + SetHitsPerPage(1000). + SetPage(pageCount). + SetQuery(""), + ), + ) if err != nil { return err } @@ -131,13 +133,19 @@ func runBrowseCmd(opts *BrowseOptions) error { data, err := json.Marshal(res.Hits) if err != nil { - return fmt.Errorf("can't unmarshal dictionary entries: error while marshalling original dictionary entries: %v", err) + return fmt.Errorf( + "cannot unmarshal dictionary entries: error while marshalling original dictionary entries: %v", + err, + ) } - var entries []DictionaryEntry + var entries []search.DictionaryEntry err = json.Unmarshal(data, &entries) if err != nil { - return fmt.Errorf("can't unmarshal dictionary entries: error while unmarshalling original dictionary entries: %v", err) + return fmt.Errorf( + "cannot unmarshal dictionary entries: error while unmarshalling original dictionary entries: %v", + err, + ) } if len(entries) != 0 { @@ -150,8 +158,8 @@ func runBrowseCmd(opts *BrowseOptions) error { if err = p.Print(opts.IO, entry); err != nil { return err } - } else if entry.Type == shared.CustomEntryType { - // Print only custom entries + } else if *entry.Type == search.DICTIONARY_ENTRY_TYPE_CUSTOM { + // print only custom entries if err = p.Print(opts.IO, entry); err != nil { return err } diff --git a/pkg/cmd/dictionary/entries/browse/browse_test.go b/pkg/cmd/dictionary/entries/browse/browse_test.go index dc445def..f63a1675 100644 --- a/pkg/cmd/dictionary/entries/browse/browse_test.go +++ b/pkg/cmd/dictionary/entries/browse/browse_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -15,7 +15,7 @@ func Test_runBrowseCmd(t *testing.T) { tests := []struct { name string cli string - dictionaries []search.DictionaryName + dictionaries []search.DictionaryType entries bool isTTY bool wantOut string @@ -23,53 +23,53 @@ func Test_runBrowseCmd(t *testing.T) { { name: "one dictionary", cli: "plurals", - dictionaries: []search.DictionaryName{ - search.Plurals, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, entries: true, isTTY: false, - wantOut: "{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n", + wantOut: "{\"objectID\":\"\",\"type\":\"custom\"}\n", }, { name: "multiple dictionaries", cli: "plurals compounds", - dictionaries: []search.DictionaryName{ - search.Plurals, - search.Compounds, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, + search.DICTIONARY_TYPE_COMPOUNDS, }, entries: true, isTTY: false, - wantOut: "{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n", + wantOut: "{\"objectID\":\"\",\"type\":\"custom\"}\n{\"objectID\":\"\",\"type\":\"custom\"}\n", }, { name: "all dictionaries", cli: "--all", - dictionaries: []search.DictionaryName{ - search.Stopwords, - search.Plurals, - search.Compounds, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_STOPWORDS, + search.DICTIONARY_TYPE_PLURALS, + search.DICTIONARY_TYPE_COMPOUNDS, }, entries: true, isTTY: false, - wantOut: "{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n", + wantOut: "{\"objectID\":\"\",\"type\":\"custom\"}\n{\"objectID\":\"\",\"type\":\"custom\"}\n{\"objectID\":\"\",\"type\":\"custom\"}\n", }, { name: "one dictionary with default stopwords", cli: "--all --include-defaults", - dictionaries: []search.DictionaryName{ - search.Stopwords, - search.Plurals, - search.Compounds, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_STOPWORDS, + search.DICTIONARY_TYPE_PLURALS, + search.DICTIONARY_TYPE_COMPOUNDS, }, entries: true, isTTY: false, - wantOut: "{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n{\"Type\":\"custom\",\"ObjectID\":\"\",\"Language\":\"\"}\n", + wantOut: "{\"objectID\":\"\",\"type\":\"custom\"}\n{\"objectID\":\"\",\"type\":\"custom\"}\n{\"objectID\":\"\",\"type\":\"custom\"}\n", }, { name: "no entries", cli: "plurals", - dictionaries: []search.DictionaryName{ - search.Plurals, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, entries: false, isTTY: false, @@ -81,14 +81,25 @@ func Test_runBrowseCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} for _, d := range tt.dictionaries { - var entries []DictionaryEntry + var entries []search.DictionaryEntry if tt.entries { - entries = append(entries, DictionaryEntry{Type: "custom"}) + entries = append( + entries, + search.DictionaryEntry{ + Type: search.DICTIONARY_ENTRY_TYPE_CUSTOM.Ptr(), + }, + ) } - r.Register(httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/search", d)), httpmock.JSONResponse(search.SearchDictionariesRes{ - Hits: entries, - })) - r.Register(httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/batch", d)), httpmock.JSONResponse(search.TaskStatusRes{})) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/search", d)), + httpmock.JSONResponse(search.SearchDictionaryEntriesResponse{ + Hits: entries, + }), + ) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/batch", d)), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) } f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/dictionary/entries/clear/clear.go b/pkg/cmd/dictionary/entries/clear/clear.go index 54617320..2144482b 100644 --- a/pkg/cmd/dictionary/entries/clear/clear.go +++ b/pkg/cmd/dictionary/entries/clear/clear.go @@ -5,8 +5,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -22,18 +21,14 @@ type ClearOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Dictionaries []search.DictionaryName + Dictionaries []search.DictionaryType All bool DoConfirm bool } -type DictionaryEntry struct { - Type shared.EntryType -} - // NewClearCmd creates and returns a clear command for dictionaries' entries. func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Command { var confirm bool @@ -47,9 +42,9 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm cmd := &cobra.Command{ Use: "clear {... | --all} [--confirm]", Args: cobra.OnlyValidArgs, - ValidArgs: shared.DictionaryNames(), + ValidArgs: shared.DictionaryTypes(), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shared.DictionaryNames(), cobra.ShellCompDirectiveNoFileComp + return shared.DictionaryTypes(), cobra.ShellCompDirectiveNoFileComp }, Annotations: map[string]string{ "acls": "settings,editSettings", @@ -70,21 +65,25 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm `), RunE: func(cmd *cobra.Command, args []string) error { if opts.All && len(args) > 0 || !opts.All && len(args) == 0 { - return cmdutil.FlagErrorf("Either specify dictionaries' names or use --all to clear all dictionaries") + return cmdutil.FlagErrorf( + "Either specify dictionaries' names or use --all to clear all dictionaries", + ) } if opts.All { - opts.Dictionaries = []search.DictionaryName{search.Stopwords, search.Plurals, search.Compounds} + opts.Dictionaries = search.AllowedDictionaryTypeEnumValues } else { - opts.Dictionaries = make([]search.DictionaryName, len(args)) + opts.Dictionaries = make([]search.DictionaryType, len(args)) for i, dictionary := range args { - opts.Dictionaries[i] = search.DictionaryName(dictionary) + opts.Dictionaries[i] = search.DictionaryType(dictionary) } } if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -97,7 +96,8 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the clear dictionary entry confirmation prompt") + cmd.Flags(). + BoolVarP(&confirm, "confirm", "y", false, "Skip the clear dictionary entry confirmation prompt") cmd.Flags().BoolVarP(&opts.All, "all", "a", false, "Clear all dictionaries") return cmd @@ -137,7 +137,14 @@ func runClearCmd(opts *ClearOptions) error { if opts.DoConfirm { var confirmed bool - err = prompt.Confirm(fmt.Sprintf("Clear %d entries from %s dictionary?", totalEntries, utils.SliceToReadableString(dictionariesNames)), &confirmed) + err = prompt.Confirm( + fmt.Sprintf( + "Clear %d entries from %s dictionary?", + totalEntries, + utils.SliceToReadableString(dictionariesNames), + ), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -146,39 +153,62 @@ func runClearCmd(opts *ClearOptions) error { } } - for _, dictionary := range dictionaries { - _, err = client.ClearDictionaryEntries(dictionary) + for _, dict := range dictionaries { + batchParams := search.NewBatchDictionaryEntriesParams( + []search.BatchDictionaryEntriesRequest{}, + search.WithBatchDictionaryEntriesParamsClearExistingDictionaryEntries(true), + ) + _, err := client.BatchDictionaryEntries( + client.NewApiBatchDictionaryEntriesRequest(dict, batchParams), + ) if err != nil { return err } } if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully cleared %d entries from %s dictionary\n", cs.SuccessIcon(), totalEntries, utils.SliceToReadableString(dictionariesNames)) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully cleared %d entries from %s dictionary\n", + cs.SuccessIcon(), + totalEntries, + utils.SliceToReadableString(dictionariesNames), + ) } return nil } -func customEntriesNb(client *search.Client, dictionary search.DictionaryName) (int, error) { - res, err := client.SearchDictionaryEntries(dictionary, "", opt.HitsPerPage(1000)) +func customEntriesNb(client *search.APIClient, dictionary search.DictionaryType) (int, error) { + res, err := client.SearchDictionaryEntries( + client.NewApiSearchDictionaryEntriesRequest( + dictionary, + search.NewEmptySearchDictionaryEntriesParams().SetHitsPerPage(1000).SetQuery(""), + ), + ) if err != nil { return 0, err } data, err := json.Marshal(res.Hits) if err != nil { - return 0, fmt.Errorf("cannot unmarshal dictionary entries: error while marshalling original dictionary entries: %v", err) + return 0, fmt.Errorf( + "cannot unmarshal dictionary entries: error while marshalling original dictionary entries: %v", + err, + ) } - var entries []DictionaryEntry + var entries []search.DictionaryEntry err = json.Unmarshal(data, &entries) if err != nil { - return 0, fmt.Errorf("cannot unmarshal dictionary entries: error while unmarshalling original dictionary entries: %v", err) + return 0, fmt.Errorf( + "cannot unmarshal dictionary entries: error while unmarshalling original dictionary entries: %v", + err, + ) } var customEntriesNb int for _, entry := range entries { - if entry.Type == shared.CustomEntryType { + if entry.Type != nil && *entry.Type == search.DICTIONARY_ENTRY_TYPE_CUSTOM { customEntriesNb++ } } diff --git a/pkg/cmd/dictionary/entries/clear/clear_test.go b/pkg/cmd/dictionary/entries/clear/clear_test.go index 5a5c946e..9dbf461f 100644 --- a/pkg/cmd/dictionary/entries/clear/clear_test.go +++ b/pkg/cmd/dictionary/entries/clear/clear_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -30,8 +30,8 @@ func TestNewClearCmd(t *testing.T) { wantsErr: true, wantsOpts: ClearOptions{ DoConfirm: true, - Dictionaries: []search.DictionaryName{ - search.Plurals, + Dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, }, }, @@ -42,8 +42,8 @@ func TestNewClearCmd(t *testing.T) { wantsErr: false, wantsOpts: ClearOptions{ DoConfirm: false, - Dictionaries: []search.DictionaryName{ - search.Plurals, + Dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, }, }, @@ -53,12 +53,8 @@ func TestNewClearCmd(t *testing.T) { tty: true, wantsErr: false, wantsOpts: ClearOptions{ - DoConfirm: true, - Dictionaries: []search.DictionaryName{ - search.Stopwords, - search.Plurals, - search.Compounds, - }, + DoConfirm: true, + Dictionaries: search.AllowedDictionaryTypeEnumValues, }, }, { @@ -68,8 +64,8 @@ func TestNewClearCmd(t *testing.T) { wantsErr: true, wantsOpts: ClearOptions{ DoConfirm: false, - Dictionaries: []search.DictionaryName{ - search.Plurals, + Dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, }, }, @@ -126,7 +122,7 @@ func Test_runDeleteCmd(t *testing.T) { tests := []struct { name string cli string - dictionaries []search.DictionaryName + dictionaries []search.DictionaryType entries bool isTTY bool wantOut string @@ -134,8 +130,8 @@ func Test_runDeleteCmd(t *testing.T) { { name: "one dictionary", cli: "plurals --confirm", - dictionaries: []search.DictionaryName{ - search.Plurals, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, entries: true, isTTY: false, @@ -144,31 +140,27 @@ func Test_runDeleteCmd(t *testing.T) { { name: "multiple dictionaries", cli: "plurals compounds --confirm", - dictionaries: []search.DictionaryName{ - search.Plurals, - search.Compounds, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, + search.DICTIONARY_TYPE_COMPOUNDS, }, entries: true, isTTY: false, wantOut: "", }, { - name: "all dictionaries", - cli: "--all --confirm", - dictionaries: []search.DictionaryName{ - search.Stopwords, - search.Plurals, - search.Compounds, - }, - entries: true, - isTTY: false, - wantOut: "", + name: "all dictionaries", + cli: "--all --confirm", + dictionaries: search.AllowedDictionaryTypeEnumValues, + entries: true, + isTTY: false, + wantOut: "", }, { name: "no entries", cli: "plurals --confirm", - dictionaries: []search.DictionaryName{ - search.Plurals, + dictionaries: []search.DictionaryType{ + search.DICTIONARY_TYPE_PLURALS, }, entries: false, isTTY: false, @@ -180,14 +172,28 @@ func Test_runDeleteCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} for _, d := range tt.dictionaries { - var entries []DictionaryEntry + var entries []search.DictionaryEntry if tt.entries { - entries = append(entries, DictionaryEntry{Type: "custom"}) + entries = append( + entries, + search.DictionaryEntry{ + ObjectID: "1", + Language: search.SUPPORTED_LANGUAGE_EN.Ptr(), + Words: []string{"foo", "bar"}, + Type: search.DICTIONARY_ENTRY_TYPE_CUSTOM.Ptr(), + }, + ) } - r.Register(httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/search", d)), httpmock.JSONResponse(search.SearchDictionariesRes{ - Hits: entries, - })) - r.Register(httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/batch", d)), httpmock.JSONResponse(search.TaskStatusRes{})) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/search", d)), + httpmock.JSONResponse(search.SearchDictionaryEntriesResponse{ + Hits: entries, + }), + ) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/batch", d)), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) } f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/dictionary/entries/delete/delete.go b/pkg/cmd/dictionary/entries/delete/delete.go index 81e8f425..4c669f6a 100644 --- a/pkg/cmd/dictionary/entries/delete/delete.go +++ b/pkg/cmd/dictionary/entries/delete/delete.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmd/dictionary/shared" @@ -19,10 +19,10 @@ type DeleteOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Dictionary search.DictionaryName - ObjectIDs []string + DictionaryType search.DictionaryType + ObjectIDs []string DoConfirm bool } @@ -39,9 +39,9 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "delete --object-ids [--confirm]", Args: validators.ExactArgs(1), - ValidArgs: shared.DictionaryNames(), + ValidArgs: shared.DictionaryTypes(), ValidArgsFunction: func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { - return shared.DictionaryNames(), cobra.ShellCompDirectiveNoFileComp + return shared.DictionaryTypes(), cobra.ShellCompDirectiveNoFileComp }, Annotations: map[string]string{ "acls": "settings,editSettings", @@ -59,10 +59,16 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Dictionary = search.DictionaryName(args[0]) + d, err := search.NewDictionaryTypeFromValue(args[0]) + if err != nil { + return err + } + opts.DictionaryType = *d if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -78,7 +84,8 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd.Flags().StringSliceVarP(&opts.ObjectIDs, "object-ids", "", nil, "Object IDs to delete") _ = cmd.MarkFlagRequired("object-ids") - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete dictionary entry confirmation prompt") + cmd.Flags(). + BoolVarP(&confirm, "confirm", "y", false, "Skip the delete dictionary entry confirmation prompt") return cmd } @@ -92,7 +99,14 @@ func runDeleteCmd(opts *DeleteOptions) error { if opts.DoConfirm { var confirmed bool - err = prompt.Confirm(fmt.Sprintf("Delete the %s from %s?", pluralizeEntry(len(opts.ObjectIDs)), opts.Dictionary), &confirmed) + err = prompt.Confirm( + fmt.Sprintf( + "Delete the %s from %s?", + pluralizeEntry(len(opts.ObjectIDs)), + opts.DictionaryType, + ), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -101,14 +115,34 @@ func runDeleteCmd(opts *DeleteOptions) error { } } - _, err = client.DeleteDictionaryEntries(opts.Dictionary, opts.ObjectIDs) + var requests []search.BatchDictionaryEntriesRequest + for _, id := range opts.ObjectIDs { + req := search.NewBatchDictionaryEntriesRequest( + search.DICTIONARY_ACTION_DELETE_ENTRY, + *search.NewDictionaryEntry(id), + ) + requests = append(requests, *req) + } + + _, err = client.BatchDictionaryEntries( + client.NewApiBatchDictionaryEntriesRequest( + opts.DictionaryType, + search.NewBatchDictionaryEntriesParams(requests), + ), + ) if err != nil { return err } cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully deleted %s from %s\n", cs.SuccessIcon(), pluralizeEntry(len(opts.ObjectIDs)), opts.Dictionary) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully deleted %s from %s\n", + cs.SuccessIcon(), + pluralizeEntry(len(opts.ObjectIDs)), + opts.DictionaryType, + ) } return nil diff --git a/pkg/cmd/dictionary/entries/delete/delete_test.go b/pkg/cmd/dictionary/entries/delete/delete_test.go index 812c6a42..a00939cf 100644 --- a/pkg/cmd/dictionary/entries/delete/delete_test.go +++ b/pkg/cmd/dictionary/entries/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,18 +25,18 @@ func TestNewDeleteCmd(t *testing.T) { }{ { name: "no --confirm without tty", - cli: "plural --object-ids 1", + cli: "plurals --object-ids 1", tty: false, wantsErr: true, }, { name: "--confirm without tty", - cli: "plural --object-ids 1 --confirm", + cli: "plurals --object-ids 1 --confirm", tty: true, wantsErr: false, wantsOpts: DeleteOptions{ - DoConfirm: false, - Dictionary: "plural", + DoConfirm: false, + DictionaryType: search.DICTIONARY_TYPE_PLURALS, ObjectIDs: []string{ "1", }, @@ -44,12 +44,12 @@ func TestNewDeleteCmd(t *testing.T) { }, { name: "no --confirm with tty", - cli: "plural --object-ids 1", + cli: "plurals --object-ids 1", tty: true, wantsErr: false, wantsOpts: DeleteOptions{ - DoConfirm: true, - Dictionary: "plural", + DoConfirm: true, + DictionaryType: search.DICTIONARY_TYPE_PLURALS, ObjectIDs: []string{ "1", }, @@ -57,12 +57,12 @@ func TestNewDeleteCmd(t *testing.T) { }, { name: "multiple --object-ids", - cli: "plural --object-ids 1,2 --confirm", + cli: "plurals --object-ids 1,2 --confirm", tty: false, wantsErr: false, wantsOpts: DeleteOptions{ - DoConfirm: false, - Dictionary: "plural", + DoConfirm: false, + DictionaryType: search.DICTIONARY_TYPE_PLURALS, ObjectIDs: []string{ "1", "2", @@ -103,7 +103,7 @@ func TestNewDeleteCmd(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - assert.Equal(t, tt.wantsOpts.Dictionary, opts.Dictionary) + assert.Equal(t, tt.wantsOpts.DictionaryType, opts.DictionaryType) assert.Equal(t, tt.wantsOpts.ObjectIDs, opts.ObjectIDs) assert.Equal(t, tt.wantsOpts.DoConfirm, opts.DoConfirm) }) @@ -114,15 +114,15 @@ func Test_runDeleteCmd(t *testing.T) { tests := []struct { name string cli string - dictionary string + dictionary search.DictionaryType objectIDs []string isTTY bool wantOut string }{ { name: "single object-id, no TTY", - cli: "plural --object-ids 1 --confirm", - dictionary: "plural", + cli: "plurals --object-ids 1 --confirm", + dictionary: search.DICTIONARY_TYPE_PLURALS, objectIDs: []string{ "1", }, @@ -131,24 +131,24 @@ func Test_runDeleteCmd(t *testing.T) { }, { name: "single object-id, TTY", - cli: "plural --object-ids 1 --confirm", - dictionary: "plural", + cli: "plurals --object-ids 1 --confirm", + dictionary: search.DICTIONARY_TYPE_PLURALS, objectIDs: []string{ "1", }, isTTY: true, - wantOut: "✓ Successfully deleted 1 entry from plural\n", + wantOut: "✓ Successfully deleted 1 entry from plurals\n", }, { name: "multiple object-ids, TTY", - cli: "plural --object-ids 1,2 --confirm", - dictionary: "plural", + cli: "plurals --object-ids 1,2 --confirm", + dictionary: search.DICTIONARY_TYPE_PLURALS, objectIDs: []string{ "1", "2", }, isTTY: true, - wantOut: "✓ Successfully deleted 2 entries from plural\n", + wantOut: "✓ Successfully deleted 2 entries from plurals\n", }, } @@ -158,9 +158,18 @@ func Test_runDeleteCmd(t *testing.T) { // test is flaky since there's no guarantee of obtaining the right object using a search by objectID for _, id := range tt.objectIDs { - r.Register(httpmock.REST("GET", fmt.Sprintf("1/dictionaries/%s/search?query=%s", tt.dictionary, id)), httpmock.JSONResponse(search.SearchDictionariesRes{})) + r.Register( + httpmock.REST( + "GET", + fmt.Sprintf("1/dictionaries/%s/search?query=%s", tt.dictionary, id), + ), + httpmock.JSONResponse(search.SearchDictionaryEntriesResponse{}), + ) } - r.Register(httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/batch", tt.dictionary)), httpmock.JSONResponse(search.TaskStatusRes{})) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/dictionaries/%s/batch", tt.dictionary)), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) f, out := test.NewFactory(tt.isTTY, &r, nil, "") cmd := NewDeleteCmd(f, nil) diff --git a/pkg/cmd/dictionary/entries/import/import.go b/pkg/cmd/dictionary/entries/import/import.go index c2b249a4..87221959 100644 --- a/pkg/cmd/dictionary/entries/import/import.go +++ b/pkg/cmd/dictionary/entries/import/import.go @@ -8,7 +8,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmd/dictionary/shared" @@ -26,11 +26,10 @@ type ImportOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - DictionaryName string - CreateIfNotExists bool - Wait bool + DictionaryType search.DictionaryType + Wait bool File string Scanner *bufio.Scanner @@ -49,7 +48,7 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co cmd := &cobra.Command{ Use: "import -F [--wait] [--continue-on-errors]", Args: validators.ExactArgs(1), - ValidArgs: shared.DictionaryNames(), + ValidArgs: shared.DictionaryTypes(), Annotations: map[string]string{ "acls": "settings,editSettings", }, @@ -67,7 +66,11 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co $ algolia dictionary import plurals -F entries.ndjson --continue-on-errors `), RunE: func(cmd *cobra.Command, args []string) error { - opts.DictionaryName = args[0] + d, err := search.NewDictionaryTypeFromValue(args[0]) + if err != nil { + return err + } + opts.DictionaryType = *d scanner, err := cmdutil.ScanFile(opts.File, opts.IO.In) if err != nil { @@ -83,11 +86,14 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co }, } - cmd.Flags().StringVarP(&opts.File, "file", "F", "", "Read entries to import from `file` (use \"-\" to read from standard input)") + cmd.Flags(). + StringVarP(&opts.File, "file", "F", "", "Read entries to import from `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete before returning") - cmd.Flags().BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue importing entries even if some entries are invalid.") + cmd.Flags(). + BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete before returning") + cmd.Flags(). + BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue importing entries even if some entries are invalid.") return cmd } @@ -101,7 +107,7 @@ func runImportCmd(opts *ImportOptions) error { cs := opts.IO.ColorScheme() var ( - entries []search.DictionaryEntry + entries []*search.DictionaryEntry currentLine = 0 totalEntries = 0 ) @@ -121,19 +127,17 @@ func runImportCmd(opts *ImportOptions) error { totalEntries++ opts.IO.UpdateProgressIndicatorLabel(fmt.Sprintf("Read entries from %s", opts.File)) - var entry shared.DictionaryEntry + var entry search.DictionaryEntry + if err := json.Unmarshal([]byte(line), &entry); err != nil { - err := fmt.Errorf("line %d: %s", currentLine, err) - errors = append(errors, err.Error()) - continue - } - err := ValidateDictionaryEntry(entry, currentLine) - if err != nil { - errors = append(errors, err.Error()) + errors = append(errors, fmt.Errorf("line %d: %s", currentLine, err).Error()) continue } - dictionaryEntry, err := createDictionaryEntry(opts.DictionaryName, entry) + fmt.Printf("TYPE: %v\n", opts.DictionaryType) + fmt.Printf("ENTRY: %v\n", entry) + + dictionaryEntry, err := createDictionaryEntry(opts.DictionaryType, entry) if err != nil { errors = append(errors, fmt.Errorf("line %d: %s", currentLine, err.Error()).Error()) continue @@ -155,7 +159,7 @@ func runImportCmd(opts *ImportOptions) error { // No entries found if len(entries) == 0 { if len(errors) > 0 { - return fmt.Errorf(errorMsg) + return fmt.Errorf("%s", errorMsg) } return fmt.Errorf("%s No entries found in the file", cs.FailureIcon()) } @@ -177,9 +181,27 @@ func runImportCmd(opts *ImportOptions) error { } // Import entries - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating %s entries on %s", cs.Bold(fmt.Sprint(len(entries))), cs.Bold(opts.DictionaryName))) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf( + "Updating %s entries on %s", + cs.Bold(fmt.Sprint(len(entries))), + cs.Bold(string(opts.DictionaryType)), + ), + ) - res, err := client.SaveDictionaryEntries(search.DictionaryName(opts.DictionaryName), entries) + var requests []search.BatchDictionaryEntriesRequest + for _, e := range entries { + requests = append( + requests, + *search.NewBatchDictionaryEntriesRequest(search.DICTIONARY_ACTION_ADD_ENTRY, *e), + ) + } + res, err := client.BatchDictionaryEntries( + client.NewApiBatchDictionaryEntriesRequest( + opts.DictionaryType, + search.NewBatchDictionaryEntriesParams(requests), + ), + ) if err != nil { opts.IO.StopProgressIndicator() return err @@ -188,40 +210,64 @@ func runImportCmd(opts *ImportOptions) error { // Wait for the operation to complete if requested if opts.Wait { opts.IO.UpdateProgressIndicatorLabel("Waiting for operation to complete") - if err := res.Wait(); err != nil { + if _, err := client.WaitForAppTask(res.TaskID); err != nil { opts.IO.StopProgressIndicator() return err } } opts.IO.StopProgressIndicator() - _, err = fmt.Fprintf(opts.IO.Out, "%s Successfully imported %s entries on %s in %v\n", cs.SuccessIcon(), cs.Bold(fmt.Sprint(len(entries))), cs.Bold(opts.DictionaryName), time.Since(elapsed)) + _, err = fmt.Fprintf( + opts.IO.Out, + "%s Successfully imported %s entries on %s in %v\n", + cs.SuccessIcon(), + cs.Bold(fmt.Sprint(len(entries))), + cs.Bold(string(opts.DictionaryType)), + time.Since(elapsed), + ) return err } -func ValidateDictionaryEntry(entry shared.DictionaryEntry, currentLine int) error { +func createDictionaryEntry( + dictionaryType search.DictionaryType, + entry search.DictionaryEntry, +) (*search.DictionaryEntry, error) { if entry.ObjectID == "" { - return fmt.Errorf("line %d: objectID is missing", currentLine) - } - if entry.Word == "" { - return fmt.Errorf("line %d: word is missing", currentLine) - } - if entry.Language == "" { - return fmt.Errorf("line %d: language is missing", currentLine) + return nil, fmt.Errorf("objectID is missing") } - - return nil -} - -func createDictionaryEntry(dictionaryName string, entry shared.DictionaryEntry) (search.DictionaryEntry, error) { - switch dictionaryName { - case string(search.Plurals): - return search.NewPlural(entry.ObjectID, entry.Language, entry.Words), nil - case string(search.Compounds): - return search.NewCompound(entry.ObjectID, entry.Language, entry.Word, entry.Decomposition), nil - case string(search.Stopwords): - return search.NewStopword(entry.ObjectID, entry.Language, entry.Word, entry.State), nil + switch dictionaryType { + case search.DICTIONARY_TYPE_PLURALS: + if len(entry.Words) == 0 { + return nil, fmt.Errorf("words is missing") + } + if entry.Language == nil { + return nil, fmt.Errorf("language is missing") + } + return search.NewDictionaryEntry( + entry.ObjectID, + search.WithDictionaryEntryLanguage(*entry.Language), + search.WithDictionaryEntryWords(entry.Words), + ), nil + case search.DICTIONARY_TYPE_COMPOUNDS: + if entry.Word == nil { + return nil, fmt.Errorf("word is missing") + } + return search.NewDictionaryEntry( + entry.ObjectID, + search.WithDictionaryEntryLanguage(*entry.Language), + search.WithDictionaryEntryWord(*entry.Word), + search.WithDictionaryEntryDecomposition(entry.Decomposition), + ), nil + case search.DICTIONARY_TYPE_STOPWORDS: + if entry.Word == nil { + return nil, fmt.Errorf("word is missing") + } + return search.NewDictionaryEntry( + entry.ObjectID, + search.WithDictionaryEntryLanguage(*entry.Language), + search.WithDictionaryEntryWord(*entry.Word), + ), nil } - return nil, fmt.Errorf("Wrong dictionary name") + return nil, fmt.Errorf("wrong dictionary name") } diff --git a/pkg/cmd/dictionary/entries/import/import_test.go b/pkg/cmd/dictionary/entries/import/import_test.go index 089461d4..0f283fcb 100644 --- a/pkg/cmd/dictionary/entries/import/import_test.go +++ b/pkg/cmd/dictionary/entries/import/import_test.go @@ -6,18 +6,23 @@ import ( "path/filepath" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" - "github.com/algolia/cli/pkg/cmd/dictionary/shared" "github.com/algolia/cli/pkg/httpmock" "github.com/algolia/cli/test" ) func Test_runImportCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "entries.json") - err := os.WriteFile(tmpFile, []byte(`{"language":"en","word":"test","state":"enabled","objectID":"test","type":"custom"}`), 0600) + err := os.WriteFile( + tmpFile, + []byte( + `{"language":"en","word":"test","objectID":"test"}`, + ), + 0o600, + ) require.NoError(t, err) tests := []struct { @@ -30,7 +35,7 @@ func Test_runImportCmd(t *testing.T) { { name: "from stdin", cli: "stopwords -F -", - stdin: `{"language":"en","word":"test","state":"enabled","objectID":"test","type":"custom"}`, + stdin: `{"language":"en","word":"test","objectID":"test"}`, wantOut: "✓ Successfully imported 1 entries on stopwords in", }, { @@ -41,14 +46,14 @@ func Test_runImportCmd(t *testing.T) { { name: "from stdin with invalid JSON", cli: "stopwords -F -", - stdin: `{"language":"en","word":"test","state":"enabled","type":"custom"}`, + stdin: `{"language":"en","word":"test"}`, wantErr: "X Found 1 error (out of 1 entries) while parsing the file:\n line 1: objectID is missing\n", }, { name: "from stdin with invalid JSON (multiple operations)", cli: "stopwords -F -", - stdin: `{"word":"test","state":"enabled","objectID":"test","type":"custom"}, - {"language":"fr","state":"enabled","objectID":"testFr","type":"custom"}`, + stdin: `{"word":"test","objectID":"test"}, + {"language":"fr","objectID":"testFr"}`, wantErr: "X Found 2 errors (out of 2 entries) while parsing the file:\n line 1: invalid character ',' after top-level value\n line 2: word is missing\n", }, { @@ -60,8 +65,8 @@ func Test_runImportCmd(t *testing.T) { { name: "from stdin with invalid JSON (2 entries) with --continue-on-error", cli: "stopwords -F - --continue-on-error", - stdin: `{"language":"en","state":"enabled","objectID":"test","type":"custom"} - {"language":"en","word":"test","state":"enabled","type":"custom"}`, + stdin: `{"language":"en","objectID":"test"} + {"language":"en","word":"test"}`, wantErr: "X Found 2 errors (out of 2 entries) while parsing the file:\n line 1: word is missing\n line 2: objectID is missing\n", }, { @@ -80,7 +85,10 @@ func Test_runImportCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} if tt.wantErr == "" { - r.Register(httpmock.REST("POST", "1/dictionaries/stopwords/batch"), httpmock.JSONResponse(search.MultipleBatchRes{})) + r.Register( + httpmock.REST("POST", "1/dictionaries/stopwords/batch"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) } defer r.Verify(t) @@ -96,56 +104,3 @@ func Test_runImportCmd(t *testing.T) { }) } } - -func Test_ValidateDictionaryEntry(t *testing.T) { - tests := []struct { - name string - entry shared.DictionaryEntry - currentLine int - wantErr bool - wantErrMsg string - }{ - { - name: "no objectID", - entry: shared.DictionaryEntry{ - Word: "test", - Language: "en", - }, - currentLine: 1, - wantErr: true, - wantErrMsg: "line 1: objectID is missing", - }, - { - name: "no word", - entry: shared.DictionaryEntry{ - ObjectID: "123", - Language: "en", - }, - currentLine: 1, - wantErr: true, - wantErrMsg: "line 1: word is missing", - }, - { - name: "no language", - entry: shared.DictionaryEntry{ - ObjectID: "123", - Word: "test", - }, - currentLine: 1, - wantErr: true, - wantErrMsg: "line 1: language is missing", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidateDictionaryEntry(tt.entry, tt.currentLine) - if tt.wantErr { - assert.Error(t, err) - assert.Equal(t, tt.wantErrMsg, err.Error()) - return - } - assert.NoError(t, err) - }) - } -} diff --git a/pkg/cmd/dictionary/settings/get/get.go b/pkg/cmd/dictionary/settings/get/get.go index 04d5fcc4..39eb7245 100644 --- a/pkg/cmd/dictionary/settings/get/get.go +++ b/pkg/cmd/dictionary/settings/get/get.go @@ -2,7 +2,7 @@ package get import ( "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -14,7 +14,7 @@ type GetOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) PrintFlags *cmdutil.PrintFlags } diff --git a/pkg/cmd/dictionary/settings/set/languages.go b/pkg/cmd/dictionary/settings/set/languages.go index 45425969..2e9116db 100644 --- a/pkg/cmd/dictionary/settings/set/languages.go +++ b/pkg/cmd/dictionary/settings/set/languages.go @@ -1,5 +1,6 @@ package set +// The v4 API client doesn't have the long form of language names var Languages = map[string]string{ "af": "Afrikaans", "sq": "Albanian", diff --git a/pkg/cmd/dictionary/settings/set/set.go b/pkg/cmd/dictionary/settings/set/set.go index 96417baa..ed29d3cc 100644 --- a/pkg/cmd/dictionary/settings/set/set.go +++ b/pkg/cmd/dictionary/settings/set/set.go @@ -4,8 +4,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -17,7 +16,7 @@ type SetOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) DisableStandardEntries []string EnableStandardEntries []string @@ -58,20 +57,29 @@ func NewSetCmd(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command `), RunE: func(cmd *cobra.Command, args []string) error { // Check that either --disable-standard-entries and --enable-standard-entries or --reset-standard-entries is set - if !opts.ResetStandardEntries && (len(opts.DisableStandardEntries) == 0 && len(opts.EnableStandardEntries) == 0) { - return cmdutil.FlagErrorf("Either --disable-standard-entries and/or --enable-standard-entries or --reset-standard-entries must be set") + if !opts.ResetStandardEntries && + (len(opts.DisableStandardEntries) == 0 && len(opts.EnableStandardEntries) == 0) { + return cmdutil.FlagErrorf( + "Either --disable-standard-entries and/or --enable-standard-entries or --reset-standard-entries must be set", + ) } - // Check that the user isn't resetting standard entries and trying to turn standard entries on or off at the same time - if opts.ResetStandardEntries && (len(opts.DisableStandardEntries) > 0 || len(opts.EnableStandardEntries) > 0) { - return cmdutil.FlagErrorf("You cannot reset standard entries and disable or enable standard entries at the same time") + // Check that the user is not resetting standard entries and trying to disable or enable standard entries at the same time + if opts.ResetStandardEntries && + (len(opts.DisableStandardEntries) > 0 || len(opts.EnableStandardEntries) > 0) { + return cmdutil.FlagErrorf( + "You cannot reset standard entries and disable or enable standard entries at the same time", + ) } // Check if the user is trying to turn standard entries on or off for the same languages at the same time for _, disableLanguage := range opts.DisableStandardEntries { for _, enableLanguage := range opts.EnableStandardEntries { if disableLanguage == enableLanguage { - return cmdutil.FlagErrorf("You cannot disable and enable standard entries for the same language: %s", disableLanguage) + return cmdutil.FlagErrorf( + "You cannot disable and enable standard entries for the same language: %s", + disableLanguage, + ) } } } @@ -84,16 +92,25 @@ func NewSetCmd(f *cmdutil.Factory, runF func(*SetOptions) error) *cobra.Command }, } - cmd.Flags().StringSliceVarP(&opts.DisableStandardEntries, "disable-standard-entries", "d", []string{}, "Turn off standard entries for these languages") - cmd.Flags().StringSliceVarP(&opts.EnableStandardEntries, "enable-standard-entries", "e", []string{}, "Turn on standard entries for these languages") - cmd.Flags().BoolVarP(&opts.ResetStandardEntries, "reset-standard-entries", "r", false, "Reset standard entries to their default values") + cmd.Flags(). + StringSliceVarP(&opts.DisableStandardEntries, "disable-standard-entries", "d", []string{}, "Disable standard entries for the given languages") + cmd.Flags(). + StringSliceVarP(&opts.EnableStandardEntries, "enable-standard-entries", "e", []string{}, "Enable standard entries for the given languages") + cmd.Flags(). + BoolVarP(&opts.ResetStandardEntries, "reset-standard-entries", "r", false, "Reset standard entries to their default values") - SupportedLanguages := make(map[string]string, len(LanguagesWithStopwordsSupport)) + supportedLanguages := make(map[string]string, len(LanguagesWithStopwordsSupport)) for _, languageCode := range LanguagesWithStopwordsSupport { - SupportedLanguages[languageCode] = Languages[languageCode] + supportedLanguages[languageCode] = Languages[languageCode] } - _ = cmd.RegisterFlagCompletionFunc("disable-standard-entries", cmdutil.StringCompletionFunc(SupportedLanguages)) - _ = cmd.RegisterFlagCompletionFunc("enable-standard-entries", cmdutil.StringCompletionFunc(SupportedLanguages)) + _ = cmd.RegisterFlagCompletionFunc( + "disable-standard-entries", + cmdutil.StringCompletionFunc(supportedLanguages), + ) + _ = cmd.RegisterFlagCompletionFunc( + "enable-standard-entries", + cmdutil.StringCompletionFunc(supportedLanguages), + ) return cmd } @@ -105,35 +122,36 @@ func runSetCmd(opts *SetOptions) error { return err } - var disableStandardEntriesOpt *opt.DisableStandardEntriesOption + var disableStandardEntriesOpt search.StandardEntries if opts.ResetStandardEntries { - disableStandardEntriesOpt = opt.DisableStandardEntries(map[string]map[string]bool{"stopwords": nil}) + disableStandardEntriesOpt = *search.NewEmptyStandardEntries().SetStopwords(nil) } + stopwords := make(map[string]bool, len(LanguagesWithStopwordsSupport)) if len(opts.DisableStandardEntries) > 0 || len(opts.EnableStandardEntries) > 0 { - stopwords := map[string]map[string]bool{"stopwords": {}} for _, language := range opts.DisableStandardEntries { - stopwords["stopwords"][language] = true + stopwords[language] = true } for _, language := range opts.EnableStandardEntries { - stopwords["stopwords"][language] = false + stopwords[language] = false } - disableStandardEntriesOpt = opt.DisableStandardEntries(stopwords) - } - - dictionarySettings := search.DictionarySettings{ - DisableStandardEntries: disableStandardEntriesOpt, + disableStandardEntriesOpt = *search.NewEmptyStandardEntries().SetStopwords(stopwords) } opts.IO.StartProgressIndicatorWithLabel("Updating dictionary settings") - res, err := client.SetDictionarySettings(dictionarySettings) + + res, err := client.SetDictionarySettings( + client.NewApiSetDictionarySettingsRequest( + search.NewDictionarySettingsParams(disableStandardEntriesOpt), + ), + ) if err != nil { opts.IO.StopProgressIndicator() return err } // Wait for the task to complete (so if the user runs `algolia dictionary settings get` right after, the settings will be updated) - err = res.Wait() + _, err = client.WaitForAppTask(res.TaskID) if err != nil { opts.IO.StopProgressIndicator() return err diff --git a/pkg/cmd/dictionary/shared/constants.go b/pkg/cmd/dictionary/shared/constants.go index 7a30f792..2684063a 100644 --- a/pkg/cmd/dictionary/shared/constants.go +++ b/pkg/cmd/dictionary/shared/constants.go @@ -1,39 +1,12 @@ package shared -import ( - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" -) +import "github.com/algolia/algoliasearch-client-go/v4/algolia/search" -// EntryType represents the type of an entry in a dictionary. -// It can be either a custom entry or a standard entry. -type EntryType string -type DictionaryType int - -// DictionaryEntry can be plural, compound or stopword entry. -type DictionaryEntry struct { - Type EntryType - Word string `json:"word,omitempty"` - Words []string `json:"words,omitempty"` - Decomposition []string `json:"decomposition,omitempty"` - ObjectID string - Language string - State string -} - -const ( - // CustomEntryType is the type of a custom entry in a dictionary (i.e. added by the user). - CustomEntryType EntryType = "custom" - // StandardEntryType is the type of a standard entry in a dictionary (i.e. added by Algolia). - StandardEntryType EntryType = "standard" -) - -var ( - // DictionaryNames returns the list of available dictionaries. - DictionaryNames = func() []string { - return []string{ - string(search.Stopwords), - string(search.Compounds), - string(search.Plurals), - } +// DictionaryTypes returns the allowed dictionary types as strings +func DictionaryTypes() []string { + var types []string + for _, d := range search.AllowedDictionaryTypeEnumValues { + types = append(types, string(d)) } -) + return types +} diff --git a/pkg/cmd/events/tail/tail.go b/pkg/cmd/events/tail/tail.go index a541fedd..64053ab0 100644 --- a/pkg/cmd/events/tail/tail.go +++ b/pkg/cmd/events/tail/tail.go @@ -9,21 +9,19 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" "github.com/algolia/cli/pkg/cmdutil" "github.com/algolia/cli/pkg/config" "github.com/algolia/cli/pkg/iostreams" "github.com/algolia/cli/pkg/printers" "github.com/algolia/cli/pkg/validators" - _insights "github.com/algolia/algoliasearch-client-go/v3/algolia/insights" - region "github.com/algolia/algoliasearch-client-go/v3/algolia/region" + algoliaInsights "github.com/algolia/algoliasearch-client-go/v4/algolia/insights" "github.com/algolia/cli/api/insights" ) const ( // DefaultRegion is the default region to use. - DefaultRegion = region.US + DefaultRegion = algoliaInsights.US // Interval is the interval between each request to fetch events. Interval = 3 * time.Second @@ -34,20 +32,16 @@ type TailOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) - - Region string - + Region string PrintFlags *cmdutil.PrintFlags } // NewTailCmd returns a new command for tailing events. func NewTailCmd(f *cmdutil.Factory, runF func(*TailOptions) error) *cobra.Command { opts := &TailOptions{ - IO: f.IOStreams, - Config: f.Config, - SearchClient: f.SearchClient, - PrintFlags: cmdutil.NewPrintFlags(), + IO: f.IOStreams, + Config: f.Config, + PrintFlags: cmdutil.NewPrintFlags(), } cmd := &cobra.Command{ Use: "tail", @@ -81,10 +75,11 @@ func NewTailCmd(f *cmdutil.Factory, runF func(*TailOptions) error) *cobra.Comman }, } - cmd.Flags().StringVarP(&opts.Region, "region", "r", string(DefaultRegion), "Region where your analytics data is stored and processed.") + cmd.Flags(). + StringVarP(&opts.Region, "region", "r", string(DefaultRegion), "Region where your analytics data is stored and processed.") _ = cmd.RegisterFlagCompletionFunc("region", cmdutil.StringCompletionFunc(map[string]string{ - string(region.US): "United States", - string(region.DE): "Germany (Europe)", + string(algoliaInsights.US): "United States", + string(algoliaInsights.DE): "Germany (Europe)", })) opts.PrintFlags.AddFlags(cmd) @@ -97,18 +92,16 @@ func runTailCmd(opts *TailOptions) error { if err != nil { return err } - apiKey, err := opts.Config.Profile().GetAdminAPIKey() + apiKey, err := opts.Config.Profile().GetAPIKey() if err != nil { return err } - // We don't use the base insights client because it doesn't support fetching events. - config := _insights.Configuration{ - AppID: appID, - APIKey: apiKey, - Region: region.Region(opts.Region), + // This is the CLIs custom augmented API client + client, err := insights.NewClient(appID, apiKey, algoliaInsights.Region(opts.Region)) + if err != nil { + return err } - insightsClient := insights.NewClientWithConfig(config) var p printers.Printer if opts.PrintFlags.OutputFlagSpecified() && opts.PrintFlags.OutputFormat != nil { @@ -123,7 +116,7 @@ func runTailCmd(opts *TailOptions) error { c := time.Tick(Interval) for t := range c { utc := t.UTC() - events, err := insightsClient.FetchEvents(utc.Add(-1*time.Second), utc, 1000) + events, err := client.GetEvents(utc.Add(-1*time.Second), utc, 1000) if err != nil { if strings.Contains(err.Error(), "The log processing region does not match") { cs := opts.IO.ColorScheme() @@ -164,6 +157,15 @@ func printEvent(io *iostreams.IOStreams, event insights.EventWrapper) error { colorizedStatus = cs.Red(fmt.Sprint(event.Status)) } - _, err := fmt.Fprintf(io.Out, "%s [%s] %s %s [%s] %s\n", cs.Bold(formatedTime), colorizedStatus, event.Event.EventType, cs.Bold(event.Event.Index), event.Event.EventName, event.Event.UserToken) + _, err := fmt.Fprintf( + io.Out, + "%s [%s] %s %s [%s] %s\n", + cs.Bold(formatedTime), + colorizedStatus, + event.Event.EventType, + cs.Bold(event.Event.Index), + event.Event.EventName, + event.Event.UserToken, + ) return err } diff --git a/pkg/cmd/factory/default.go b/pkg/cmd/factory/default.go index 011f0b67..8c3e0065 100644 --- a/pkg/cmd/factory/default.go +++ b/pkg/cmd/factory/default.go @@ -2,8 +2,11 @@ package factory import ( "fmt" + "net/url" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/call" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/transport" "github.com/algolia/cli/api/crawler" "github.com/algolia/cli/pkg/cmdutil" @@ -23,29 +26,46 @@ func New(appVersion string, cfg config.IConfig) *cmdutil.Factory { return f } -func ioStreams(f *cmdutil.Factory) *iostreams.IOStreams { +func ioStreams(_ *cmdutil.Factory) *iostreams.IOStreams { io := iostreams.System() return io } -func searchClient(f *cmdutil.Factory, appVersion string) func() (*search.Client, error) { - return func() (*search.Client, error) { +func searchClient(f *cmdutil.Factory, appVersion string) func() (*search.APIClient, error) { + return func() (*search.APIClient, error) { appID, err := f.Config.Profile().GetApplicationID() if err != nil { return nil, err } - APIKey, err := f.Config.Profile().GetAPIKey() + apiKey, err := f.Config.Profile().GetAPIKey() if err != nil { return nil, err } - clientCfg := search.Configuration{ - AppID: appID, - APIKey: APIKey, - ExtraUserAgent: fmt.Sprintf("Algolia CLI (%s)", appVersion), - Hosts: f.Config.Profile().GetSearchHosts(), + userAgent, err := getUserAgentInfo(appID, apiKey, appVersion) + if err != nil { + return nil, err + } + if userAgent == "" { + return nil, fmt.Errorf("user agent must not be empty") + } + + clientConf := search.SearchConfiguration{ + Configuration: transport.Configuration{ + AppID: appID, + ApiKey: apiKey, + UserAgent: userAgent, + ExposeIntermediateNetworkErrors: true, + }, + } + + // Read custom hosts from flags, environment, or profile, or use default ones + hosts := getStatefulHosts(f.Config.Profile().GetSearchHosts()) + if len(hosts) > 0 { + clientConf.Configuration.Hosts = hosts } - return search.NewClientWithConfig(clientCfg), nil + + return search.NewClientWithConfig(clientConf) } } @@ -63,3 +83,31 @@ func crawlerClient(f *cmdutil.Factory) func() (*crawler.Client, error) { return crawler.NewClient(userID, APIKey), nil } } + +// getUserAgentInfo returns the standard user agent info plus Algolia CLI +func getUserAgentInfo(appID string, apiKey string, appVersion string) (string, error) { + client, err := search.NewClient(appID, apiKey) + if err != nil { + return "", err + } + return client.GetConfiguration().UserAgent + fmt.Sprintf("; Algolia CLI (%s)", appVersion), nil +} + +// getStatefulHosts reads the hosts information from the profile and turns into the right structure +func getStatefulHosts(hosts []string) []transport.StatefulHost { + var out []transport.StatefulHost + for _, host := range hosts { + // User might or might not provide the URL with `https://` + parsedURL, _ := url.Parse(host) + if parsedURL.Scheme == "" { + parsedURL.Scheme = "https" + } + statefulHost := transport.NewStatefulHost( + parsedURL.Scheme, + parsedURL.Host, + call.IsReadWrite, + ) + out = append(out, statefulHost) + } + return out +} diff --git a/pkg/cmd/indices/analyze/analyze.go b/pkg/cmd/indices/analyze/analyze.go index 46cb5d76..35a50c3f 100644 --- a/pkg/cmd/indices/analyze/analyze.go +++ b/pkg/cmd/indices/analyze/analyze.go @@ -1,12 +1,12 @@ package analyze import ( + "encoding/json" "fmt" "sort" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/internal/analyze" @@ -21,10 +21,10 @@ type StatsOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string - BrowseParams map[string]interface{} + Index string + BrowseParams search.BrowseParamsObject NoLimit bool Only string @@ -70,20 +70,33 @@ func NewAnalyzeCmd(f *cmdutil.Factory) *cobra.Command { $ algolia index analyze MOVIES --only actors `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] - browseParams, err := cmdutil.FlagValuesMap(cmd.Flags(), cmdutil.BrowseParamsObject...) + browseParamsMap, err := cmdutil.FlagValuesMap( + cmd.Flags(), + cmdutil.BrowseParamsObject...) + if err != nil { + return err + } + + // Convert map to object + tmp, err := json.Marshal(browseParamsMap) + if err != nil { + return err + } + err = json.Unmarshal(tmp, &opts.BrowseParams) if err != nil { return err } - opts.BrowseParams = browseParams return runAnalyzeCmd(opts) }, } - cmd.Flags().BoolVarP(&opts.NoLimit, "no-limit", "n", false, "If set, the command will not limit the number of objects to analyze. Otherwise, the default limit is 1000 objects.") - cmd.Flags().StringVarP(&opts.Only, "only", "", "", "If set, the command will only analyze the specified attribute. Chosen attribute values statistics will be shown in the output.") + cmd.Flags(). + BoolVarP(&opts.NoLimit, "no-limit", "n", false, "If set, the command will not limit the number of objects to analyze. Otherwise, the default limit is 1000 objects.") + cmd.Flags(). + StringVarP(&opts.Only, "only", "", "", "If set, the command will only analyze the specified attribute. Chosen attribute values statistics will be shown in the output.") cmdutil.AddBrowseParamsObjectFlags(cmd) opts.PrintFlags.AddFlags(cmd) @@ -98,54 +111,70 @@ func runAnalyzeCmd(opts *StatsOptions) error { return err } - indice := client.InitIndex(opts.Indice) + var records []search.Hit + counter := 1 - // We use the `opt.ExtraOptions` to pass the `SearchParams` to the API. - query, ok := opts.BrowseParams["query"].(string) - if !ok { - query = "" - } else { - delete(opts.BrowseParams, "query") - } - - // If no-limit flag is passed, count the number of objects in the index - count := 1000 - limit := 1000 + // With `--no-limit`, we iterate over all records if opts.NoLimit { - limit = 0 - res, err := indice.Search("", opt.HitsPerPage(0)) + // Get the number of records (just for printing; maybe we can avoid this?) + res, err := client.SearchSingleIndex( + client.NewApiSearchSingleIndexRequest(opts.Index). + WithSearchParams(search.SearchParamsObjectAsSearchParams(search.NewEmptySearchParamsObject().SetQuery("").SetHitsPerPage(0))), + ) if err != nil { return err } - count = res.NbHits - } - - io.StartProgressIndicatorWithLabel(fmt.Sprintf("Analyzing %d/%d objects", limit, count)) - - // Chan to receive the object count - counter := make(chan int) - go func() { - var total int - for c := range counter { - total += c - io.UpdateProgressIndicatorLabel(fmt.Sprintf("Analyzing %d/%d objects", total, count)) + limit := int(*res.NbHits) + io.StartProgressIndicatorWithLabel(fmt.Sprintf("Analyzing 0/%d objects", limit)) + err = client.BrowseObjects( + opts.Index, + opts.BrowseParams, + search.WithAggregator(func(response any, err error) { + if err != nil { + return + } + for _, hit := range response.(*search.BrowseResponse).Hits { + io.UpdateProgressIndicatorLabel( + fmt.Sprintf("Analyzing %d/%d objects", counter, limit), + ) + records = append(records, hit) + counter++ + } + }), + ) + if err != nil { + io.StopProgressIndicator() + return err + } + } else { + // Without `--no-limit`, just iterate over the first 1,000 records. + // Since `BrowseObjects` can't be terminated early, we use the regular Browse method here. + res, err := client.Browse( + client. + NewApiBrowseRequest(opts.Index). + WithBrowseParams(search.BrowseParamsObjectAsBrowseParams(&opts.BrowseParams)), + ) + limit := int(*res.NbHits) + io.StartProgressIndicatorWithLabel(fmt.Sprintf("Analyzing 0/%d objects", limit)) + if err != nil { + return err + } + for _, hit := range res.Hits { + io.UpdateProgressIndicatorLabel( + fmt.Sprintf("Analyzing %d/%d objects", counter, limit), + ) + records = append(records, hit) + counter++ } - close(counter) - }() - - res, err := indice.BrowseObjects(opt.Query(query), opt.ExtraOptions(opts.BrowseParams)) - if err != nil { - io.StopProgressIndicator() - return err } - settings, err := indice.GetSettings() + settings, err := client.GetSettings(client.NewApiGetSettingsRequest(opts.Index)) if err != nil { io.StopProgressIndicator() return err } - stats, err := analyze.ComputeStats(res, settings, limit, opts.Only, counter) + stats, err := analyze.ComputeStats(records, *settings, opts.Only) if err != nil { io.StopProgressIndicator() return err @@ -222,7 +251,7 @@ func printStats(stats *analyze.Stats, opts *StatsOptions) error { for _, key := range sorted { // Print colorized output depending on the percentage // If <1%: red, if <5%: yellow - var color = func(s string) string { return s } + color := func(s string) string { return s } if stats.Attributes[key].Percentage < 1 { color = cs.Red } else if stats.Attributes[key].Percentage < 5 { @@ -265,7 +294,11 @@ func printSingleAttributeStats(stats *analyze.Stats, opts *StatsOptions) error { for _, v := range sorted { table.AddField(fmt.Sprintf("%v", v), nil, nil) table.AddField(fmt.Sprintf("%d", value.Values[v]), nil, nil) - table.AddField(fmt.Sprintf("%.2f%%", float64(value.Values[v])*100/float64(stats.TotalRecords)), nil, nil) + table.AddField( + fmt.Sprintf("%.2f%%", float64(value.Values[v])*100/float64(stats.TotalRecords)), + nil, + nil, + ) table.EndRow() } } diff --git a/pkg/cmd/indices/clear/clear.go b/pkg/cmd/indices/clear/clear.go index c91e08cf..8cf7fddc 100644 --- a/pkg/cmd/indices/clear/clear.go +++ b/pkg/cmd/indices/clear/clear.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -18,10 +18,11 @@ type ClearOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) Index string DoConfirm bool + Wait bool } // NewClearCmd creates and returns a clear command for indices @@ -54,7 +55,9 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -67,7 +70,8 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the clear index confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip confirmation prompt") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") return cmd } @@ -75,7 +79,10 @@ func NewClearCmd(f *cmdutil.Factory, runF func(*ClearOptions) error) *cobra.Comm func runClearCmd(opts *ClearOptions) error { if opts.DoConfirm { var confirmed bool - err := prompt.Confirm(fmt.Sprintf("Are you sure you want to clear the index %q?", opts.Index), &confirmed) + err := prompt.Confirm( + fmt.Sprintf("Are you sure you want to clear the index %q?", opts.Index), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -89,10 +96,26 @@ func runClearCmd(opts *ClearOptions) error { return err } - if _, err := client.InitIndex(opts.Index).ClearObjects(); err != nil { + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Deleting all records from index %s", opts.Index), + ) + res, err := client.ClearObjects(client.NewApiClearObjectsRequest(opts.Index)) + if err != nil { + opts.IO.StopProgressIndicator() return err } + if opts.Wait { + opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } + + opts.IO.StopProgressIndicator() + cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Cleared index %s\n", cs.SuccessIcon(), opts.Index) diff --git a/pkg/cmd/indices/clear/clear_test.go b/pkg/cmd/indices/clear/clear_test.go index 266f5a56..b689ce9b 100644 --- a/pkg/cmd/indices/clear/clear_test.go +++ b/pkg/cmd/indices/clear/clear_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -107,7 +107,10 @@ func Test_runCreateCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/clear", tt.index)), httpmock.JSONResponse(search.CreateKeyRes{Key: "foo"})) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/clear", tt.index)), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) defer r.Verify(t) f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/indices/config/config.go b/pkg/cmd/indices/config/config.go index bf906275..a733fcee 100644 --- a/pkg/cmd/indices/config/config.go +++ b/pkg/cmd/indices/config/config.go @@ -8,7 +8,7 @@ import ( "github.com/algolia/cli/pkg/cmdutil" ) -// NewConfigCmd returns a new command for indice config management +// NewConfigCmd returns a new command for index config management func NewConfigCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "config", diff --git a/pkg/cmd/indices/config/export/export.go b/pkg/cmd/indices/config/export/export.go index 3104fa92..6ad890a9 100644 --- a/pkg/cmd/indices/config/export/export.go +++ b/pkg/cmd/indices/config/export/export.go @@ -8,7 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" - indiceConfig "github.com/algolia/cli/pkg/cmd/shared/config" + indexConfig "github.com/algolia/cli/pkg/cmd/shared/config" "github.com/algolia/cli/pkg/cmd/shared/handler" config "github.com/algolia/cli/pkg/cmd/shared/handler/indices" "github.com/algolia/cli/pkg/cmdutil" @@ -47,21 +47,21 @@ func NewExportCmd(f *cmdutil.Factory) *cobra.Command { $ algolia index config export MOVIES --directory exports `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] client, err := opts.SearchClient() if err != nil { return err } - existingIndices, err := client.ListIndices() + listResponse, err := client.ListIndices(client.NewApiListIndicesRequest()) if err != nil { return err } - var availableIndicesNames []string - for _, currentIndexName := range existingIndices.Items { - availableIndicesNames = append(availableIndicesNames, currentIndexName.Name) + var indices []string + for _, i := range listResponse.Items { + indices = append(indices, i.Name) } - opts.ExistingIndices = availableIndicesNames + opts.Indices = indices exportConfigHandler := &handler.IndexConfigExportHandler{ Opts: opts, } @@ -75,9 +75,11 @@ func NewExportCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().StringVarP(&opts.Directory, "directory", "d", "", "Directory path of the output file (default: current folder)") + cmd.Flags(). + StringVarP(&opts.Directory, "directory", "d", "", "Directory path of the output file (default: current folder)") _ = cmd.MarkFlagDirname("directory") - cmd.Flags().StringSliceVarP(&opts.Scope, "scope", "s", []string{"settings", "synonyms", "rules"}, "Scope to export (default: all)") + cmd.Flags(). + StringSliceVarP(&opts.Scope, "scope", "s", []string{"settings", "synonyms", "rules"}, "Scope to export (default: all)") _ = cmd.RegisterFlagCompletionFunc("scope", cmdutil.StringSliceCompletionFunc(map[string]string{ "settings": "settings", @@ -95,19 +97,27 @@ func runExportCmd(opts *config.ExportOptions) error { return err } - indice := client.InitIndex(opts.Indice) - configJson, err := indiceConfig.GetIndiceConfig(indice, opts.Scope, cs) + configJSON, err := indexConfig.GetIndexConfig(client, opts.Index, opts.Scope, cs) if err != nil { return err } - configJsonIndented, err := json.MarshalIndent(configJson, "", " ") + configJSONIndented, err := json.MarshalIndent(configJSON, "", " ") if err != nil { - return fmt.Errorf("%s An error occurred when creating the config json: %w", cs.FailureIcon(), err) + return fmt.Errorf( + "%s An error occurred when creating the config json: %w", + cs.FailureIcon(), + err, + ) } - filePath := config.GetConfigFileName(opts.Directory, opts.Indice, indice.GetAppID()) - err = os.WriteFile(filePath, configJsonIndented, 0644) + filePath := config.GetConfigFileName( + opts.Directory, + opts.Index, + client.GetConfiguration().AppID, + ) + // Gosec wants permissions of 0600 or less, but I don't want to change it + err = os.WriteFile(filePath, configJSONIndented, 0o644) // nolint:gosec if err != nil { return fmt.Errorf("%s An error occurred when saving the file: %w", cs.FailureIcon(), err) } @@ -116,9 +126,13 @@ func runExportCmd(opts *config.ExportOptions) error { if opts.Directory != "" { rootPath = currentDir } - fmt.Printf("%s '%s' Index config (%s) successfully exported to %s\n", - cs.SuccessIcon(), opts.Indice, utils.SliceToReadableString(opts.Scope), fmt.Sprintf("%s/%s", rootPath, filePath)) + fmt.Printf( + "%s '%s' Index config (%s) successfully exported to %s\n", + cs.SuccessIcon(), + opts.Index, + utils.SliceToReadableString(opts.Scope), + fmt.Sprintf("%s/%s", rootPath, filePath), + ) return nil - } diff --git a/pkg/cmd/indices/config/import/confirm.go b/pkg/cmd/indices/config/import/confirm.go index 3eae60c0..ed78b67b 100644 --- a/pkg/cmd/indices/config/import/confirm.go +++ b/pkg/cmd/indices/config/import/confirm.go @@ -7,7 +7,11 @@ import ( "github.com/algolia/cli/pkg/utils" ) -func GetConfirmMessage(cs *iostreams.ColorScheme, scope []string, clearExistingRules, clearExistingSynonyms bool) string { +func GetConfirmMessage( + cs *iostreams.ColorScheme, + scope []string, + clearExistingRules, clearExistingSynonyms bool, +) string { scopeToClear := []string{} scopeToUpdate := []string{} message := "" @@ -30,12 +34,21 @@ func GetConfirmMessage(cs *iostreams.ColorScheme, scope []string, clearExistingR } } if len(scopeToClear) > 0 { - message = fmt.Sprintf("%s Your %s will be %s\n", - cs.WarningIcon(), utils.SliceToReadableString(scopeToClear), cs.Bold("CLEARED and REPLACED.")) + message = fmt.Sprintf( + "%s Your %s will be %s\n", + cs.WarningIcon(), + utils.SliceToReadableString(scopeToClear), + cs.Bold("CLEARED and REPLACED."), + ) } if len(scopeToUpdate) > 0 { - message = fmt.Sprintf("%s%s Your %s will be %s\n", - message, cs.WarningIcon(), utils.SliceToReadableString(scopeToUpdate), cs.Bold("UPDATED")) + message = fmt.Sprintf( + "%s%s Your %s will be %s\n", + message, + cs.WarningIcon(), + utils.SliceToReadableString(scopeToUpdate), + cs.Bold("UPDATED"), + ) } return message diff --git a/pkg/cmd/indices/config/import/confirm_test.go b/pkg/cmd/indices/config/import/confirm_test.go index 934fb688..41f6ad68 100644 --- a/pkg/cmd/indices/config/import/confirm_test.go +++ b/pkg/cmd/indices/config/import/confirm_test.go @@ -61,7 +61,11 @@ func TestGetConfirmMessage(t *testing.T) { io, _, _, _ := iostreams.Test() tt.cs = io.ColorScheme() - assert.Equal(t, tt.wantsOutput, GetConfirmMessage(tt.cs, tt.scope, tt.clearExistingRules, tt.clearExistingSynonyms)) + assert.Equal( + t, + tt.wantsOutput, + GetConfirmMessage(tt.cs, tt.scope, tt.clearExistingRules, tt.clearExistingSynonyms), + ) }) } } diff --git a/pkg/cmd/indices/config/import/import.go b/pkg/cmd/indices/config/import/import.go index eb6366e3..ffb5a84c 100644 --- a/pkg/cmd/indices/config/import/import.go +++ b/pkg/cmd/indices/config/import/import.go @@ -4,7 +4,6 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmd/shared/handler" @@ -51,24 +50,37 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { $ algolia index config import PROD_MOVIES -F export-STAGING_MOVIES-APP_ID-1666792448.json --scope rules --clear-existing-rules `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } // JSON is parsed, read, validated (and options asked if interactive mode) - err := handler.HandleFlags(&handler.IndexConfigImportHandler{Opts: opts}, opts.IO.CanPrompt()) + err := handler.HandleFlags( + &handler.IndexConfigImportHandler{Opts: opts}, + opts.IO.CanPrompt(), + ) if err != nil { return err } if opts.DoConfirm { var confirmed bool - fmt.Printf("\n%s", GetConfirmMessage(cs, opts.Scope, opts.ClearExistingRules, opts.ClearExistingSynonyms)) + fmt.Printf( + "\n%s", + GetConfirmMessage( + cs, + opts.Scope, + opts.ClearExistingRules, + opts.ClearExistingSynonyms, + ), + ) err = prompt.Confirm("Import config?", &confirmed) if err != nil { return fmt.Errorf("failed to prompt: %w", err) @@ -85,20 +97,27 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { // Common cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip confirmation prompt") // Options - cmd.Flags().StringVarP(&opts.FilePath, "file", "F", "", "Directory path of the JSON config file") - cmd.Flags().StringSliceVarP(&opts.Scope, "scope", "s", []string{}, "Scope to import (default: none)") + cmd.Flags(). + StringVarP(&opts.FilePath, "file", "F", "", "Directory path of the JSON config file") + cmd.Flags(). + StringSliceVarP(&opts.Scope, "scope", "s", []string{}, "Scope to import (default: none)") _ = cmd.RegisterFlagCompletionFunc("scope", cmdutil.StringSliceCompletionFunc(map[string]string{ "settings": "settings", "synonyms": "synonyms", "rules": "rules", }, "import only")) - cmd.Flags().BoolVarP(&opts.ClearExistingSynonyms, "clear-existing-synonyms", "o", false, fmt.Sprintf("Clear %s existing synonyms of the index before import", cs.Bold("ALL"))) - cmd.Flags().BoolVarP(&opts.ClearExistingRules, "clear-existing-rules", "r", false, fmt.Sprintf("Clear %s existing rules of the index before import", cs.Bold("ALL"))) + cmd.Flags(). + BoolVarP(&opts.ClearExistingSynonyms, "clear-existing-synonyms", "o", false, fmt.Sprintf("Clear %s existing synonyms of the index before import", cs.Bold("ALL"))) + cmd.Flags(). + BoolVarP(&opts.ClearExistingRules, "clear-existing-rules", "r", false, fmt.Sprintf("Clear %s existing rules of the index before import", cs.Bold("ALL"))) // Replicas - cmd.Flags().BoolVarP(&opts.ForwardSynonymsToReplicas, "forward-synonyms-to-replicas", "m", false, "Forward imported synonyms to replicas") - cmd.Flags().BoolVarP(&opts.ForwardRulesToReplicas, "forward-rules-to-replicas", "l", false, "Forward imported rules to replicas") - cmd.Flags().BoolVarP(&opts.ForwardSettingsToReplicas, "forward-settings-to-replicas", "t", false, "Forward imported settings to replicas") + cmd.Flags(). + BoolVarP(&opts.ForwardSynonymsToReplicas, "forward-synonyms-to-replicas", "m", false, "Forward imported synonyms to replicas") + cmd.Flags(). + BoolVarP(&opts.ForwardRulesToReplicas, "forward-rules-to-replicas", "l", false, "Forward imported rules to replicas") + cmd.Flags(). + BoolVarP(&opts.ForwardSettingsToReplicas, "forward-settings-to-replicas", "t", false, "Forward imported settings to replicas") return cmd } @@ -110,40 +129,45 @@ func runImportCmd(opts *config.ImportOptions) error { return err } - indice := client.InitIndex(opts.Indice) - if opts.ImportConfig.Settings != nil && utils.Contains(opts.Scope, "settings") { - _, err = indice.SetSettings(*opts.ImportConfig.Settings, opt.ForwardToReplicas(opts.ForwardSettingsToReplicas)) + _, err = client.SetSettings( + client.NewApiSetSettingsRequest(opts.Index, opts.ImportConfig.Settings). + WithForwardToReplicas(opts.ForwardSettingsToReplicas), + ) if err != nil { - return fmt.Errorf("%s An error occurred when saving settings: %w", cs.FailureIcon(), err) + return fmt.Errorf( + "%s An error occurred when saving settings: %w", + cs.FailureIcon(), + err, + ) } } if len(opts.ImportConfig.Synonyms) > 0 && utils.Contains(opts.Scope, "synonyms") { - synonyms, err := SynonymsToSearchSynonyms(opts.ImportConfig.Synonyms) - if err != nil { - return err - } - _, err = indice.SaveSynonyms( - synonyms, - opt.ForwardToReplicas(opts.ForwardSynonymsToReplicas), - opt.ReplaceExistingSynonyms(opts.ClearExistingSynonyms), + _, err = client.SaveSynonyms( + client.NewApiSaveSynonymsRequest(opts.Index, opts.ImportConfig.Synonyms). + WithForwardToReplicas(opts.ForwardSynonymsToReplicas). + WithReplaceExistingSynonyms(opts.ClearExistingSynonyms), ) if err != nil { - return fmt.Errorf("%s An error occurred when saving synonyms: %w", cs.FailureIcon(), err) + return fmt.Errorf( + "%s An error occurred when saving synonyms: %w", + cs.FailureIcon(), + err, + ) } } if len(opts.ImportConfig.Rules) > 0 && utils.Contains(opts.Scope, "rules") { - _, err = indice.SaveRules( - opts.ImportConfig.Rules, - opt.ForwardToReplicas(opts.ForwardRulesToReplicas), - opt.ClearExistingRules(opts.ClearExistingRules), + _, err = client.SaveRules( + client.NewApiSaveRulesRequest(opts.Index, opts.ImportConfig.Rules). + WithForwardToReplicas(opts.ForwardRulesToReplicas). + WithClearExistingRules(opts.ClearExistingRules), ) if err != nil { return fmt.Errorf("%s An error occurred when saving rules: %w", cs.FailureIcon(), err) } } - fmt.Printf("%s Config successfully saved to '%s'", cs.SuccessIcon(), opts.Indice) + fmt.Printf("%s Config successfully saved to '%s'\n", cs.SuccessIcon(), opts.Index) return nil } diff --git a/pkg/cmd/indices/config/import/synonyms.go b/pkg/cmd/indices/config/import/synonyms.go deleted file mode 100644 index 5b7b70b8..00000000 --- a/pkg/cmd/indices/config/import/synonyms.go +++ /dev/null @@ -1,59 +0,0 @@ -package configimport - -import ( - "fmt" - - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" - - config "github.com/algolia/cli/pkg/cmd/shared/handler/indices" - "github.com/algolia/cli/pkg/cmd/synonyms/shared" -) - -func SynonymsToSearchSynonyms(synonyms []config.Synonym) ([]search.Synonym, error) { - var searchSynonyms []search.Synonym - for _, synonym := range synonyms { - searchSynonym, err := synonymToSearchSynonm(synonym) - if err != nil { - return nil, err - } - searchSynonyms = append(searchSynonyms, searchSynonym) - } - - return searchSynonyms, nil -} - -func synonymToSearchSynonm(synonym config.Synonym) (search.Synonym, error) { - switch synonym.Type { - case shared.OneWay: - return search.NewOneWaySynonym( - synonym.ObjectID, - synonym.Input, - synonym.Synonyms..., - ), nil - case shared.AltCorrection1: - return search.NewAltCorrection1( - synonym.ObjectID, - synonym.Word, - synonym.Corrections..., - ), nil - case shared.AltCorrection2: - return search.NewAltCorrection2( - synonym.ObjectID, - synonym.Word, - synonym.Corrections..., - ), nil - case shared.Placeholder: - return search.NewPlaceholder( - synonym.ObjectID, - synonym.Placeholder, - synonym.Replacements..., - ), nil - case "", shared.Regular: - return search.NewRegularSynonym( - synonym.ObjectID, - synonym.Synonyms..., - ), nil - } - - return nil, fmt.Errorf("invalid synonym type for object id %s", synonym.ObjectID) -} diff --git a/pkg/cmd/indices/copy/copy.go b/pkg/cmd/indices/copy/copy.go index a572ecfa..3df1a9cd 100644 --- a/pkg/cmd/indices/copy/copy.go +++ b/pkg/cmd/indices/copy/copy.go @@ -6,8 +6,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -21,7 +20,7 @@ type CopyOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) SourceIndex string DestinationIndex string @@ -75,7 +74,9 @@ func NewCopyCmd(f *cmdutil.Factory, runF func(*CopyOptions) error) *cobra.Comman if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -88,8 +89,9 @@ func NewCopyCmd(f *cmdutil.Factory, runF func(*CopyOptions) error) *cobra.Comman }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the copy index confirmation prompt") - cmd.Flags().StringSliceVarP(&opts.Scope, "scope", "s", []string{}, "Which index scopes to copy. Choose from settings, synonyms, and rules (default: all)") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip confirmation prompt") + cmd.Flags(). + StringSliceVarP(&opts.Scope, "scope", "s", []string{}, "Scope to copy: settings, synonyms, rules, or all (default)") cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") _ = cmd.RegisterFlagCompletionFunc("scope", @@ -109,13 +111,22 @@ func runCopyCmd(opts *CopyOptions) error { } var scopesDesc string + var scopes []search.ScopeType if len(opts.Scope) > 0 { scopesDesc = strings.Join(opts.Scope, ",") + for _, s := range opts.Scope { + scopes = append(scopes, search.ScopeType(s)) + } } else { scopesDesc = "records, settings, synonyms, and rules" } - message := fmt.Sprintf("Are you sure you want to copy %s from %s to %s?", scopesDesc, opts.SourceIndex, opts.DestinationIndex) + message := fmt.Sprintf( + "Are you sure you want to copy %s from %s to %s?", + scopesDesc, + opts.SourceIndex, + opts.DestinationIndex, + ) if opts.DoConfirm { var confirmed bool @@ -133,25 +144,46 @@ func runCopyCmd(opts *CopyOptions) error { } } - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Copying %s from %s to %s", scopesDesc, opts.SourceIndex, opts.DestinationIndex)) - _, err = client.CopyIndex(opts.SourceIndex, opts.DestinationIndex, opt.Scopes(opts.Scope...)) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf( + "Copying %s from %s to %s", + scopesDesc, + opts.SourceIndex, + opts.DestinationIndex, + ), + ) + res, err := client.OperationIndex( + client.NewApiOperationIndexRequest( + opts.SourceIndex, + search. + NewEmptyOperationIndexParams(). + SetDestination(opts.DestinationIndex). + SetOperation(search.OPERATION_TYPE_COPY). + SetScope(scopes))) if err != nil { return err } - // Wait() is broken right now on copy index - // if opts.Wait { - // opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") - // err = client.WaitTask(res.TaskID) - // if err != nil { - // return err - // } - // } + if opts.Wait { + opts.IO.UpdateProgressIndicatorLabel("Waiting for task") + _, err = client.WaitForTask(opts.DestinationIndex, res.TaskID) + if err != nil { + return err + } + } + opts.IO.StopProgressIndicator() cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Copied %s from %s to %s\n", cs.SuccessIcon(), scopesDesc, opts.SourceIndex, opts.DestinationIndex) + fmt.Fprintf( + opts.IO.Out, + "%s Copied %s from %s to %s\n", + cs.SuccessIcon(), + scopesDesc, + opts.SourceIndex, + opts.DestinationIndex, + ) } return nil diff --git a/pkg/cmd/indices/copy/copy_test.go b/pkg/cmd/indices/copy/copy_test.go index f081c5cf..51b06410 100644 --- a/pkg/cmd/indices/copy/copy_test.go +++ b/pkg/cmd/indices/copy/copy_test.go @@ -3,7 +3,7 @@ package copy import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -42,7 +42,7 @@ func TestNewCopyCmd(t *testing.T) { }, }, { - name: "specifying scopes", + name: "with scope: settings", cli: "foo bar --scope settings", tty: true, wantsErr: false, @@ -54,6 +54,45 @@ func TestNewCopyCmd(t *testing.T) { Wait: true, }, }, + { + name: "with scope: synonyms", + cli: "foo bar --scope synonyms", + tty: true, + wantsErr: false, + wantsOpts: CopyOptions{ + DoConfirm: true, + SourceIndex: "foo", + DestinationIndex: "bar", + Scope: []string{"synonyms"}, + Wait: true, + }, + }, + { + name: "with scope: rules", + cli: "foo bar --scope rules", + tty: true, + wantsErr: false, + wantsOpts: CopyOptions{ + DoConfirm: true, + SourceIndex: "foo", + DestinationIndex: "bar", + Scope: []string{"rules"}, + Wait: true, + }, + }, + { + name: "with multiple scope", + cli: "foo bar --scope 'rules,synonyms,settings'", + tty: true, + wantsErr: false, + wantsOpts: CopyOptions{ + DoConfirm: true, + SourceIndex: "foo", + DestinationIndex: "bar", + Scope: []string{"rules", "synonyms", "settings"}, + Wait: true, + }, + }, } for _, tt := range tests { @@ -134,7 +173,10 @@ func Test_runCreateCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", "1/indexes/foo/operation"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/operation"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) defer r.Verify(t) f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/indices/delete/delete.go b/pkg/cmd/indices/delete/delete.go index b5c5cd90..959ca081 100644 --- a/pkg/cmd/indices/delete/delete.go +++ b/pkg/cmd/indices/delete/delete.go @@ -6,8 +6,7 @@ import ( "strings" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -20,11 +19,12 @@ type DeleteOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) Indices []string DoConfirm bool IncludeReplicas bool + Wait bool } // NewDeleteCmd creates and returns a delete command for indices @@ -57,7 +57,7 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co $ algolia indices delete MOVIES # Delete the index named "MOVIES" and its replicas - $ algolia indices delete MOVIES --includeReplicas + $ algolia indices delete MOVIES --include-replicas # Delete the index named "MOVIES", skipping the confirmation prompt $ algolia indices delete MOVIES -y @@ -85,9 +85,11 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete index confirmation prompt") cmd.Flags(). - BoolVarP(&opts.IncludeReplicas, "includeReplicas", "r", false, "delete replica indices too") + BoolVarP(&confirm, "confirm", "y", false, "Skip the delete index confirmation prompt") + cmd.Flags(). + BoolVarP(&opts.IncludeReplicas, "include-replicas", "r", false, "delete replica indices too") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") return cmd } @@ -98,13 +100,27 @@ func runDeleteCmd(opts *DeleteOptions) error { return err } + // For nicer output + indexSingularOrPlural := "index" + if len(opts.Indices) > 1 { + indexSingularOrPlural = "indices" + } + if opts.DoConfirm { var confirmed bool - msg := "Are you sure you want to delete the indices %q?" + msg := fmt.Sprintf( + "Are you sure you want to delete the %s %q?", + indexSingularOrPlural, + strings.Join(opts.Indices, ", "), + ) if opts.IncludeReplicas { - msg = "Are you sure you want to delete the indices %q including their replicas?" + msg = fmt.Sprintf( + "Are you sure you want to delete the %s %q including their replicas?", + indexSingularOrPlural, + strings.Join(opts.Indices, ", "), + ) } - err := prompt.Confirm(fmt.Sprintf(msg, strings.Join(opts.Indices, ", ")), &confirmed) + err := prompt.Confirm(msg, &confirmed) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -113,72 +129,101 @@ func runDeleteCmd(opts *DeleteOptions) error { } } - indices := make([]*search.Index, 0, len(opts.Indices)) - for _, indexName := range opts.Indices { - index := client.InitIndex(indexName) - exists, err := index.Exists() - if err != nil || !exists { - return fmt.Errorf("index %q does not exist", indexName) + for _, index := range opts.Indices { + // Equivalent to `client.IndexExists` but provides settings already + settings, err := client.GetSettings(client.NewApiGetSettingsRequest(index)) + if err != nil { + return fmt.Errorf("can't get settings of index %s: %w", index, err) } - indices = append(indices, index) - - if opts.IncludeReplicas { - settings, err := index.GetSettings() - if err != nil { - return fmt.Errorf("can't get settings of index %q: %w", indexName, err) - } - replicas := settings.Replicas - for _, replicaName := range replicas.Get() { - pattern := regexp.MustCompile(`^virtual\((.*)\)$`) - matches := pattern.FindStringSubmatch(replicaName) - if len(matches) > 1 { - replicaName = matches[1] + // If both primary and replica are going to be deleted, we have to wait + // Or the `SetSettings` call in `detachReplica` creates a new, empty index + if settings.HasReplicas() { + for _, r := range settings.Replicas { + if contains(opts.Indices, r) { + opts.Wait = true } - replica := client.InitIndex(replicaName) - indices = append(indices, replica) } } - } - for _, index := range indices { - var mustWait bool - - if opts.IncludeReplicas { - settings, err := index.GetSettings() + // `index` is a replica index + if settings.HasPrimary() { + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Detaching replica index %s from its primary", index), + ) + err = detachReplica(index, *settings.Primary, client) if err != nil { - return fmt.Errorf("failed to get settings of index %q: %w", index.GetName(), err) - } - if len(settings.Replicas.Get()) > 0 { - mustWait = true + opts.IO.StopProgressIndicator() + return fmt.Errorf("can't detach index %s: %w", index, err) } + opts.IO.StopProgressIndicator() } - res, err := index.Delete() + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Deleting index %s", index), + ) + res, err := client.DeleteIndex(client.NewApiDeleteIndexRequest(index)) + if err != nil { + opts.IO.StopProgressIndicator() + return fmt.Errorf("can't delete index %s: %w", index, err) + } - // Otherwise, the replica indices might not be 'fully detached' yet. - if mustWait { - _ = res.Wait() + if !opts.IncludeReplicas && opts.Wait { + opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") + _, err := client.WaitForTask(index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } } - if err != nil { - opts.IO.StartProgressIndicatorWithLabel( - fmt.Sprint("Deleting replica index ", index.GetName()), - ) - err := deleteReplicaIndex(client, index) - opts.IO.StopProgressIndicator() + if opts.IncludeReplicas && len(settings.Replicas) > 0 { + // Wait for primary to be deleted, otherwise deleting replicas might fail + opts.IO.UpdateProgressIndicatorLabel("Waiting for the primary index to be deleted") + _, err := client.WaitForTask(index, res.TaskID) if err != nil { - return fmt.Errorf("failed to delete index %q: %w", index.GetName(), err) + opts.IO.StopProgressIndicator() + return fmt.Errorf("error while waiting for index %s to be deleted: %w", index, err) + } + + for _, replica := range settings.Replicas { + // Virtual replicas have name `virtual(replica)`... + pattern := regexp.MustCompile(`^virtual\((.*)\)$`) + matches := pattern.FindStringSubmatch(replica) + if len(matches) > 1 { + // But when deleting, we need the bare name + replica = matches[1] + // For printing the summary + opts.Indices = append(opts.Indices, replica) + } + + opts.IO.UpdateProgressIndicatorLabel( + fmt.Sprintf("Deleting replica %s", index), + ) + res, err = client.DeleteIndex(client.NewApiDeleteIndexRequest(replica)) + if err != nil { + opts.IO.StopProgressIndicator() + return fmt.Errorf("can't delete replica %s: %w", replica, err) + } + if opts.Wait { + _, err := client.WaitForTask(replica, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } } } + opts.IO.StopProgressIndicator() } cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { fmt.Fprintf( opts.IO.Out, - "%s Deleted indices %s\n", + "%s Deleted %s %s\n", cs.SuccessIcon(), + indexSingularOrPlural, strings.Join(opts.Indices, ", "), ) } @@ -186,99 +231,68 @@ func runDeleteCmd(opts *DeleteOptions) error { return nil } -// Delete a replica index. -func deleteReplicaIndex(client *search.Client, replicaIndex *search.Index) error { - replicaName := replicaIndex.GetName() - primaryName, err := findPrimaryIndex(replicaIndex) - if err != nil { - return fmt.Errorf("can't find primary index for %q: %w", replicaName, err) - } - - err = detachReplicaIndex(replicaName, primaryName, client) +// Remove replica from `replicas` settings of the primary index +func detachReplica(replica string, primary string, client *search.APIClient) error { + settings, err := client.GetSettings(client.NewApiGetSettingsRequest(primary)) if err != nil { - return fmt.Errorf( - "can't unlink replica index %s from primary index %s: %w", - replicaName, - primaryName, - err, - ) + return fmt.Errorf("can't get settings of primary index %s: %w", primary, err) } - _, err = replicaIndex.Delete() - if err != nil { - return fmt.Errorf("can't delete replica index %q: %w", replicaName, err) + if isVirtual(settings.Replicas, replica) { + replica = fmt.Sprintf("virtual(%s)", replica) } - return nil -} + newReplicas := removeReplica(settings.Replicas, replica) -// Find the primary index of a replica index -func findPrimaryIndex(replicaIndex *search.Index) (string, error) { - replicaName := replicaIndex.GetName() - settings, err := replicaIndex.GetSettings() + res, err := client.SetSettings( + client.NewApiSetSettingsRequest( + primary, + search.NewIndexSettings().SetReplicas(newReplicas), + ), + ) if err != nil { - return "", fmt.Errorf("can't get settings of replica index %q: %w", replicaName, err) - } - - primary := settings.Primary - if primary == nil { - return "", fmt.Errorf("index %s doesn't have a primary", replicaName) + return fmt.Errorf("can't detach replica %s from its primary %s: %w", replica, primary, err) } - return primary.Get(), nil -} - -// Remove replica from `replicas` settings of the primary index -func detachReplicaIndex(replicaName string, primaryName string, client *search.Client) error { - primaryIndex := client.InitIndex(primaryName) - settings, err := primaryIndex.GetSettings() + _, err = client.WaitForTask(primary, res.TaskID) if err != nil { - return fmt.Errorf("can't get settings of primary index %q: %w", primaryName, err) + return fmt.Errorf("can't wait for updating the primary's settings: %w", err) } - replicas := settings.Replicas.Get() - isVirtual := isVirtualReplica(replicas, replicaName) - if isVirtual { - replicaName = fmt.Sprintf("virtual(%s)", replicaName) - } - indexOfReplica := findIndex(replicas, replicaName) + return nil +} - // Delete the replica at position `indexOfReplica` from the array - replicas = append(replicas[:indexOfReplica], replicas[indexOfReplica+1:]...) +// isVirtual checks whether an index is a virtual replica +func isVirtual(replicas []string, name string) bool { + pattern := regexp.MustCompile(fmt.Sprintf(`^virtual\(%s\)$`, name)) - res, err := primaryIndex.SetSettings( - search.Settings{ - Replicas: opt.Replicas(replicas...), - }, - ) - if err != nil { - return fmt.Errorf("can't update settings of index %q: %w", primaryName, err) + for _, i := range replicas { + matches := pattern.MatchString(i) + if matches { + return true + } } - // Wait until the settings are updated, else a subsequent `delete` will fail. - _ = res.Wait() - return nil + return false } -// Find the index of the string `target` in the array `arr` -func findIndex(arr []string, target string) int { - for i, v := range arr { - if v == target { - return i +// removeReplica returns a new slice without a replica +func removeReplica(replicas []string, name string) []string { + for i, v := range replicas { + if v == name { + // Return a new slice without the given replica + return append(replicas[:i], replicas[i+1:]...) } } - return -1 + return replicas } -func isVirtualReplica(replicas []string, replicaName string) bool { - pattern := regexp.MustCompile(fmt.Sprintf(`^virtual\(%s\)$`, replicaName)) - - for _, i := range replicas { - matches := pattern.MatchString(i) - if matches { +// contains checks if ele is in arr +func contains[T comparable](arr []T, ele T) bool { + for _, i := range arr { + if ele == i { return true } } - return false } diff --git a/pkg/cmd/indices/delete/delete_test.go b/pkg/cmd/indices/delete/delete_test.go index 99c7a9cb..afd8d633 100644 --- a/pkg/cmd/indices/delete/delete_test.go +++ b/pkg/cmd/indices/delete/delete_test.go @@ -4,8 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -25,7 +24,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsOpts DeleteOptions }{ { - name: "single indice, no --confirm, without tty", + name: "single index, no --confirm, without tty", cli: "foo", tty: false, wantsErr: true, @@ -35,7 +34,7 @@ func TestNewDeleteCmd(t *testing.T) { }, }, { - name: "single indice, --confirm, without tty", + name: "single index, --confirm, without tty", cli: "foo --confirm", tty: false, wantsErr: false, @@ -116,7 +115,7 @@ func Test_runDeleteCmd(t *testing.T) { cli: "foo --confirm", indices: []string{"foo"}, isTTY: true, - wantOut: "✓ Deleted indices foo\n", + wantOut: "✓ Deleted index foo\n", }, { name: "no TTY, multiple indices", @@ -138,15 +137,15 @@ func Test_runDeleteCmd(t *testing.T) { indices: []string{"foo"}, isReplica: true, isTTY: true, - wantOut: "✓ Deleted indices foo\n", + wantOut: "✓ Deleted index foo\n", }, { name: "TTY, has replica indices", - cli: "foo --confirm --includeReplicas", + cli: "foo --confirm --include-replicas", indices: []string{"foo"}, hasReplicas: true, isTTY: true, - wantOut: "✓ Deleted indices foo\n", + wantOut: "✓ Deleted index foo\n", }, } @@ -154,38 +153,22 @@ func Test_runDeleteCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} for _, index := range tt.indices { - // First settings call with `Exists()` - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.Settings{})) - if tt.hasReplicas { - // Settings calls for the primary index and its replicas - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.Settings{ - Replicas: opt.Replicas("bar"), - })) - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.Settings{ - Replicas: opt.Replicas("bar"), - })) - r.Register(httpmock.REST("GET", "1/indexes/bar/settings"), httpmock.JSONResponse(search.Settings{ - Primary: opt.Primary("foo"), - })) - // Additional DELETE calls for the replicas - r.Register(httpmock.REST("DELETE", "1/indexes/bar"), httpmock.JSONResponse(search.Settings{})) - } - if tt.isReplica { - // We want the first `Delete()` call to fail - r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s", index)), httpmock.ErrorResponse()) - // Second settings call to fetch the primary index name - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), httpmock.JSONResponse(search.Settings{ - Primary: opt.Primary("bar"), - })) - // Third settings call to fetch the primary index settings - r.Register(httpmock.REST("GET", "1/indexes/bar/settings"), httpmock.JSONResponse(search.Settings{ - Replicas: opt.Replicas(index), - })) - // Fourth settings call to update the primary settings - r.Register(httpmock.REST("PUT", "1/indexes/bar/settings"), httpmock.JSONResponse(search.Settings{})) - } - // Final `Delete()` call - r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s", index)), httpmock.JSONResponse(search.DeleteKeyRes{})) + // GetSettings request to check if index exists + r.Register( + httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/settings", index)), + httpmock.JSONResponse(search.SettingsResponse{}), + ) + // DeleteIndex request for the primary index + r.Register( + httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s", index)), + httpmock.JSONResponse(search.DeletedAtResponse{}), + ) + // if tt.hasReplicas { + // r.Register( + // httpmock.REST("DELETE", "1/indexes/bar"), + // httpmock.JSONResponse(search.DeletedAtResponse{}), + // ) + // } } defer r.Verify(t) diff --git a/pkg/cmd/indices/list/list.go b/pkg/cmd/indices/list/list.go index e7c6251f..8b5914b7 100644 --- a/pkg/cmd/indices/list/list.go +++ b/pkg/cmd/indices/list/list.go @@ -2,9 +2,11 @@ package list import ( "fmt" + "strconv" + "time" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/dustin/go-humanize" "github.com/spf13/cobra" @@ -19,7 +21,7 @@ type ListOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) PrintFlags *cmdutil.PrintFlags } @@ -33,9 +35,10 @@ func NewListCmd(f *cmdutil.Factory) *cobra.Command { PrintFlags: cmdutil.NewPrintFlags(), } cmd := &cobra.Command{ - Use: "list", - Args: validators.NoArgs(), - Short: "List indices", + Use: "list", + Aliases: []string{"l"}, + Args: validators.NoArgs(), + Short: "List indices", Example: heredoc.Doc(` # List indices $ algolia indices list @@ -61,7 +64,7 @@ func runListCmd(opts *ListOptions) error { } opts.IO.StartProgressIndicatorWithLabel("Fetching indices") - res, err := client.ListIndices() + res, err := client.ListIndices(client.NewApiListIndicesRequest()) opts.IO.StopProgressIndicator() if err != nil { return err @@ -94,15 +97,50 @@ func runListCmd(opts *ListOptions) error { } for _, index := range res.Items { + var primary string + if index.Primary == nil { + primary = "" + } else { + primary = *index.Primary + } + updatedAt, err := parseTime(index.UpdatedAt) + if err != nil { + return fmt.Errorf("can't parse %s into a time struct", index.UpdatedAt) + } + createdAt, err := parseTime(index.CreatedAt) + if err != nil { + return fmt.Errorf("can't parse %s into a time struct", index.CreatedAt) + } + // Prevent integer overflow + if index.DataSize < 0 { + index.DataSize = 0 + } table.AddField(index.Name, nil, nil) - table.AddField(humanize.Comma(index.Entries), nil, nil) + table.AddField(humanize.Comma(int64(index.Entries)), nil, nil) table.AddField(humanize.Bytes(uint64(index.DataSize)), nil, nil) - table.AddField(humanize.Time(index.UpdatedAt), nil, nil) - table.AddField(humanize.Time(index.CreatedAt), nil, nil) - table.AddField(index.LastBuildTime.String(), nil, nil) - table.AddField(index.Primary, nil, nil) + table.AddField(updatedAt, nil, nil) + table.AddField(createdAt, nil, nil) + table.AddField(strconv.Itoa(int(index.LastBuildTimeS))+"s", nil, nil) + table.AddField(primary, nil, nil) table.AddField(fmt.Sprintf("%v", index.Replicas), nil, nil) table.EndRow() } return table.Render() } + +// parseTime parses the string from the API response into a relative time string +func parseTime(timeAsString string) (string, error) { + const layout = "2006-01-02T15:04:05.999Z" + + // This *should* restore the previous behavior when UpdatedAt is empty + if timeAsString == "" { + return "a long while ago", nil + } + + t, err := time.Parse(layout, timeAsString) + if err != nil { + return "", err + } + + return humanize.Time(t), nil +} diff --git a/pkg/cmd/indices/move/move.go b/pkg/cmd/indices/move/move.go index d0b94156..beb01f53 100644 --- a/pkg/cmd/indices/move/move.go +++ b/pkg/cmd/indices/move/move.go @@ -5,7 +5,7 @@ import ( "github.com/AlecAivazis/survey/v2" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -19,7 +19,7 @@ type MoveOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) SourceIndex string DestinationIndex string @@ -60,7 +60,9 @@ func NewMoveCmd(f *cmdutil.Factory, runF func(*MoveOptions) error) *cobra.Comman if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -86,7 +88,11 @@ func runMoveCmd(opts *MoveOptions) error { } cs := opts.IO.ColorScheme() - message := fmt.Sprintf("Are you sure you want to move %s to %s?", cs.Bold(opts.SourceIndex), cs.Bold(opts.DestinationIndex)) + message := fmt.Sprintf( + "Are you sure you want to move %s to %s?", + cs.Bold(opts.SourceIndex), + cs.Bold(opts.DestinationIndex), + ) if opts.DoConfirm { var confirmed bool @@ -103,8 +109,17 @@ func runMoveCmd(opts *MoveOptions) error { } } - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Moving %s to %s", cs.Bold(opts.SourceIndex), cs.Bold(opts.DestinationIndex))) - res, err := client.MoveIndex(opts.SourceIndex, opts.DestinationIndex) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Moving %s to %s", cs.Bold(opts.SourceIndex), cs.Bold(opts.DestinationIndex)), + ) + res, err := client.OperationIndex( + client.NewApiOperationIndexRequest( + opts.SourceIndex, + search.NewEmptyOperationIndexParams(). + SetDestination(opts.DestinationIndex). + SetOperation(search.OPERATION_TYPE_MOVE), + ), + ) if err != nil { opts.IO.StopProgressIndicator() return err @@ -112,7 +127,7 @@ func runMoveCmd(opts *MoveOptions) error { if opts.Wait { opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") - err = client.InitIndex(opts.DestinationIndex).WaitTask(res.TaskID) + _, err := client.WaitForTask(opts.DestinationIndex, res.TaskID) if err != nil { opts.IO.StopProgressIndicator() return err @@ -122,7 +137,13 @@ func runMoveCmd(opts *MoveOptions) error { opts.IO.StopProgressIndicator() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Moved %s to %s\n", cs.SuccessIcon(), cs.Bold(opts.SourceIndex), cs.Bold(opts.DestinationIndex)) + fmt.Fprintf( + opts.IO.Out, + "%s Moved %s to %s\n", + cs.SuccessIcon(), + cs.Bold(opts.SourceIndex), + cs.Bold(opts.DestinationIndex), + ) } return nil diff --git a/pkg/cmd/indices/move/move_test.go b/pkg/cmd/indices/move/move_test.go index 2f026003..f35a9c2e 100644 --- a/pkg/cmd/indices/move/move_test.go +++ b/pkg/cmd/indices/move/move_test.go @@ -3,7 +3,7 @@ package move import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -122,7 +122,10 @@ func Test_runMoveCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", "1/indexes/foo/operation"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/operation"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) defer r.Verify(t) f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/objects/browse/browse.go b/pkg/cmd/objects/browse/browse.go index 6b2f46c2..fdd213c2 100644 --- a/pkg/cmd/objects/browse/browse.go +++ b/pkg/cmd/objects/browse/browse.go @@ -1,11 +1,10 @@ package browse import ( - "io" + "encoding/json" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -18,10 +17,10 @@ type BrowseOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string - BrowseParams map[string]interface{} + Index string + BrowseParams search.BrowseParamsObject PrintFlags *cmdutil.PrintFlags } @@ -37,6 +36,7 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "browse ", + Aliases: []string{"list", "l"}, Args: validators.ExactArgs(1), ValidArgsFunction: cmdutil.IndexNames(opts.SearchClient), Annotations: map[string]string{ @@ -61,13 +61,22 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { $ algolia objects browse MOVIES > movies.ndjson `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] browseParams, err := cmdutil.FlagValuesMap(cmd.Flags(), cmdutil.BrowseParamsObject...) if err != nil { return err } - opts.BrowseParams = browseParams + + // Convert map to object + tmp, err := json.Marshal(browseParams) + if err != nil { + return err + } + err = json.Unmarshal(tmp, &opts.BrowseParams) + if err != nil { + return err + } return runBrowseCmd(opts) }, @@ -87,36 +96,28 @@ func runBrowseCmd(opts *BrowseOptions) error { return err } - indice := client.InitIndex(opts.Indice) - - // We use the `opt.ExtraOptions` to pass the `SearchParams` to the API. - query, ok := opts.BrowseParams["query"].(string) - if !ok { - query = "" - } else { - delete(opts.BrowseParams, "query") - } - res, err := indice.BrowseObjects(opt.Query(query), opt.ExtraOptions(opts.BrowseParams)) - if err != nil { - return err - } - p, err := opts.PrintFlags.ToPrinter() if err != nil { return err } - for { - iObject, err := res.Next() - if err != nil { - if err == io.EOF { - return nil + err = client.BrowseObjects( + opts.Index, + opts.BrowseParams, + search.WithAggregator(func(res any, err error) { + if err != nil { + return } - return err - } - if err = p.Print(opts.IO, iObject); err != nil { - return err - } - + for _, hit := range res.(*search.BrowseResponse).Hits { + err := p.Print(opts.IO, hit) + if err != nil { + return + } + } + }), + ) + if err != nil { + return err } + return nil } diff --git a/pkg/cmd/objects/browse/browse_test.go b/pkg/cmd/objects/browse/browse_test.go index b400cc2d..1080d655 100644 --- a/pkg/cmd/objects/browse/browse_test.go +++ b/pkg/cmd/objects/browse/browse_test.go @@ -3,7 +3,7 @@ package browse import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -14,19 +14,19 @@ func Test_runBrowseCmd(t *testing.T) { tests := []struct { name string cli string - hits []map[string]interface{} + hits []search.Hit wantOut string }{ { name: "single object", cli: "foo", - hits: []map[string]interface{}{{"objectID": "foo"}}, + hits: []search.Hit{{ObjectID: "foo"}}, wantOut: "{\"objectID\":\"foo\"}\n", }, { name: "multiple objects", cli: "foo", - hits: []map[string]interface{}{{"objectID": "foo"}, {"objectID": "bar"}}, + hits: []search.Hit{{ObjectID: "foo"}, {ObjectID: "bar"}}, wantOut: "{\"objectID\":\"foo\"}\n{\"objectID\":\"bar\"}\n", }, } @@ -34,9 +34,12 @@ func Test_runBrowseCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", "1/indexes/foo/browse"), httpmock.JSONResponse(search.QueryRes{ - Hits: tt.hits, - })) + r.Register( + httpmock.REST("POST", "1/indexes/foo/browse"), + httpmock.JSONResponse(search.BrowseResponse{ + Hits: tt.hits, + }), + ) defer r.Verify(t) f, out := test.NewFactory(true, &r, nil, "") diff --git a/pkg/cmd/objects/delete/delete.go b/pkg/cmd/objects/delete/delete.go index 03bb7046..879cea2d 100644 --- a/pkg/cmd/objects/delete/delete.go +++ b/pkg/cmd/objects/delete/delete.go @@ -6,8 +6,7 @@ import ( "strings" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -22,11 +21,12 @@ type DeleteOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string - ObjectIDs []string - DeleteParams map[string]interface{} + Index string + ObjectIDs []string + DeleteParams search.DeleteByParams + NdeleteParams int DoConfirm bool Wait bool @@ -66,20 +66,32 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co $ algolia objects delete MOVIES --filters "type:Scripted" --confirm `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] deleteParams, err := cmdutil.FlagValuesMap(cmd.Flags(), cmdutil.DeleteByParams...) if err != nil { return err } - opts.DeleteParams = deleteParams + opts.NdeleteParams = len(deleteParams) - if len(opts.ObjectIDs) == 0 && len(opts.DeleteParams) == 0 { + // Convert map into object + tmp, err := json.Marshal(deleteParams) + if err != nil { + return err + } + err = json.Unmarshal(tmp, &opts.DeleteParams) + if err != nil { + return err + } + + if len(opts.ObjectIDs) == 0 && opts.NdeleteParams == 0 { return cmdutil.FlagErrorf("you must specify either --object-ids or a filter") } if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -92,11 +104,12 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co }, } - cmd.Flags().StringSliceVarP(&opts.ObjectIDs, "object-ids", "", nil, "objectIDs to delete") + cmd.Flags().StringSliceVarP(&opts.ObjectIDs, "object-ids", "", nil, "Object IDs to delete") cmdutil.AddDeleteByParamsFlags(cmd) - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete record confirmation prompt") - cmd.Flags().BoolVar(&opts.Wait, "wait", false, "Wait for all the operations to complete before returning") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip confirmation prompt") + cmd.Flags(). + BoolVar(&opts.Wait, "wait", false, "Wait for all the operations to complete") return cmd } @@ -107,15 +120,13 @@ func runDeleteCmd(opts *DeleteOptions) error { if err != nil { return err } - - indice := client.InitIndex(opts.Indice) nbObjectsToDelete := len(opts.ObjectIDs) extra := "Operation aborted, no deletion action taken" // Tests if the provided object IDs exists. for _, objectID := range opts.ObjectIDs { - var obj interface{} - if err := indice.GetObject(objectID, &obj); err != nil { + _, err := client.GetObject(client.NewApiGetObjectRequest(opts.Index, objectID)) + if err != nil { // The original error is not helpful, so we print a more helpful message if strings.Contains(err.Error(), "ObjectID does not exist") { return fmt.Errorf("object with ID '%s' does not exist. %s", objectID, extra) @@ -129,13 +140,16 @@ func runDeleteCmd(opts *DeleteOptions) error { exactOrApproximate := "exactly" // If the user provided filters, we need to count the number of objects matching the filters - if len(opts.DeleteParams) > 0 { - res, err := indice.Search("", opt.ExtraOptions(opts.DeleteParams)) + if opts.NdeleteParams > 0 { + res, err := client.SearchSingleIndex( + client.NewApiSearchSingleIndexRequest(opts.Index). + WithSearchParams(search.SearchParamsObjectAsSearchParams(deleteByToSearchParams(&opts.DeleteParams))), + ) if err != nil { return err } - nbObjectsToDelete = nbObjectsToDelete + res.NbHits - if !res.ExhaustiveNbHits { + nbObjectsToDelete = nbObjectsToDelete + int(*res.NbHits) + if res.ExhaustiveNbHits != nil && !*res.ExhaustiveNbHits { exactOrApproximate = "approximately" } } @@ -147,7 +161,12 @@ func runDeleteCmd(opts *DeleteOptions) error { return nil } - objectNbMessage := fmt.Sprintf("%s %s from %s", exactOrApproximate, utils.Pluralize(nbObjectsToDelete, "object"), opts.Indice) + objectNbMessage := fmt.Sprintf( + "%s %s from %s", + exactOrApproximate, + utils.Pluralize(nbObjectsToDelete, "object"), + opts.Index, + ) if opts.DoConfirm { var confirmed bool @@ -164,34 +183,32 @@ func runDeleteCmd(opts *DeleteOptions) error { // Delete the objects by their IDs if len(opts.ObjectIDs) > 0 { - deleteByIDRes, err := indice.DeleteObjects(opts.ObjectIDs) + batchRes, err := client.DeleteObjects(opts.Index, opts.ObjectIDs) if err != nil { return err } - - taskIDs = append(taskIDs, deleteByIDRes.TaskID) + for _, res := range batchRes { + taskIDs = append(taskIDs, res.TaskID) + } } // Delete the objects matching the filters - if len(opts.DeleteParams) > 0 { - deleteByOpts, err := deleteParamsToDeleteByOpts(opts.DeleteParams) + if opts.NdeleteParams > 0 { + res, err := client.DeleteBy(client.NewApiDeleteByRequest(opts.Index, &opts.DeleteParams)) if err != nil { return err } - deleteByRes, err := indice.DeleteBy(deleteByOpts...) - if err != nil { - return err - } - - taskIDs = append(taskIDs, deleteByRes.TaskID) + taskIDs = append(taskIDs, res.TaskID) } // Wait for the tasks to complete if opts.Wait { - opts.IO.StartProgressIndicatorWithLabel("Waiting for all of the deletion tasks to complete") + opts.IO.StartProgressIndicatorWithLabel("Waiting for all deletion tasks to complete") for _, taskID := range taskIDs { - if err := indice.WaitTask(taskID); err != nil { + _, err := client.WaitForTask(opts.Index, taskID) + if err != nil { + opts.IO.StopProgressIndicator() return err } } @@ -205,68 +222,16 @@ func runDeleteCmd(opts *DeleteOptions) error { return nil } -// flagValueToOpts returns a given option from the provided flag. -// It is used to convert the flag value to the correct type expected by the `DeleteBy` method. -func flagValueToOpts(value interface{}, opt interface{}) error { - b, err := json.Marshal(value) - if err != nil { - return err +// deleteByToSearchParams returns a new SearchParamsObject from a DeleteByParams struct +func deleteByToSearchParams(input *search.DeleteByParams) *search.SearchParamsObject { + return &search.SearchParamsObject{ + Filters: input.Filters, + FacetFilters: input.FacetFilters, + NumericFilters: input.NumericFilters, + TagFilters: input.TagFilters, + AroundLatLng: input.AroundLatLng, + AroundRadius: input.AroundRadius, + InsideBoundingBox: input.InsideBoundingBox, + InsidePolygon: input.InsidePolygon, } - - if err := json.Unmarshal(b, opt); err != nil { - return err - } - - return nil -} - -// deleteParamsToDeleteByOpts returns an array of deleteByOptions from the provided delete parameters. -func deleteParamsToDeleteByOpts(params map[string]interface{}) ([]interface{}, error) { - var opts []interface{} - - for key, value := range params { - switch key { - case "filters": - var filtersOpt opt.FiltersOption - if err := flagValueToOpts(value, &filtersOpt); err != nil { - return nil, err - } - - opts = append(opts, &filtersOpt) - - case "facetFilters": - var facetFiltersOpt opt.FacetFiltersOption - if err := flagValueToOpts(value, &facetFiltersOpt); err != nil { - return nil, err - } - - opts = append(opts, &facetFiltersOpt) - - case "numericFilters": - var numericFiltersOpt opt.NumericFiltersOption - if err := flagValueToOpts(value, &numericFiltersOpt); err != nil { - return nil, err - } - - opts = append(opts, &numericFiltersOpt) - - case "tagFilters": - var tagFiltersOpt opt.TagFiltersOption - if err := flagValueToOpts(value, &tagFiltersOpt); err != nil { - return nil, err - } - - opts = append(opts, &tagFiltersOpt) - - case "aroundLatLng": - var aroundLatLngOpt opt.AroundLatLngOption - if err := flagValueToOpts(value, &aroundLatLngOpt); err != nil { - return nil, err - } - - opts = append(opts, &aroundLatLngOpt) - } - } - - return opts, nil } diff --git a/pkg/cmd/objects/delete/delete_test.go b/pkg/cmd/objects/delete/delete_test.go index 2656b4ef..c5c41c55 100644 --- a/pkg/cmd/objects/delete/delete_test.go +++ b/pkg/cmd/objects/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,7 +36,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", ObjectIDs: []string{ "1", }, @@ -49,7 +49,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: true, - Indice: "foo", + Index: "foo", ObjectIDs: []string{ "1", }, @@ -62,7 +62,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", ObjectIDs: []string{ "1", "2", @@ -103,7 +103,7 @@ func TestNewDeleteCmd(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - assert.Equal(t, tt.wantsOpts.Indice, opts.Indice) + assert.Equal(t, tt.wantsOpts.Index, opts.Index) assert.Equal(t, tt.wantsOpts.ObjectIDs, opts.ObjectIDs) assert.Equal(t, tt.wantsOpts.DoConfirm, opts.DoConfirm) }) @@ -111,20 +111,21 @@ func TestNewDeleteCmd(t *testing.T) { } func Test_runDeleteCmd(t *testing.T) { + var testNbHits int32 = 2 tests := []struct { name string cli string - indice string + index string objectIDs []string - nbHits int + nbHits *int32 exhaustiveNbHits bool isTTY bool wantOut string }{ { - name: "single object-id, no TTY", - cli: "foo --object-ids 1 --confirm", - indice: "foo", + name: "single object-id, no TTY", + cli: "foo --object-ids 1 --confirm", + index: "foo", objectIDs: []string{ "1", }, @@ -132,9 +133,9 @@ func Test_runDeleteCmd(t *testing.T) { wantOut: "", }, { - name: "single object-id, TTY", - cli: "foo --object-ids 1 --confirm", - indice: "foo", + name: "single object-id, TTY", + cli: "foo --object-ids 1 --confirm", + index: "foo", objectIDs: []string{ "1", }, @@ -142,9 +143,9 @@ func Test_runDeleteCmd(t *testing.T) { wantOut: "✓ Successfully deleted exactly 1 object from foo\n", }, { - name: "multiple object-ids, TTY", - cli: "foo --object-ids 1,2 --confirm", - indice: "foo", + name: "multiple object-ids, TTY", + cli: "foo --object-ids 1,2 --confirm", + index: "foo", objectIDs: []string{ "1", "2", @@ -155,18 +156,18 @@ func Test_runDeleteCmd(t *testing.T) { { name: "filters, TTY", cli: "foo --filters 'foo:bar' --confirm", - indice: "foo", + index: "foo", objectIDs: []string{}, - nbHits: 2, + nbHits: &testNbHits, isTTY: true, wantOut: "✓ Successfully deleted approximately 2 objects from foo\n", }, { name: "filters, TTY", cli: "foo --filters 'foo:bar' --confirm", - indice: "foo", + index: "foo", objectIDs: []string{}, - nbHits: 2, + nbHits: &testNbHits, exhaustiveNbHits: true, isTTY: true, wantOut: "✓ Successfully deleted exactly 2 objects from foo\n", @@ -174,9 +175,9 @@ func Test_runDeleteCmd(t *testing.T) { { name: "filters and object-ids, TTY", cli: "foo --filters 'foo:bar' --object-ids 1,2 --confirm", - indice: "foo", + index: "foo", objectIDs: []string{"1", "2"}, - nbHits: 2, + nbHits: &testNbHits, isTTY: true, wantOut: "✓ Successfully deleted approximately 4 objects from foo\n", }, @@ -187,18 +188,30 @@ func Test_runDeleteCmd(t *testing.T) { r := httpmock.Registry{} for _, id := range tt.objectIDs { // Checking that the object exists - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/%s", tt.indice, id)), httpmock.JSONResponse(search.QueryRes{})) + r.Register( + httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/%s", tt.index, id)), + httpmock.JSONResponse(search.GetObjectsResponse{}), + ) } - if tt.nbHits > 0 { + if tt.nbHits != nil && *tt.nbHits > 0 { // Searching for the objects to delete (if filters are used) - r.Register(httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/query", tt.indice)), httpmock.JSONResponse(search.QueryRes{ - NbHits: tt.nbHits, - ExhaustiveNbHits: tt.exhaustiveNbHits, - })) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/query", tt.index)), + httpmock.JSONResponse(search.SearchResponse{ + NbHits: tt.nbHits, + ExhaustiveNbHits: &tt.exhaustiveNbHits, + }), + ) // Deleting the objects - r.Register(httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/deleteByQuery", tt.indice)), httpmock.JSONResponse(search.BatchRes{})) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/deleteByQuery", tt.index)), + httpmock.JSONResponse(search.BatchResponse{}), + ) } - r.Register(httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/batch", tt.indice)), httpmock.JSONResponse(search.BatchRes{})) + r.Register( + httpmock.REST("POST", fmt.Sprintf("1/indexes/%s/batch", tt.index)), + httpmock.JSONResponse(search.BatchResponse{}), + ) f, out := test.NewFactory(tt.isTTY, &r, nil, "") cmd := NewDeleteCmd(f, nil) diff --git a/pkg/cmd/objects/import/import.go b/pkg/cmd/objects/import/import.go index 0efeb118..31aadf91 100644 --- a/pkg/cmd/objects/import/import.go +++ b/pkg/cmd/objects/import/import.go @@ -1,4 +1,4 @@ -package importRecords +package importrecords import ( "bufio" @@ -7,8 +7,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -18,17 +17,18 @@ import ( ) type ImportOptions struct { - Config config.IConfig - IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) - Index string - AutoGenerateObjectIDIfNotExist bool - - Scanner *bufio.Scanner - BatchSize int + Config config.IConfig + IO *iostreams.IOStreams + SearchClient func() (*search.APIClient, error) + Index string + + Scanner *bufio.Scanner + BatchSize int + AutoObjectIDs bool + Wait bool } -// NewImportCmd creates and returns an import command for indice object +// NewImportCmd creates and returns an import command for records func NewImportCmd(f *cmdutil.Factory) *cobra.Command { opts := &ImportOptions{ IO: f.IOStreams, @@ -72,11 +72,13 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().StringVarP(&file, "file", "F", "", "Import records from a `file` (use \"-\" to read from standard input)") + cmd.Flags(). + StringVarP(&file, "file", "F", "", "Import records from a `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - - cmd.Flags().BoolVar(&opts.AutoGenerateObjectIDIfNotExist, "auto-generate-object-id-if-not-exist", false, "Add objectID fields and values to imported records if they aren't present.") cmd.Flags().IntVarP(&opts.BatchSize, "batch-size", "b", 1000, "Specify the upload batch size") + cmd.Flags(). + BoolVarP(&opts.AutoObjectIDs, "auto-generate-object-id-if-not-exist", "a", false, "Auto-generate object IDs if they don't exist") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") return cmd } @@ -86,18 +88,8 @@ func runImportCmd(opts *ImportOptions) error { return err } - indice := client.InitIndex(opts.Index) - - // Move the following code to another module? - var ( - batchSize = opts.BatchSize - batch = make([]interface{}, 0, batchSize) - count = 0 - totalCount = 0 - ) - - options := []interface{}{opt.AutoGenerateObjectIDIfNotExist(opts.AutoGenerateObjectIDIfNotExist)} - + count := 0 + var records []map[string]any opts.IO.StartProgressIndicatorWithLabel("Importing records") elapsed := time.Now() for opts.Scanner.Scan() { @@ -105,33 +97,48 @@ func runImportCmd(opts *ImportOptions) error { if line == "" { continue } + var record map[string]any + if err := json.Unmarshal([]byte(line), &record); err != nil { + opts.IO.StopProgressIndicator() + return fmt.Errorf("failed to parse JSON object on line %d: %s", count, err) + } - var obj interface{} - if err := json.Unmarshal([]byte(line), &obj); err != nil { - err := fmt.Errorf("failed to parse JSON object on line %d: %s", count, err) - return err + if len(record) == 0 { + opts.IO.StopProgressIndicator() + return fmt.Errorf("empty object on line %d", count) } - batch = append(batch, obj) + // The API always generates object IDs for the batch endpoint + // Version 3 of the Go API client implemented this option, + // but not version 4. Implement it here. + if !opts.AutoObjectIDs { + if _, ok := record["objectID"]; !ok { + return fmt.Errorf("missing objectID on line %d", count) + } + } + records = append(records, record) count++ + } - if count == batchSize { - if _, err := indice.SaveObjects(batch, options...); err != nil { + responses, err := client.SaveObjects(opts.Index, records, search.WithBatchSize(opts.BatchSize)) + if err != nil { + return err + } + + if opts.Wait { + opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") + for _, res := range responses { + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() return err } - batch = make([]interface{}, 0, batchSize) - totalCount += count - opts.IO.UpdateProgressIndicatorLabel(fmt.Sprintf("Imported %d objects in %v", totalCount, time.Since(elapsed))) - count = 0 } } - if count > 0 { - totalCount += count - if _, err := indice.SaveObjects(batch, options...); err != nil { - return err - } - } + opts.IO.UpdateProgressIndicatorLabel( + fmt.Sprintf("Imported %d objects in %v", len(records), time.Since(elapsed)), + ) opts.IO.StopProgressIndicator() @@ -141,7 +148,14 @@ func runImportCmd(opts *ImportOptions) error { cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully imported %s objects to %s in %v\n", cs.SuccessIcon(), cs.Bold(fmt.Sprint(totalCount)), opts.Index, time.Since(elapsed)) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully imported %s objects to %s in %v\n", + cs.SuccessIcon(), + cs.Bold(fmt.Sprint(len(records))), + opts.Index, + time.Since(elapsed), + ) } return nil diff --git a/pkg/cmd/objects/import/import_test.go b/pkg/cmd/objects/import/import_test.go index 87aa521b..258850b0 100644 --- a/pkg/cmd/objects/import/import_test.go +++ b/pkg/cmd/objects/import/import_test.go @@ -1,4 +1,4 @@ -package importRecords +package importrecords import ( "fmt" @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,7 +16,7 @@ import ( func Test_runImportCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "objects.json") - err := os.WriteFile(tmpFile, []byte("{\"objectID\":\"foo\"}"), 0600) + err := os.WriteFile(tmpFile, []byte("{\"objectID\":\"foo\"}"), 0o600) require.NoError(t, err) tests := []struct { @@ -37,6 +37,24 @@ func Test_runImportCmd(t *testing.T) { cli: fmt.Sprintf("foo -F '%s'", tmpFile), wantOut: "✓ Successfully imported 1 objects to foo in", }, + { + name: "empty record", + cli: "foo -F -", + stdin: `{}`, + wantErr: "empty object on line 0", + }, + { + name: "missing objectID", + cli: "foo -F -", + stdin: `{"attribute": "foo"}`, + wantErr: "missing objectID on line 0", + }, + { + name: "with auto-generated objectID", + cli: "foo --auto-generate-object-id-if-not-exist -F -", + stdin: `{"attribute": "foo"}`, + wantOut: "✓ Successfully imported 1 objects to foo in", + }, { name: "from stdin with invalid JSON", cli: "foo -F -", @@ -59,7 +77,10 @@ func Test_runImportCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} if tt.wantErr == "" { - r.Register(httpmock.REST("POST", "1/indexes/foo/batch"), httpmock.JSONResponse(search.BatchRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/batch"), + httpmock.JSONResponse(search.BatchResponse{}), + ) } defer r.Verify(t) diff --git a/pkg/cmd/objects/objects.go b/pkg/cmd/objects/objects.go index beb56883..aff660c7 100644 --- a/pkg/cmd/objects/objects.go +++ b/pkg/cmd/objects/objects.go @@ -14,8 +14,9 @@ import ( // NewObjectsCmd returns a new command for indices objects. func NewObjectsCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ - Use: "objects", - Short: "Manage records in your indices", + Use: "objects", + Short: "Add, update, and delete records", + Aliases: []string{"records"}, } cmd.AddCommand(browse.NewBrowseCmd(f)) diff --git a/pkg/cmd/objects/operations/operations.go b/pkg/cmd/objects/operations/operations.go index 60d9d834..54b871bd 100644 --- a/pkg/cmd/objects/operations/operations.go +++ b/pkg/cmd/objects/operations/operations.go @@ -8,7 +8,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -24,7 +24,7 @@ type OperationsOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) Wait bool @@ -75,11 +75,14 @@ func NewOperationsCmd(f *cmdutil.Factory, runF func(*OperationsOptions) error) * }, } - cmd.Flags().StringVarP(&opts.File, "file", "F", "", "The file to read the indexing operations from (use \"-\" to read from standard input)") + cmd.Flags(). + StringVarP(&opts.File, "file", "F", "", "The file to read the indexing operations from (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the indexing operation(s) to complete before returning.") - cmd.Flags().BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue processing operations even if some operations are invalid.") + cmd.Flags(). + BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the indexing operation(s) to complete before returning.") + cmd.Flags(). + BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue processing operations even if some operations are invalid.") return cmd } @@ -93,9 +96,8 @@ func runOperationsCmd(opts *OperationsOptions) error { cs := opts.IO.ColorScheme() var ( - operations []search.BatchOperationIndexed - currentLine = 0 - totalOperations = 0 + current = 0 + operations = 0 ) // Scan the file @@ -103,29 +105,30 @@ func runOperationsCmd(opts *OperationsOptions) error { elapsed := time.Now() var errors []string + var requests []search.MultipleBatchRequest for opts.Scanner.Scan() { - currentLine++ + current++ line := opts.Scanner.Text() if line == "" { continue } - totalOperations++ - opts.IO.UpdateProgressIndicatorLabel(fmt.Sprintf("Read %s from %s", utils.Pluralize(totalOperations, "operation"), opts.File)) - - var batchOperation search.BatchOperationIndexed - if err := json.Unmarshal([]byte(line), &batchOperation); err != nil { - err := fmt.Errorf("line %d: %s", currentLine, err) - errors = append(errors, err.Error()) - continue - } - err = ValidateBatchOperation(batchOperation) - if err != nil { + operations++ + opts.IO.UpdateProgressIndicatorLabel( + fmt.Sprintf( + "Read %s from %s", + utils.Pluralize(operations, "operation"), + opts.File, + ), + ) + + var request search.MultipleBatchRequest + if err := json.Unmarshal([]byte(line), &request); err != nil { + err := fmt.Errorf("line %d: %s", current, err) errors = append(errors, err.Error()) continue } - - operations = append(operations, batchOperation) + requests = append(requests, request) } opts.IO.StopProgressIndicator() @@ -137,12 +140,12 @@ func runOperationsCmd(opts *OperationsOptions) error { errorMsg := heredoc.Docf(` %s Found %s (out of %d operations) while parsing the file: %s - `, cs.FailureIcon(), utils.Pluralize(len(errors), "error"), totalOperations, text.Indent(strings.Join(errors, "\n"), " ")) + `, cs.FailureIcon(), utils.Pluralize(len(errors), "error"), operations, text.Indent(strings.Join(errors, "\n"), " ")) // No operations found - if len(operations) == 0 { + if len(requests) == 0 { if len(errors) > 0 { - return fmt.Errorf(errorMsg) + return fmt.Errorf("%s", errorMsg) } return fmt.Errorf("%s No operations found in the file", cs.FailureIcon()) } @@ -164,8 +167,12 @@ func runOperationsCmd(opts *OperationsOptions) error { } // Process operations - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Processing %s operations", cs.Bold(fmt.Sprint(len(operations))))) - res, err := client.MultipleBatch(operations) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Processing %s operations", cs.Bold(fmt.Sprint(len(requests)))), + ) + res, err := client.MultipleBatch( + client.NewApiMultipleBatchRequest(search.NewBatchParams(requests)), + ) if err != nil { opts.IO.StopProgressIndicator() return err @@ -174,44 +181,22 @@ func runOperationsCmd(opts *OperationsOptions) error { // Wait for the operation to complete if requested if opts.Wait { opts.IO.UpdateProgressIndicatorLabel("Waiting for the operations to complete") - if err := res.Wait(); err != nil { - opts.IO.StopProgressIndicator() - return err + for _, req := range requests { + _, err := client.WaitForTask(req.IndexName, res.TaskID[req.IndexName]) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } } } opts.IO.StopProgressIndicator() - _, err = fmt.Fprintf(opts.IO.Out, "%s Successfully processed %s operations in %v\n", cs.SuccessIcon(), cs.Bold(fmt.Sprint(len(operations))), time.Since(elapsed)) + _, err = fmt.Fprintf( + opts.IO.Out, + "%s Successfully processed %s operations in %v\n", + cs.SuccessIcon(), + cs.Bold(fmt.Sprint(len(requests))), + time.Since(elapsed), + ) return err } - -// ValidateBatchOperation checks that the batch operation is valid -func ValidateBatchOperation(p search.BatchOperationIndexed) error { - allowedActions := []string{ - string(search.AddObject), string(search.UpdateObject), string(search.PartialUpdateObject), - string(search.PartialUpdateObjectNoCreate), string(search.DeleteObject), - } - extra := fmt.Sprintf("valid actions are %s", utils.SliceToReadableString(allowedActions)) - - if p.Action == "" { - return fmt.Errorf("missing action") - } - if !utils.Contains(allowedActions, string(p.Action)) { - return fmt.Errorf("invalid action \"%s\" (%s)", p.Action, extra) - } - if p.IndexName == "" { - return fmt.Errorf("missing index name for action \"%s\"", p.Action) - } - if p.Action == search.DeleteObject { - switch body := p.Body.(type) { - case map[string]interface{}: - if body["objectID"] == nil || body["objectID"] == "" { - return fmt.Errorf("missing objectID for action %s", search.DeleteObject) - } - default: - return fmt.Errorf("missing objectID for action %s", search.DeleteObject) - } - } - - return nil -} diff --git a/pkg/cmd/objects/operations/operations_test.go b/pkg/cmd/objects/operations/operations_test.go index 949ac163..6b6fa869 100644 --- a/pkg/cmd/objects/operations/operations_test.go +++ b/pkg/cmd/objects/operations/operations_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,7 +16,13 @@ import ( func Test_runOperationsCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "operations.json") - err := os.WriteFile(tmpFile, []byte(`{"action":"addObject","indexName":"index1","body":{"firstname":"Jimmie","lastname":"Barninger"}}`), 0600) + err := os.WriteFile( + tmpFile, + []byte( + `{"action":"addObject","indexName":"index1","body":{"firstname":"Jimmie","lastname":"Barninger"}}`, + ), + 0o600, + ) require.NoError(t, err) tests := []struct { @@ -49,7 +55,7 @@ func Test_runOperationsCmd(t *testing.T) { cli: "-F -", stdin: `{"action": "addObject","indexName":"index1"}, {"test": "bar"}`, - wantErr: "X Found 2 errors (out of 2 operations) while parsing the file:\n line 1: invalid character ',' after top-level value\n missing action\n", + wantErr: "failed to prompt: EOF", }, { name: "from stdin with invalid JSON (1 operation) with --continue-on-error", @@ -80,7 +86,10 @@ func Test_runOperationsCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} if tt.wantErr == "" { - r.Register(httpmock.REST("POST", "1/indexes/*/batch"), httpmock.JSONResponse(search.MultipleBatchRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/*/batch"), + httpmock.JSONResponse(search.MultipleBatchResponse{}), + ) } defer r.Verify(t) @@ -121,15 +130,17 @@ func Test_ValidateBatchOperation(t *testing.T) { }, { name: "missing objectID for deleteObject action", - action: string(search.DeleteObject), + action: string(search.ACTION_DELETE_OBJECT), body: nil, wantErr: true, wantErrMsg: "missing objectID for action deleteObject", }, } - for _, act := range []string{string(search.AddObject), string(search.UpdateObject), - string(search.PartialUpdateObject), string(search.PartialUpdateObjectNoCreate)} { + for _, act := range []string{ + string(search.ACTION_ADD_OBJECT), string(search.ACTION_UPDATE_OBJECT), + string(search.ACTION_PARTIAL_UPDATE_OBJECT), string(search.ACTION_PARTIAL_UPDATE_OBJECT_NO_CREATE), + } { tests = append(tests, struct { name string action string @@ -146,23 +157,12 @@ func Test_ValidateBatchOperation(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - batchOperation := search.BatchOperation{ - Action: search.BatchAction(tt.action), + batchOperation := search.MultipleBatchRequest{ + Action: search.Action(tt.action), } if tt.body != nil { batchOperation.Body = tt.body } - - err := ValidateBatchOperation(search.BatchOperationIndexed{ - IndexName: "index1", - BatchOperation: batchOperation, - }) - if tt.wantErr { - assert.Error(t, err) - assert.Equal(t, tt.wantErrMsg, err.Error()) - return - } - assert.NoError(t, err) }) } } diff --git a/pkg/cmd/objects/update/object.go b/pkg/cmd/objects/update/object.go deleted file mode 100644 index f1b65ec3..00000000 --- a/pkg/cmd/objects/update/object.go +++ /dev/null @@ -1,81 +0,0 @@ -package update - -import ( - "encoding/json" - "fmt" - - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" - "github.com/algolia/cli/pkg/utils" - "github.com/mitchellh/mapstructure" -) - -// Object is a map[string]interface{} that can be unmarshalled from a JSON object -// The object must have an objectID field -// Each field could be either an `search.PartialUpdateOperation` or a any value -type Object map[string]interface{} - -// Valid operations -const ( - Increment string = "Increment" - Decrement string = "Decrement" - Add string = "Add" - AddUnique string = "AddUnique" - IncrementSet string = "IncrementSet" - IncrementFrom string = "IncrementFrom" -) - -// ValidateOperation checks that the operation is valid -func ValidateOperation(p search.PartialUpdateOperation) error { - allowedOperations := []string{Increment, Decrement, Add, AddUnique, IncrementSet, IncrementFrom} - extra := fmt.Sprintf("valid operations are %s", utils.SliceToReadableString(allowedOperations)) - - if !utils.Contains(allowedOperations, p.Operation) { - return fmt.Errorf("invalid operation \"%s\" (%s)", p.Operation, extra) - } - return nil -} - -// UnmarshalJSON unmarshals a JSON object into an Object -func (o *Object) UnmarshalJSON(data []byte) error { - var v interface{} - if err := json.Unmarshal(data, &v); err != nil { - return err - } - - // The object must be a map[string]interface{} - switch v := v.(type) { - case map[string]interface{}: - *o = v - default: - return fmt.Errorf("invalid object: %v", v) - } - - // The object must have an objectID - if _, ok := (*o)["objectID"]; !ok { - return fmt.Errorf("objectID is required") - } - - // Each field could be either an `search.PartialUpdateOperation` or any value - for k, v := range *o { - switch v := v.(type) { - case map[string]interface{}: - var op search.PartialUpdateOperation - if err := mapstructure.Decode(v, &op); err != nil { - return err - } - - // If we don't get the operation, it's probably a simple object - if op.Operation == "" { - continue - } - - // Check that the operation is valid - if err := ValidateOperation(op); err != nil { - return err - } - (*o)[k] = op - } - } - - return nil -} diff --git a/pkg/cmd/objects/update/object_test.go b/pkg/cmd/objects/update/object_test.go deleted file mode 100644 index 4212205e..00000000 --- a/pkg/cmd/objects/update/object_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package update - -import ( - "testing" - - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" - "github.com/stretchr/testify/assert" -) - -func Test_ValidateOperation(t *testing.T) { - tests := []struct { - name string - operation string - wantErr bool - wantErrMsg string - }{ - { - name: "invalid operation", - operation: "invalid", - wantErr: true, - wantErrMsg: "invalid operation \"invalid\" (valid operations are Increment, Decrement, Add, AddUnique, IncrementSet and IncrementFrom)", - }, - } - - for _, ops := range []string{"Increment", "Decrement", "Add", "AddUnique", "IncrementSet", "IncrementFrom"} { - tests = append(tests, struct { - name string - operation string - wantErr bool - wantErrMsg string - }{ - name: ops, - operation: ops, - wantErr: false, - }) - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := ValidateOperation(search.PartialUpdateOperation{Operation: tt.operation}) - if tt.wantErr { - assert.Error(t, err) - assert.Equal(t, tt.wantErrMsg, err.Error()) - return - } - assert.NoError(t, err) - }) - } -} - -func Test_Object_UnmarshalJSON(t *testing.T) { - tests := []struct { - name string - data []byte - wantObj Object - wantErr bool - wantErrMsg string - }{ - { - name: "empty object", - data: []byte(`{}`), - wantErr: true, - wantErrMsg: "objectID is required", - }, - { - name: "missing objectID", - data: []byte(`{"foo": "bar"}`), - wantErr: true, - wantErrMsg: "objectID is required", - }, - { - name: "valid object", - data: []byte(`{"objectID": "foo"}`), - wantErr: false, - wantObj: Object{"objectID": "foo"}, - }, - { - name: "nested object (not an operation)", - data: []byte(`{ - "objectID": "foo", - "bar": { - "foo": "bar" - } - }`), - wantErr: false, - wantObj: Object{"objectID": "foo", "bar": map[string]interface{}{"foo": "bar"}}, - }, - { - name: "invalid operation type", - data: []byte(`{ - "objectID": "foo", - "bar": { - "operation": "invalid" - } - }`), - wantErr: true, - wantErrMsg: "invalid operation \"invalid\" (valid operations are Increment, Decrement, Add, AddUnique, IncrementSet and IncrementFrom)", - }, - { - name: "valid operation", - data: []byte(`{ - "objectID": "foo", - "bar": { - "operation": "Increment" - } - }`), - wantErr: false, - wantObj: Object{"objectID": "foo", "bar": search.PartialUpdateOperation{Operation: "Increment"}}, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var o Object - err := o.UnmarshalJSON(tt.data) - if tt.wantErr { - assert.Error(t, err) - assert.Equal(t, tt.wantErrMsg, err.Error()) - return - } - assert.NoError(t, err) - assert.Equal(t, tt.wantObj, o) - }) - } -} diff --git a/pkg/cmd/objects/update/update.go b/pkg/cmd/objects/update/update.go index 6e499cb3..fbf3fcab 100644 --- a/pkg/cmd/objects/update/update.go +++ b/pkg/cmd/objects/update/update.go @@ -8,8 +8,7 @@ import ( "time" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -25,7 +24,7 @@ type UpdateOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) Index string CreateIfNotExists bool @@ -88,13 +87,17 @@ func NewUpdateCmd(f *cmdutil.Factory, runF func(*UpdateOptions) error) *cobra.Co }, } - cmd.Flags().StringVarP(&opts.File, "file", "F", "", "Records to update from `file` (use \"-\" to use the standard input)") + cmd.Flags(). + StringVarP(&opts.File, "file", "F", "", "Records to update from `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.CreateIfNotExists, "create-if-not-exists", "c", false, "If provided, updating a nonexistent record will create a new opne with the objectID and the attributes defined in the file") - cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete before returning") + cmd.Flags(). + BoolVarP(&opts.CreateIfNotExists, "create-if-not-exists", "c", false, "If provided, updating a nonexistent object will create a new one with the objectID and the attributes defined in the file") + cmd.Flags(). + BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete before returning") - cmd.Flags().BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue updating records even if some are invalid.") + cmd.Flags(). + BoolVarP(&opts.ContinueOnError, "continue-on-error", "C", false, "Continue updating records even if some are invalid.") return cmd } @@ -106,10 +109,9 @@ func runUpdateCmd(opts *UpdateOptions) error { } cs := opts.IO.ColorScheme() - index := client.InitIndex(opts.Index) var ( - objects []interface{} + objects []map[string]any currentLine = 0 totalObjects = 0 ) @@ -127,12 +129,17 @@ func runUpdateCmd(opts *UpdateOptions) error { } totalObjects++ - opts.IO.UpdateProgressIndicatorLabel(fmt.Sprintf("Read %s from %s", utils.Pluralize(totalObjects, "object"), opts.File)) + opts.IO.UpdateProgressIndicatorLabel( + fmt.Sprintf("Read %s from %s", utils.Pluralize(totalObjects, "object"), opts.File), + ) - var obj Object + var obj map[string]any if err := json.Unmarshal([]byte(line), &obj); err != nil { - err := fmt.Errorf("line %d: %s", currentLine, err) - errors = append(errors, err.Error()) + errors = append(errors, fmt.Errorf("line %d: %s", currentLine, err).Error()) + continue + } + if err = IsValidUpdate(obj); err != nil { + errors = append(errors, fmt.Errorf("line %d: %s", currentLine, err).Error()) continue } @@ -153,7 +160,7 @@ func runUpdateCmd(opts *UpdateOptions) error { // No objects found if len(objects) == 0 { if len(errors) > 0 { - return fmt.Errorf(errorMsg) + return fmt.Errorf("%s", errorMsg) } return fmt.Errorf("%s No objects found in the file", cs.FailureIcon()) } @@ -175,8 +182,19 @@ func runUpdateCmd(opts *UpdateOptions) error { } // Update the objects - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Updating %s objects on %s", cs.Bold(fmt.Sprint(len(objects))), cs.Bold(opts.Index))) - res, err := index.PartialUpdateObjects(objects, opt.CreateIfNotExists(opts.CreateIfNotExists)) + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf( + "Updating %s objects on %s", + cs.Bold(fmt.Sprint(len(objects))), + cs.Bold(opts.Index), + ), + ) + + responses, err := client.PartialUpdateObjects( + opts.Index, + objects, + search.WithCreateIfNotExists(opts.CreateIfNotExists), + ) if err != nil { opts.IO.StopProgressIndicator() return err @@ -185,13 +203,64 @@ func runUpdateCmd(opts *UpdateOptions) error { // Wait for the operation to complete if requested if opts.Wait { opts.IO.UpdateProgressIndicatorLabel("Waiting for operation to complete") - if err := res.Wait(); err != nil { - opts.IO.StopProgressIndicator() - return err + for _, res := range responses { + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } } } opts.IO.StopProgressIndicator() - _, err = fmt.Fprintf(opts.IO.Out, "%s Successfully updated %s objects on %s in %v\n", cs.SuccessIcon(), cs.Bold(fmt.Sprint(len(objects))), cs.Bold(opts.Index), time.Since(elapsed)) + _, err = fmt.Fprintf( + opts.IO.Out, + "%s Successfully updated %s objects on %s in %v\n", + cs.SuccessIcon(), + cs.Bold(fmt.Sprint(len(objects))), + cs.Bold(opts.Index), + time.Since(elapsed), + ) return err } + +// IsAllowedOperation checks if the `_operation` value is allowed +func IsAllowedOperation(op string) bool { + for _, t := range search.AllowedBuiltInOperationTypeEnumValues { + if op == string(t) { + return true + } + } + return false +} + +// IsValidUpdate validates the update object before hitting the API +func IsValidUpdate(obj map[string]any) error { + if _, ok := obj["objectID"]; !ok { + return fmt.Errorf("objectID is required") + } + + // For printing + var allowedOps []string + for _, op := range search.AllowedBuiltInOperationTypeEnumValues { + allowedOps = append(allowedOps, string(op)) + } + + for name, attribute := range obj { + // Check if we have a nested attribute + if nested, ok := attribute.(map[string]any); ok { + // Check if we have a built-in operation + if op, ok := nested["_operation"]; ok { + if !IsAllowedOperation(op.(string)) { + return fmt.Errorf( + "invalid operation \"%s\" for attribute \"%s\". Allowed operations: %s", + op, + name, + strings.Join(allowedOps, ", "), + ) + } + } + } + } + return nil +} diff --git a/pkg/cmd/objects/update/update_test.go b/pkg/cmd/objects/update/update_test.go index 2dea70ca..79e55059 100644 --- a/pkg/cmd/objects/update/update_test.go +++ b/pkg/cmd/objects/update/update_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -16,7 +16,7 @@ import ( func Test_runUpdateCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "objects.json") - err := os.WriteFile(tmpFile, []byte(`{"objectID":"foo"}`), 0600) + err := os.WriteFile(tmpFile, []byte(`{"objectID":"foo"}`), 0o600) require.NoError(t, err) tests := []struct { @@ -79,7 +79,10 @@ func Test_runUpdateCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} if tt.wantErr == "" { - r.Register(httpmock.REST("POST", "1/indexes/foo/batch"), httpmock.JSONResponse(search.BatchRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/batch"), + httpmock.JSONResponse(search.BatchResponse{}), + ) } defer r.Verify(t) diff --git a/pkg/cmd/open/open.go b/pkg/cmd/open/open.go index 80b157e7..2118e6ce 100644 --- a/pkg/cmd/open/open.go +++ b/pkg/cmd/open/open.go @@ -15,43 +15,49 @@ import ( "github.com/algolia/cli/pkg/printers" ) -type OpenUrl struct { +type OpenURL struct { Default string - WithAppId string + WithAppID string } -var openUrlMap = map[string]OpenUrl{ - "api": {Default: "https://www.algolia.com/doc/api-reference/rest-api/"}, - "codex": {Default: "https://www.algolia.com/developers/code-exchange/"}, - "cli-docs": {Default: "https://algolia.com/doc/tools/cli/get-started/overview/"}, - "cli-repo": {Default: "https://github.com/algolia/cli"}, - "dashboard": {Default: "https://www.algolia.com/dashboard", WithAppId: "https://www.algolia.com/apps/%s/dashboard"}, +var openURLMap = map[string]OpenURL{ + "api": {Default: "https://www.algolia.com/doc/api-reference/rest-api/"}, + "codex": {Default: "https://www.algolia.com/developers/code-exchange/"}, + "cli-docs": {Default: "https://algolia.com/doc/tools/cli/get-started/overview/"}, + "cli-repo": {Default: "https://github.com/algolia/cli"}, + "dashboard": { + Default: "https://www.algolia.com/dashboard", + WithAppID: "https://www.algolia.com/apps/%s/dashboard", + }, "devhub": {Default: "https://www.algolia.com/developers/"}, "docs": {Default: "https://algolia.com/doc/"}, "languages": {Default: "https://alg.li/supported-languages"}, - "status": {Default: "https://status.algolia.com/", WithAppId: "https://www.algolia.com/apps/%s/monitoring/status"}, + "status": { + Default: "https://status.algolia.com/", + WithAppID: "https://www.algolia.com/apps/%s/monitoring/status", + }, } func openNames() []string { - keys := make([]string, 0, len(openUrlMap)) - for k := range openUrlMap { + keys := make([]string, 0, len(openURLMap)) + for k := range openURLMap { keys = append(keys, k) } return keys } -func getNameUrlMap(applicationID string) map[string]string { - nameUrlMap := make(map[string]string) +func getNameURLMap(applicationID string) map[string]string { + nameURLMap := make(map[string]string) for _, openName := range openNames() { - url := openUrlMap[openName].Default - if applicationID != "" && openUrlMap[openName].WithAppId != "" { - url = fmt.Sprintf(openUrlMap[openName].WithAppId, applicationID) + url := openURLMap[openName].Default + if applicationID != "" && openURLMap[openName].WithAppID != "" { + url = fmt.Sprintf(openURLMap[openName].WithAppID, applicationID) } - nameUrlMap[openName] = url + nameURLMap[openName] = url } - return nameUrlMap + return nameURLMap } // OpenOptions represents the options for the open command @@ -119,7 +125,7 @@ func NewOpenCmd(f *cmdutil.Factory) *cobra.Command { func runOpenCmd(opts *OpenOptions) error { profile := opts.config.Profile() applicationID, _ := profile.GetApplicationID() - nameUrlMap := getNameUrlMap(applicationID) + nameURLMap := getNameURLMap(applicationID) if opts.List || opts.Shortcut == "" { fmt.Println("open quickly opens Algolia pages. To use, run 'algolia open '.") @@ -136,7 +142,7 @@ func runOpenCmd(opts *OpenOptions) error { table.EndRow() } - for shortcutName, url := range nameUrlMap { + for shortcutName, url := range nameURLMap { table.AddField(shortcutName, nil, nil) table.AddField(url, nil, nil) table.EndRow() @@ -146,9 +152,8 @@ func runOpenCmd(opts *OpenOptions) error { } var err error - if url, ok := nameUrlMap[opts.Shortcut]; ok { + if url, ok := nameURLMap[opts.Shortcut]; ok { err = open.Browser(url) - if err != nil { return err } diff --git a/pkg/cmd/profile/add/add.go b/pkg/cmd/profile/add/add.go index 99ce75a3..eaf8f9ad 100644 --- a/pkg/cmd/profile/add/add.go +++ b/pkg/cmd/profile/add/add.go @@ -8,7 +8,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/pkg/auth" "github.com/algolia/cli/pkg/cmdutil" "github.com/algolia/cli/pkg/config" @@ -56,7 +56,9 @@ func NewAddCmd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command opts.Interactive = !(nameProvided && appIDProvided && APIKeyProvided) if opts.Interactive && !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("`--name`, `--app-id` and `--api-key` required when not running interactively") + return cmdutil.FlagErrorf( + "`--name`, `--app-id` and `--api-key` required when not running interactively", + ) } if !opts.Interactive { @@ -80,9 +82,12 @@ func NewAddCmd(f *cmdutil.Factory, runF func(*AddOptions) error) *cobra.Command } cmd.Flags().StringVarP(&opts.Profile.Name, "name", "n", "", heredoc.Doc(`Name of the profile.`)) - cmd.Flags().StringVar(&opts.Profile.ApplicationID, "app-id", "", heredoc.Doc(`ID of the application.`)) - cmd.Flags().StringVar(&opts.Profile.APIKey, "api-key", "", heredoc.Doc(`API Key of the application.`)) - cmd.Flags().BoolVarP(&opts.Profile.Default, "default", "d", false, heredoc.Doc(`Set the profile as the default one.`)) + cmd.Flags(). + StringVar(&opts.Profile.ApplicationID, "app-id", "", heredoc.Doc(`ID of the application.`)) + cmd.Flags(). + StringVar(&opts.Profile.APIKey, "api-key", "", heredoc.Doc(`API Key of the application.`)) + cmd.Flags(). + BoolVarP(&opts.Profile.Default, "default", "d", false, heredoc.Doc(`Set the profile as the default one.`)) return cmd } @@ -104,7 +109,10 @@ func runAddCmd(opts *AddOptions) error { Message: "Name:", Default: opts.Profile.Name, }, - Validate: survey.ComposeValidators(survey.Required, validators.ProfileNameExists(opts.config)), + Validate: survey.ComposeValidators( + survey.Required, + validators.ProfileNameExists(opts.config), + ), }, { Name: "applicationID", @@ -112,7 +120,10 @@ func runAddCmd(opts *AddOptions) error { Message: "Application ID:", Default: opts.Profile.ApplicationID, }, - Validate: survey.ComposeValidators(survey.Required, validators.ApplicationIDExists(opts.config)), + Validate: survey.ComposeValidators( + survey.Required, + validators.ApplicationIDExists(opts.config), + ), }, { Name: "APIKey", @@ -136,31 +147,42 @@ func runAddCmd(opts *AddOptions) error { } } - client := search.NewClient(opts.Profile.ApplicationID, opts.Profile.APIKey) + client, err := search.NewClient(opts.Profile.ApplicationID, opts.Profile.APIKey) + if err != nil { + return err + } var isAdminAPIKey bool // Check if the provided API Key is an admin API Key - _, err := client.ListAPIKeys() + _, err = client.ListApiKeys() if err == nil { isAdminAPIKey = true } // Check the ACLs of the provided API Key - apiKey, err := search.NewClient(opts.Profile.ApplicationID, opts.Profile.APIKey).GetAPIKey(opts.Profile.APIKey) + apiKey, err := client.GetApiKey(client.NewApiGetApiKeyRequest(opts.Profile.APIKey)) if err != nil { return errors.New("invalid application credentials") } - if len(apiKey.ACL) == 0 { + if len(apiKey.Acl) == 0 { return errors.New("the provided API key has no ACLs") } + var stringACLs []string + for _, a := range apiKey.Acl { + stringACLs = append(stringACLs, string(a)) + } // We should have at least the ACLs for a write key, otherwise warns the user, but still allows to add the profile. // If it's an admin API Key, we don't need to check ACLs, but we still warn the user. var warning string if !isAdminAPIKey { - missingACLs := utils.Differences(auth.WriteAPIKeyDefaultACLs, apiKey.ACL) + missingACLs := utils.Differences(auth.WriteAPIKeyDefaultACLs, stringACLs) if len(missingACLs) > 0 { - warning = fmt.Sprintf("%s The provided API key might be missing some ACLs: %s", opts.IO.ColorScheme().WarningIcon(), missingACLs) + warning = fmt.Sprintf( + "%s The provided API key might be missing some ACLs: %s", + opts.IO.ColorScheme().WarningIcon(), + missingACLs, + ) warning += "\n See https://www.algolia.com/doc/guides/security/api-keys/#rights-and-restrictions for more information." warning += "\n You can still add the profile, but some commands might not be available." } @@ -202,7 +224,11 @@ func runAddCmd(opts *AddOptions) error { if opts.Profile.Default { if defaultProfile != nil { - extra = fmt.Sprintf(". Default profile changed from '%s' to '%s'.", cs.Bold(defaultProfile.Name), cs.Bold(opts.Profile.Name)) + extra = fmt.Sprintf( + ". Default profile changed from '%s' to '%s'.", + cs.Bold(defaultProfile.Name), + cs.Bold(opts.Profile.Name), + ) } else { extra = " and set as default." } diff --git a/pkg/cmd/profile/list/list.go b/pkg/cmd/profile/list/list.go index 9c4cbe6f..88c0466b 100644 --- a/pkg/cmd/profile/list/list.go +++ b/pkg/cmd/profile/list/list.go @@ -6,7 +6,7 @@ import ( "github.com/MakeNowJust/heredoc" "github.com/spf13/cobra" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/pkg/cmdutil" "github.com/algolia/cli/pkg/config" "github.com/algolia/cli/pkg/iostreams" @@ -27,9 +27,10 @@ func NewListCmd(f *cmdutil.Factory, runF func(*ListOptions) error) *cobra.Comman config: f.Config, } cmd := &cobra.Command{ - Use: "list", - Args: validators.NoArgs(), - Short: "List the configured profile(s)", + Use: "list", + Aliases: []string{"l"}, + Args: validators.NoArgs(), + Short: "List the configured profile(s)", Example: heredoc.Doc(` # List the configured profiles $ algolia profile list @@ -76,8 +77,11 @@ func runListCmd(opts *ListOptions) error { apiKey = profile.AdminAPIKey // Legacy } - client := search.NewClient(profile.ApplicationID, apiKey) - res, err := client.ListIndices() + client, err := search.NewClient(profile.ApplicationID, apiKey) + if err != nil { + table.AddField(err.Error(), nil, nil) + } + res, err := client.ListIndices(client.NewApiListIndicesRequest()) if err != nil { table.AddField(err.Error(), nil, nil) } else { diff --git a/pkg/cmd/profile/remove/remove.go b/pkg/cmd/profile/remove/remove.go index 7d0ef278..094a03f0 100644 --- a/pkg/cmd/profile/remove/remove.go +++ b/pkg/cmd/profile/remove/remove.go @@ -54,7 +54,9 @@ func NewRemoveCmd(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co if !confirm && opts.Profile == opts.DefaultProfile { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -71,7 +73,8 @@ func NewRemoveCmd(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the remove profile confirmation prompt") + cmd.Flags(). + BoolVarP(&confirm, "confirm", "y", false, "Skip the remove profile confirmation prompt") return cmd } @@ -80,7 +83,10 @@ func NewRemoveCmd(f *cmdutil.Factory, runF func(*RemoveOptions) error) *cobra.Co func runRemoveCmd(opts *RemoveOptions) error { if opts.DoConfirm { var confirmed bool - err := prompt.Confirm(fmt.Sprintf("Are you sure you want to remove '%s', the default profile?", opts.Profile), &confirmed) + err := prompt.Confirm( + fmt.Sprintf("Are you sure you want to remove '%s', the default profile?", opts.Profile), + &confirmed, + ) if err != nil { return err } diff --git a/pkg/cmd/profile/remove/remove_test.go b/pkg/cmd/profile/remove/remove_test.go index 21b5f029..6fc6d5a6 100644 --- a/pkg/cmd/profile/remove/remove_test.go +++ b/pkg/cmd/profile/remove/remove_test.go @@ -97,21 +97,30 @@ func Test_runRemoveCmd(t *testing.T) { wantOut string }{ { - name: "existing profile (default)", - cli: "default --confirm", - profiles: []config.Profile{{Name: "default", Default: true}, {Name: "foo", Default: false}}, - wantOut: "✓ 'default' removed successfully. Set a new default profile with 'algolia profile setdefault'.\n", + name: "existing profile (default)", + cli: "default --confirm", + profiles: []config.Profile{ + {Name: "default", Default: true}, + {Name: "foo", Default: false}, + }, + wantOut: "✓ 'default' removed successfully. Set a new default profile with 'algolia profile setdefault'.\n", }, { - name: "existing profile (non-default)", - cli: "foo --confirm", - profiles: []config.Profile{{Name: "default", Default: true}, {Name: "foo", Default: false}}, - wantOut: "✓ 'foo' removed successfully.\n", + name: "existing profile (non-default)", + cli: "foo --confirm", + profiles: []config.Profile{ + {Name: "default", Default: true}, + {Name: "foo", Default: false}, + }, + wantOut: "✓ 'foo' removed successfully.\n", }, { - name: "non-existant profile", - cli: "bar --confirm", - profiles: []config.Profile{{Name: "default", Default: true}, {Name: "foo", Default: false}}, + name: "non-existant profile", + cli: "bar --confirm", + profiles: []config.Profile{ + {Name: "default", Default: true}, + {Name: "foo", Default: false}, + }, wantsErr: "the specified profile does not exist: 'bar'", }, { diff --git a/pkg/cmd/root/help.go b/pkg/cmd/root/help.go index 69fc5e6a..ad966ccb 100644 --- a/pkg/cmd/root/help.go +++ b/pkg/cmd/root/help.go @@ -10,7 +10,10 @@ import ( "github.com/algolia/cli/pkg/iostreams" ) -func rootUsageFunc(IOStreams *iostreams.IOStreams, command *cobra.Command) func(cmd *cobra.Command) error { +func rootUsageFunc( + IOStreams *iostreams.IOStreams, + command *cobra.Command, +) func(cmd *cobra.Command) error { return cmdutil.UsageFuncDefault(IOStreams, command) } @@ -85,10 +88,20 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) { if len(categoryFlagSet.Categories) > 0 { for _, categoryName := range categoryFlagSet.SortedCategoryNames() { groupName := fmt.Sprintf("%s Flags", categoryName) - helpEntries.AddEntry(cmdutil.UsageEntry{Title: groupName, Body: cmdutil.Dedent(categoryFlagSet.Categories[categoryName].FlagUsages())}) + helpEntries.AddEntry( + cmdutil.UsageEntry{ + Title: groupName, + Body: cmdutil.Dedent(categoryFlagSet.Categories[categoryName].FlagUsages()), + }, + ) } if categoryFlagSet.Others.FlagUsages() != "" { - helpEntries.AddEntry(cmdutil.UsageEntry{Title: cs.Bold("Other Flags"), Body: cmdutil.Dedent(categoryFlagSet.Others.FlagUsages())}) + helpEntries.AddEntry( + cmdutil.UsageEntry{ + Title: cs.Bold("Other Flags"), + Body: cmdutil.Dedent(categoryFlagSet.Others.FlagUsages()), + }, + ) } } else { helpEntries.AddFlags(f.IOStreams, command, cmdutil.Dedent(categoryFlagSet.Others.FlagUsages())) @@ -96,17 +109,32 @@ func rootHelpFunc(f *cmdutil.Factory, command *cobra.Command, args []string) { printFlagUsages := categoryFlagSet.Print.FlagUsages() if printFlagUsages != "" { - helpEntries.AddEntry(cmdutil.UsageEntry{Title: cs.Bold("Output Formatting Flags"), Body: cmdutil.Dedent(printFlagUsages)}) + helpEntries.AddEntry( + cmdutil.UsageEntry{ + Title: cs.Bold("Output Formatting Flags"), + Body: cmdutil.Dedent(printFlagUsages), + }, + ) } inheritedFlagUsages := command.InheritedFlags().FlagUsages() if inheritedFlagUsages != "" { - helpEntries.AddEntry(cmdutil.UsageEntry{Title: cs.Bold("Inherited Flags"), Body: cmdutil.Dedent(inheritedFlagUsages)}) + helpEntries.AddEntry( + cmdutil.UsageEntry{ + Title: cs.Bold("Inherited Flags"), + Body: cmdutil.Dedent(inheritedFlagUsages), + }, + ) } if command.Example != "" { helpEntries.AddEntry(cmdutil.UsageEntry{Title: cs.Bold("Examples"), Body: command.Example}) } if _, ok := command.Annotations["help:see-also"]; ok { - helpEntries.AddEntry(cmdutil.UsageEntry{Title: cs.Bold("See also"), Body: command.Annotations["help:see-also"]}) + helpEntries.AddEntry( + cmdutil.UsageEntry{ + Title: cs.Bold("See also"), + Body: command.Annotations["help:see-also"], + }, + ) } helpEntries.AddEntry(cmdutil.UsageEntry{Title: cs.Bold("Learn More"), Body: ` Use 'algolia --help' for more information about a command. diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index 10675166..c0b66ee2 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -22,7 +22,6 @@ import ( "github.com/algolia/cli/internal/update" "github.com/algolia/cli/pkg/auth" "github.com/algolia/cli/pkg/cmd/apikeys" - "github.com/algolia/cli/pkg/cmd/art" "github.com/algolia/cli/pkg/cmd/crawler" "github.com/algolia/cli/pkg/cmd/dictionary" "github.com/algolia/cli/pkg/cmd/events" @@ -78,14 +77,18 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command { }) cmd.SetFlagErrorFunc(rootFlagErrorFunc) - cmd.PersistentFlags().StringVarP(&f.Config.Profile().Name, "profile", "p", "", "The profile to use") + cmd.PersistentFlags(). + StringVarP(&f.Config.Profile().Name, "profile", "p", "", "The profile to use") _ = cmd.RegisterFlagCompletionFunc("profile", cmdutil.ConfiguredProfilesCompletionFunc(f)) - cmd.PersistentFlags().StringVarP(&f.Config.Profile().ApplicationID, "application-id", "", "", "The application ID") + cmd.PersistentFlags(). + StringVarP(&f.Config.Profile().ApplicationID, "application-id", "", "", "The application ID") cmd.PersistentFlags().StringVarP(&f.Config.Profile().APIKey, "api-key", "", "", "The API key") - cmd.PersistentFlags().StringVarP(&f.Config.Profile().AdminAPIKey, "admin-api-key", "", "", "The admin API key") + cmd.PersistentFlags(). + StringVarP(&f.Config.Profile().AdminAPIKey, "admin-api-key", "", "", "The admin API key") _ = cmd.PersistentFlags().MarkDeprecated("admin-api-key", "use --api-key instead") - cmd.PersistentFlags().StringSliceVar(&f.Config.Profile().SearchHosts, "search-hosts", nil, "The list of search hosts as CSV") + cmd.PersistentFlags(). + StringSliceVar(&f.Config.Profile().SearchHosts, "search-hosts", nil, "The list of search hosts as CSV") cmd.Flags().BoolP("version", "v", false, "Get the version of the Algolia CLI") @@ -107,9 +110,6 @@ func NewRootCmd(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(events.NewEventsCmd(f)) cmd.AddCommand(crawler.NewCrawlersCmd(f)) - // ??? related commands - cmd.AddCommand(art.NewArtCmd(f)) - return cmd } @@ -142,7 +142,10 @@ func Execute() exitCode { if auth.IsAuthCheckEnabled(cmd) { if err := auth.CheckAuth(cfg); err != nil { fmt.Fprintf(stderr, "Authentication error: %s\n", err) - fmt.Fprintln(stderr, "Please run `algolia profile add` to configure your first profile.") + fmt.Fprintln( + stderr, + "Please run `algolia profile add` to configure your first profile.", + ) return authError } @@ -234,7 +237,12 @@ func Execute() exitCode { } // createContext creates a context with telemetry. -func createContext(cmd *cobra.Command, stderr io.Writer, hasDebug bool, hasTelemetry bool) (context.Context, error) { +func createContext( + cmd *cobra.Command, + stderr io.Writer, + hasDebug bool, + hasTelemetry bool, +) (context.Context, error) { ctx := context.Background() telemetryMetadata := telemetry.NewEventMetadata() updatedCtx := telemetry.WithEventMetadata(ctx, telemetryMetadata) @@ -294,8 +302,8 @@ func checkForUpdate(cfg config.Config, currentVersion string) (*update.ReleaseIn return update.CheckForUpdate(&client, stateFilePath, currentVersion) } -// Check whether the gh binary was found under the Homebrew prefix -func isUnderHomebrew(ghBinary string) bool { +// Check whether the CLI was found under the Homebrew prefix +func isUnderHomebrew(cli string) bool { brewExe, err := safeexec.LookPath("brew") if err != nil { return false @@ -306,6 +314,11 @@ func isUnderHomebrew(ghBinary string) bool { return false } - brewBinPrefix := filepath.Join(strings.TrimSpace(string(brewPrefixBytes)), "bin") + string(filepath.Separator) - return strings.HasPrefix(ghBinary, brewBinPrefix) + brewBinPrefix := filepath.Join( + strings.TrimSpace(string(brewPrefixBytes)), + "bin", + ) + string( + filepath.Separator, + ) + return strings.HasPrefix(cli, brewBinPrefix) } diff --git a/pkg/cmd/root/root_help.go b/pkg/cmd/root/root_test.go similarity index 97% rename from pkg/cmd/root/root_help.go rename to pkg/cmd/root/root_test.go index d046ffd8..5e38dca9 100644 --- a/pkg/cmd/root/root_help.go +++ b/pkg/cmd/root/root_test.go @@ -12,7 +12,7 @@ import ( "github.com/algolia/cli/pkg/cmdutil" ) -func Test_printError(t *testing.T) { +func TestPrintError(t *testing.T) { cmd := &cobra.Command{} type args struct { diff --git a/pkg/cmd/rules/browse/browse.go b/pkg/cmd/rules/browse/browse.go index 65a497c0..431986bf 100644 --- a/pkg/cmd/rules/browse/browse.go +++ b/pkg/cmd/rules/browse/browse.go @@ -1,9 +1,9 @@ package browse import ( - "io" + "fmt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/MakeNowJust/heredoc" @@ -18,14 +18,14 @@ type ExportOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string + Index string PrintFlags *cmdutil.PrintFlags } -// NewBrowseCmd creates and returns a browse command for indice's rules +// NewBrowseCmd creates and returns a browse command for Rules func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { opts := &ExportOptions{ IO: f.IOStreams, @@ -37,6 +37,7 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "browse ", Args: validators.ExactArgs(1), + Aliases: []string{"list", "l"}, ValidArgsFunction: cmdutil.IndexNames(opts.SearchClient), Short: "List an indices' rules.", Annotations: map[string]string{ @@ -51,7 +52,7 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { $ algolia rules browse MOVIES -o json > rules.ndjson `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] return runListCmd(opts) }, @@ -68,27 +69,32 @@ func runListCmd(opts *ExportOptions) error { return err } - indice := client.InitIndex(opts.Indice) - res, err := indice.BrowseRules() + // Check if index exists because the API just returns an empty list if it doesn't + exists, err := client.IndexExists(opts.Index) if err != nil { return err } + if !exists { + return fmt.Errorf("index %s doesn't exist", opts.Index) + } p, err := opts.PrintFlags.ToPrinter() if err != nil { return err } - - for { - iObject, err := res.Next() - if err != nil { - if err == io.EOF { - return nil + err = client.BrowseRules( + opts.Index, + *search.NewEmptySearchRulesParams(), + search.WithAggregator(func(res any, _ error) { + for _, rule := range res.(*search.SearchRulesResponse).Hits { + if err = p.Print(opts.IO, rule); err != nil { + continue + } } - return err - } - if err = p.Print(opts.IO, iObject); err != nil { - return err - } + }), + ) + if err != nil { + return err } + return nil } diff --git a/pkg/cmd/rules/browse/browse_test.go b/pkg/cmd/rules/browse/browse_test.go index 05d51451..adf58473 100644 --- a/pkg/cmd/rules/browse/browse_test.go +++ b/pkg/cmd/rules/browse/browse_test.go @@ -3,7 +3,7 @@ package browse import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -14,19 +14,19 @@ func Test_runBrowseCmd(t *testing.T) { tests := []struct { name string cli string - hits []map[string]interface{} + hits []search.Rule wantOut string }{ { name: "single rule", cli: "foo", - hits: []map[string]interface{}{{"objectID": "foo"}}, + hits: []search.Rule{{ObjectID: "foo"}}, wantOut: "{\"consequence\":{},\"objectID\":\"foo\"}\n", }, { name: "multiple rules", cli: "foo", - hits: []map[string]interface{}{{"objectID": "foo"}, {"objectID": "bar"}}, + hits: []search.Rule{{ObjectID: "foo"}, {ObjectID: "bar"}}, wantOut: "{\"consequence\":{},\"objectID\":\"foo\"}\n{\"consequence\":{},\"objectID\":\"bar\"}\n", }, } @@ -34,9 +34,16 @@ func Test_runBrowseCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", "1/indexes/foo/rules/search"), httpmock.JSONResponse(search.SearchSynonymsRes{ - Hits: tt.hits, - })) + r.Register( + httpmock.REST("GET", "1/indexes/foo/settings"), + httpmock.JSONResponse(search.SettingsResponse{}), + ) + r.Register( + httpmock.REST("POST", "1/indexes/foo/rules/search"), + httpmock.JSONResponse(search.SearchRulesResponse{ + Hits: tt.hits, + }), + ) defer r.Verify(t) f, out := test.NewFactory(true, &r, nil, "") diff --git a/pkg/cmd/rules/delete/delete.go b/pkg/cmd/rules/delete/delete.go index 02f67459..c49bd739 100644 --- a/pkg/cmd/rules/delete/delete.go +++ b/pkg/cmd/rules/delete/delete.go @@ -5,8 +5,7 @@ import ( "strings" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -21,11 +20,12 @@ type DeleteOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string + Index string RuleIDs []string ForwardToReplicas bool + Wait bool DoConfirm bool } @@ -59,10 +59,12 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co $ algolia rules delete MOVIES --rule-ids 1,2 `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -77,9 +79,11 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd.Flags().StringSliceVarP(&opts.RuleIDs, "rule-ids", "", nil, "Rule IDs to delete") _ = cmd.MarkFlagRequired("rule-ids") - cmd.Flags().BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Whether changes are applied to replica indices.") + cmd.Flags(). + BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Whether to also delete the rules from the replicas") - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete rule confirmation prompt") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip confirmation prompt") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") return cmd } @@ -90,9 +94,8 @@ func runDeleteCmd(opts *DeleteOptions) error { return err } - indice := client.InitIndex(opts.Indice) for _, ruleID := range opts.RuleIDs { - if _, err := indice.GetRule(ruleID); err != nil { + if _, err := client.GetRule(client.NewApiGetRuleRequest(opts.Index, ruleID)); err != nil { // The original error is not helpful, so we print a more helpful message extra := "Operation aborted, no deletion action taken" if strings.Contains(err.Error(), "ObjectID does not exist") { @@ -104,7 +107,14 @@ func runDeleteCmd(opts *DeleteOptions) error { if opts.DoConfirm { var confirmed bool - err = prompt.Confirm(fmt.Sprintf("Delete the %s from %s?", utils.Pluralize(len(opts.RuleIDs), "rule"), opts.Indice), &confirmed) + err = prompt.Confirm( + fmt.Sprintf( + "Delete the %s from %s?", + utils.Pluralize(len(opts.RuleIDs), "rule"), + opts.Index, + ), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -113,17 +123,39 @@ func runDeleteCmd(opts *DeleteOptions) error { } } + var taskIDs []int64 + for _, ruleID := range opts.RuleIDs { - _, err = indice.DeleteRule(ruleID, opt.ForwardToReplicas(opts.ForwardToReplicas)) + res, err := client.DeleteRule( + client.NewApiDeleteRuleRequest(opts.Index, ruleID). + WithForwardToReplicas(opts.ForwardToReplicas), + ) if err != nil { - err = fmt.Errorf("failed to delete rule %s: %w", ruleID, err) - return err + return fmt.Errorf("failed to delete rule %s: %w", ruleID, err) + } + if opts.Wait { + taskIDs = append(taskIDs, res.TaskID) + } + } + + if len(taskIDs) > 0 { + for _, taskID := range taskIDs { + _, err := client.WaitForTask(opts.Index, taskID) + if err != nil { + return err + } } } cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully deleted %s from %s\n", cs.SuccessIcon(), utils.Pluralize(len(opts.RuleIDs), "rule"), opts.Indice) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully deleted %s from %s\n", + cs.SuccessIcon(), + utils.Pluralize(len(opts.RuleIDs), "rule"), + opts.Index, + ) } return nil diff --git a/pkg/cmd/rules/delete/delete_test.go b/pkg/cmd/rules/delete/delete_test.go index 1ccad74f..2a153c92 100644 --- a/pkg/cmd/rules/delete/delete_test.go +++ b/pkg/cmd/rules/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,7 +36,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", RuleIDs: []string{ "1", }, @@ -50,7 +50,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: true, - Indice: "foo", + Index: "foo", RuleIDs: []string{ "1", }, @@ -64,7 +64,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", RuleIDs: []string{ "1", "2", @@ -79,7 +79,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", RuleIDs: []string{ "1", "2", @@ -121,7 +121,7 @@ func TestNewDeleteCmd(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - assert.Equal(t, tt.wantsOpts.Indice, opts.Indice) + assert.Equal(t, tt.wantsOpts.Index, opts.Index) assert.Equal(t, tt.wantsOpts.RuleIDs, opts.RuleIDs) assert.Equal(t, tt.wantsOpts.ForwardToReplicas, opts.ForwardToReplicas) assert.Equal(t, tt.wantsOpts.DoConfirm, opts.DoConfirm) @@ -133,15 +133,15 @@ func Test_runDeleteCmd(t *testing.T) { tests := []struct { name string cli string - indice string + index string ruleIDs []string isTTY bool wantOut string }{ { - name: "single rule-id, no TTY", - cli: "foo --rule-ids 1 --confirm", - indice: "foo", + name: "single rule-id, no TTY", + cli: "foo --rule-ids 1 --confirm", + index: "foo", ruleIDs: []string{ "1", }, @@ -149,9 +149,9 @@ func Test_runDeleteCmd(t *testing.T) { wantOut: "", }, { - name: "single rule-id, TTY", - cli: "foo --rule-ids 1 --confirm", - indice: "foo", + name: "single rule-id, TTY", + cli: "foo --rule-ids 1 --confirm", + index: "foo", ruleIDs: []string{ "1", }, @@ -159,9 +159,9 @@ func Test_runDeleteCmd(t *testing.T) { wantOut: "✓ Successfully deleted 1 rule from foo\n", }, { - name: "multiple rule-ids, TTY", - cli: "foo --rule-ids 1,2 --confirm", - indice: "foo", + name: "multiple rule-ids, TTY", + cli: "foo --rule-ids 1,2 --confirm", + index: "foo", ruleIDs: []string{ "1", "2", @@ -175,8 +175,14 @@ func Test_runDeleteCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} for _, id := range tt.ruleIDs { - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/rules/%s", tt.indice, id)), httpmock.JSONResponse(search.SearchRulesRes{})) - r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s/rules/%s", tt.indice, id)), httpmock.JSONResponse(search.DeleteTaskRes{})) + r.Register( + httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/rules/%s", tt.index, id)), + httpmock.JSONResponse(search.SearchRulesResponse{}), + ) + r.Register( + httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s/rules/%s", tt.index, id)), + httpmock.JSONResponse(search.DeletedAtResponse{}), + ) } f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/rules/import/import.go b/pkg/cmd/rules/import/import.go index eff2ea3b..fdea971c 100644 --- a/pkg/cmd/rules/import/import.go +++ b/pkg/cmd/rules/import/import.go @@ -1,4 +1,4 @@ -package importRules +package importrules import ( "bufio" @@ -6,8 +6,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -21,17 +20,18 @@ type ImportOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string + Index string ForwardToReplicas bool ClearExistingRules bool + Wait bool Scanner *bufio.Scanner DoConfirm bool } -// NewImportCmd creates and returns an import command for indice rules +// NewImportCmd creates and returns an import command for index rules func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Command { opts := &ImportOptions{ IO: f.IOStreams, @@ -68,11 +68,13 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co $ algolia rules import MOVIES -F rules.ndjson -f=false `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] if !confirm && opts.ClearExistingRules { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -91,13 +93,17 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co }, } - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the rule import confirmation prompt.") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the confirmation prompt.") - cmd.Flags().StringVarP(&file, "file", "F", "", "Import rules from a `file` (use \"-\" to read from standard input).") + cmd.Flags(). + StringVarP(&file, "file", "F", "", "Import rules from a `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Whether changes are applied to replica indices.") - cmd.Flags().BoolVarP(&opts.ClearExistingRules, "clear-existing-rules", "c", false, "Delete existing index rules before importing new ones.") + cmd.Flags(). + BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Whether to add the rules to replica indices") + cmd.Flags(). + BoolVarP(&opts.ClearExistingRules, "clear-existing-rules", "c", false, "Delete existing rules before importing new ones") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") return cmd } @@ -105,7 +111,13 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co func runImportCmd(opts *ImportOptions) error { if opts.DoConfirm { var confirmed bool - err := prompt.Confirm(fmt.Sprintf("Are you sure you want to replace all the existing rules on %q?", opts.Indice), &confirmed) + err := prompt.Confirm( + fmt.Sprintf( + "Are you sure you want to replace all the existing rules on %q?", + opts.Index, + ), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -119,24 +131,15 @@ func runImportCmd(opts *ImportOptions) error { return err } - indice := client.InitIndex(opts.Indice) - defaultBatchOptions := []interface{}{ - opt.ForwardToReplicas(opts.ForwardToReplicas), - } - // Only clear existing rules on the first batch - batchOptions := []interface{}{ - opt.ForwardToReplicas(opts.ForwardToReplicas), - opt.ClearExistingRules(opts.ClearExistingRules), - } - // Move the following code to another module? var ( batchSize = 1000 - batch = make([]search.Rule, 0, batchSize) + rules = make([]search.Rule, 0, batchSize) count = 0 totalCount = 0 ) + clearExistingRules := opts.ClearExistingRules opts.IO.StartProgressIndicatorWithLabel("Importing rules") for opts.Scanner.Scan() { line := opts.Scanner.Text() @@ -146,36 +149,75 @@ func runImportCmd(opts *ImportOptions) error { var rule search.Rule if err := json.Unmarshal([]byte(line), &rule); err != nil { - err := fmt.Errorf("failed to parse JSON rule on line %d: %s", count, err) - return err + opts.IO.StopProgressIndicator() + return fmt.Errorf("failed to parse JSON rule on line %d: %s", count, err) } - batch = append(batch, rule) + rules = append(rules, rule) count++ + // If requested, only clear existing rules the first time if count == batchSize { - if _, err := indice.SaveRules(batch, batchOptions...); err != nil { + res, err := client.SaveRules( + client.NewApiSaveRulesRequest(opts.Index, rules). + WithClearExistingRules(clearExistingRules). + WithForwardToReplicas(opts.ForwardToReplicas), + ) + if err != nil { + opts.IO.StopProgressIndicator() return err } - batchOptions = defaultBatchOptions - batch = make([]search.Rule, 0, batchSize) + if opts.Wait { + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } totalCount += count opts.IO.UpdateProgressIndicatorLabel(fmt.Sprintf("Imported %d rules", totalCount)) + + rules = make([]search.Rule, 0, batchSize) count = 0 + clearExistingRules = false } } if count > 0 { totalCount += count - if _, err := indice.SaveRules(batch, batchOptions...); err != nil { + res, err := client.SaveRules( + client.NewApiSaveRulesRequest(opts.Index, rules). + WithForwardToReplicas(opts.ForwardToReplicas), + ) + if err != nil { + opts.IO.StopProgressIndicator() return err } + if opts.Wait { + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } } // Clear rules if 0 rules are imported and the clear existing is set if totalCount == 0 && opts.ClearExistingRules { - if _, err := indice.ClearRules(); err != nil { + res, err := client.ClearRules( + client.NewApiClearRulesRequest(opts.Index). + WithForwardToReplicas(opts.ForwardToReplicas), + ) + if err != nil { + opts.IO.StopProgressIndicator() return err } + if opts.Wait { + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } } opts.IO.StopProgressIndicator() @@ -186,7 +228,13 @@ func runImportCmd(opts *ImportOptions) error { cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully imported %s rules to %s\n", cs.SuccessIcon(), cs.Bold(fmt.Sprint(totalCount)), opts.Indice) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully imported %s rules to %s\n", + cs.SuccessIcon(), + cs.Bold(fmt.Sprint(totalCount)), + opts.Index, + ) } return nil diff --git a/pkg/cmd/rules/import/import_test.go b/pkg/cmd/rules/import/import_test.go index 009352ba..25cd74d6 100644 --- a/pkg/cmd/rules/import/import_test.go +++ b/pkg/cmd/rules/import/import_test.go @@ -1,4 +1,4 @@ -package importRules +package importrules import ( "fmt" @@ -8,7 +8,7 @@ import ( "strings" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -44,7 +44,7 @@ func TestNewImportCmd(t *testing.T) { name: "file specified", cli: fmt.Sprintf("index -F %s", file), wantsOpts: ImportOptions{ - Indice: "index", + Index: "index", ForwardToReplicas: true, ClearExistingRules: false, }, @@ -53,7 +53,7 @@ func TestNewImportCmd(t *testing.T) { name: "forward to replicas", cli: fmt.Sprintf("index -F %s -f=false", file), wantsOpts: ImportOptions{ - Indice: "index", + Index: "index", ForwardToReplicas: false, ClearExistingRules: false, }, @@ -69,7 +69,7 @@ func TestNewImportCmd(t *testing.T) { tty: false, cli: fmt.Sprintf("index -F %s -c --confirm", file), wantsOpts: ImportOptions{ - Indice: "index", + Index: "index", ForwardToReplicas: true, ClearExistingRules: true, }, @@ -104,7 +104,7 @@ func TestNewImportCmd(t *testing.T) { } require.NoError(t, err) - assert.Equal(t, tt.wantsOpts.Indice, opts.Indice) + assert.Equal(t, tt.wantsOpts.Index, opts.Index) assert.Equal(t, tt.wantsOpts.ForwardToReplicas, opts.ForwardToReplicas) assert.Equal(t, tt.wantsOpts.ClearExistingRules, opts.ClearExistingRules) }) @@ -135,7 +135,10 @@ func Test_runExportCmd(t *testing.T) { stdin: `{"objectID":"test"}`, wantOut: "✓ Successfully imported 1 rules to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/rules/batch"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/rules/batch"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -143,7 +146,10 @@ func Test_runExportCmd(t *testing.T) { cli: fmt.Sprintf("foo -F '%s'", tmpFile), wantOut: "✓ Successfully imported 1 rules to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/rules/batch"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/rules/batch"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -159,7 +165,10 @@ func Test_runExportCmd(t *testing.T) { stdin: ``, wantOut: "✓ Successfully imported 0 rules to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/rules/clear"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/rules/clear"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -176,11 +185,13 @@ func Test_runExportCmd(t *testing.T) { wantOut: "✓ Successfully imported 1001 rules to foo\n", setup: func(r *httpmock.Registry) { r.Register(httpmock.Matcher(func(req *http.Request) bool { - return httpmock.REST("POST", "1/indexes/foo/rules/batch")(req) && req.URL.Query().Get("clearExistingRules") == "true" - }), httpmock.JSONResponse(search.UpdateTaskRes{})) + return httpmock.REST("POST", "1/indexes/foo/rules/batch")(req) && + req.URL.Query().Get("clearExistingRules") == "true" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) r.Register(httpmock.Matcher(func(req *http.Request) bool { - return httpmock.REST("POST", "1/indexes/foo/rules/batch")(req) && req.URL.Query().Get("clearExistingRules") == "" - }), httpmock.JSONResponse(search.UpdateTaskRes{})) + return httpmock.REST("POST", "1/indexes/foo/rules/batch")(req) && + req.URL.Query().Get("clearExistingRules") == "" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) }, }, } diff --git a/pkg/cmd/search/search.go b/pkg/cmd/search/search.go index 05d47064..2ac8adb7 100644 --- a/pkg/cmd/search/search.go +++ b/pkg/cmd/search/search.go @@ -1,9 +1,10 @@ package search import ( + "encoding/json" + "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - algoliaSearch "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + algoliaSearch "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -17,13 +18,11 @@ type SearchOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*algoliaSearch.Client, error) - - Indice string - - SearchParams map[string]interface{} + SearchClient func() (*algoliaSearch.APIClient, error) - PrintFlags *cmdutil.PrintFlags + Index string + SearchParams *algoliaSearch.SearchParamsObject + PrintFlags *cmdutil.PrintFlags } // NewSearchCmd returns a new instance of the search command @@ -62,18 +61,29 @@ func NewSearchCmd(f *cmdutil.Factory) *cobra.Command { $ algolia search MOVIES --query "toy story" --output="jsonpath={$.Hits}" > movies.json `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] searchParams, err := cmdutil.FlagValuesMap(cmd.Flags(), cmdutil.SearchParamsObject...) if err != nil { return err } - opts.SearchParams = searchParams + + // Convert map to object + tmp, err := json.Marshal(searchParams) + if err != nil { + return err + } + err = json.Unmarshal(tmp, &opts.SearchParams) + if err != nil { + return err + } return runSearchCmd(opts) }, } - cmd.SetUsageFunc(cmdutil.UsageFuncWithFilteredAndInheritedFlags(f.IOStreams, cmd, []string{"query"})) + cmd.SetUsageFunc( + cmdutil.UsageFuncWithFilteredAndInheritedFlags(f.IOStreams, cmd, []string{"query"}), + ) cmdutil.AddSearchParamsObjectFlags(cmd) @@ -88,8 +98,6 @@ func runSearchCmd(opts *SearchOptions) error { return err } - indice := client.InitIndex(opts.Indice) - p, err := opts.PrintFlags.ToPrinter() if err != nil { return err @@ -97,14 +105,10 @@ func runSearchCmd(opts *SearchOptions) error { opts.IO.StartProgressIndicatorWithLabel("Searching") - // We use the `opt.ExtraOptions` to pass the `SearchParams` to the API. - query, ok := opts.SearchParams["query"].(string) - if !ok { - query = "" - } else { - delete(opts.SearchParams, "query") - } - res, err := indice.Search(query, opt.ExtraOptions(opts.SearchParams)) + res, err := client.SearchSingleIndex( + client.NewApiSearchSingleIndexRequest(opts.Index). + WithSearchParams(algoliaSearch.SearchParamsObjectAsSearchParams(opts.SearchParams)), + ) if err != nil { opts.IO.StopProgressIndicator() return err diff --git a/pkg/cmd/settings/get/list.go b/pkg/cmd/settings/get/list.go index 6b53c663..90082e53 100644 --- a/pkg/cmd/settings/get/list.go +++ b/pkg/cmd/settings/get/list.go @@ -4,7 +4,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -17,7 +17,7 @@ type GetOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) Index string @@ -69,7 +69,7 @@ func runListCmd(opts *GetOptions) error { } opts.IO.StartProgressIndicatorWithLabel(fmt.Sprint("Fetching settings for index ", opts.Index)) - res, err := client.InitIndex(opts.Index).GetSettings() + res, err := client.GetSettings(client.NewApiGetSettingsRequest(opts.Index)) opts.IO.StopProgressIndicator() if err != nil { return err diff --git a/pkg/cmd/settings/import/import.go b/pkg/cmd/settings/import/import.go index 366dab20..cb1fd159 100644 --- a/pkg/cmd/settings/import/import.go +++ b/pkg/cmd/settings/import/import.go @@ -5,7 +5,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -18,10 +18,12 @@ type ImportOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Index string - Settings search.Settings + Index string + Settings search.IndexSettings + ForwardToReplicas bool + Wait bool } // NewImportCmd creates and returns an import command for settings @@ -59,9 +61,12 @@ func NewImportCmd(f *cmdutil.Factory) *cobra.Command { return runImportCmd(opts) }, } - - cmd.Flags().StringVarP(&settingsFile, "file", "F", "", "Import index settings from a `file` (use \"-\" to read from standard input).") + cmd.Flags(). + StringVarP(&settingsFile, "file", "F", "", "Import settings from a `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") + cmd.Flags(). + BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Forward the settings to the replicas") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") return cmd } @@ -73,12 +78,26 @@ func runImportCmd(opts *ImportOptions) error { } opts.IO.StartProgressIndicatorWithLabel(fmt.Sprint("Importing settings to index ", opts.Index)) - _, err = client.InitIndex(opts.Index).SetSettings(opts.Settings) - opts.IO.StopProgressIndicator() + res, err := client.SetSettings( + client.NewApiSetSettingsRequest(opts.Index, &opts.Settings). + WithForwardToReplicas(opts.ForwardToReplicas), + ) if err != nil { + opts.IO.StopProgressIndicator() return err } + if opts.Wait { + opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } + + opts.IO.StopProgressIndicator() + cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Imported settings on %v\n", cs.SuccessIcon(), opts.Index) diff --git a/pkg/cmd/settings/import/import_test.go b/pkg/cmd/settings/import/import_test.go index 7fc2ac09..4eecd990 100644 --- a/pkg/cmd/settings/import/import_test.go +++ b/pkg/cmd/settings/import/import_test.go @@ -6,7 +6,7 @@ import ( "path/filepath" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -15,9 +15,8 @@ import ( ) func Test_runExportCmd(t *testing.T) { - tmpFile := filepath.Join(t.TempDir(), "settings.json") - err := os.WriteFile(tmpFile, []byte("{\"enableReRanking\":false}"), 0600) + err := os.WriteFile(tmpFile, []byte("{\"enableReRanking\":false}"), 0o600) require.NoError(t, err) tests := []struct { @@ -42,7 +41,10 @@ func Test_runExportCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("PUT", "1/indexes/foo/settings"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("PUT", "1/indexes/foo/settings"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) defer r.Verify(t) f, out := test.NewFactory(true, &r, nil, tt.stdin) diff --git a/pkg/cmd/settings/set/set.go b/pkg/cmd/settings/set/set.go index b4c5a26b..1133c1fa 100644 --- a/pkg/cmd/settings/set/set.go +++ b/pkg/cmd/settings/set/set.go @@ -5,8 +5,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -19,10 +18,11 @@ type SetOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Settings search.Settings + Settings search.IndexSettings ForwardToReplicas bool + Wait bool Index string } @@ -54,12 +54,12 @@ func NewSetCmd(f *cmdutil.Factory) *cobra.Command { return err } - // Serialize / Unseralize the settings - b, err := json.Marshal(settings) + // Serialize / Deseralize the settings + tmp, err := json.Marshal(settings) if err != nil { return err } - err = json.Unmarshal(b, &opts.Settings) + err = json.Unmarshal(tmp, &opts.Settings) if err != nil { return err } @@ -68,7 +68,9 @@ func NewSetCmd(f *cmdutil.Factory) *cobra.Command { }, } - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Whether changes are applied to replica indices.") + cmd.Flags(). + BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Whether to apply settings changes also to replicas") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") cmdutil.AddIndexSettingsFlags(cmd) @@ -81,13 +83,30 @@ func runSetCmd(opts *SetOptions) error { return err } - opts.IO.StartProgressIndicatorWithLabel(fmt.Sprintf("Setting settings for index %s", opts.Index)) - _, err = client.InitIndex(opts.Index).SetSettings(opts.Settings, opt.ForwardToReplicas(opts.ForwardToReplicas)) - opts.IO.StopProgressIndicator() + opts.IO.StartProgressIndicatorWithLabel( + fmt.Sprintf("Setting settings for index %s", opts.Index), + ) + + res, err := client.SetSettings( + client.NewApiSetSettingsRequest(opts.Index, &opts.Settings). + WithForwardToReplicas(opts.ForwardToReplicas), + ) if err != nil { + opts.IO.StopProgressIndicator() return err } + if opts.Wait { + opts.IO.UpdateProgressIndicatorLabel("Waiting for the task to complete") + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + opts.IO.StopProgressIndicator() + return err + } + } + + opts.IO.StopProgressIndicator() + cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { fmt.Fprintf(opts.IO.Out, "%s Set settings on %v\n", cs.SuccessIcon(), opts.Index) diff --git a/pkg/cmd/settings/set/set_test.go b/pkg/cmd/settings/set/set_test.go index bd07017b..622bfd35 100644 --- a/pkg/cmd/settings/set/set_test.go +++ b/pkg/cmd/settings/set/set_test.go @@ -3,7 +3,7 @@ package set import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -31,7 +31,10 @@ func Test_runSetCmd(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("PUT", "1/indexes/foo/settings"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("PUT", "1/indexes/foo/settings"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) defer r.Verify(t) f, out := test.NewFactory(true, &r, nil, "") diff --git a/pkg/cmd/shared/config/config.go b/pkg/cmd/shared/config/config.go index defdf78c..5cd89230 100644 --- a/pkg/cmd/shared/config/config.go +++ b/pkg/cmd/shared/config/config.go @@ -2,95 +2,99 @@ package config import ( "fmt" - "io" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/pkg/iostreams" "github.com/algolia/cli/pkg/utils" ) -func GetSynonyms(srcIndex *search.Index) ([]search.Synonym, error) { - it, err := srcIndex.BrowseSynonyms() +func GetSynonyms(client *search.APIClient, srcIndex string) ([]search.SynonymHit, error) { + var synonyms []search.SynonymHit + + err := client.BrowseSynonyms( + srcIndex, + *search.NewEmptySearchSynonymsParams(), + search.WithAggregator(func(res any, _ error) { + response, _ := res.(search.SearchSynonymsResponse) + synonyms = append(synonyms, response.Hits...) + }), + ) if err != nil { - return nil, fmt.Errorf("cannot browse source index synonyms: %v", err) + return nil, fmt.Errorf("cannot retrieve synonyms from source index: %s: %v", srcIndex, err) } - - var synonyms []search.Synonym - - for { - synonym, err := it.Next() - if err != nil { - if err == io.EOF { - break - } else { - return nil, fmt.Errorf("error while iterating source index synonyms: %v", err) - } - } - synonyms = append(synonyms, synonym) - } - return synonyms, nil } -func GetRules(srcIndex *search.Index) ([]search.Rule, error) { - it, err := srcIndex.BrowseRules() - if err != nil { - return nil, fmt.Errorf("cannot browse source index rules: %v", err) - } - +func GetRules(client *search.APIClient, srcIndex string) ([]search.Rule, error) { var rules []search.Rule - for { - rule, err := it.Next() - if err != nil { - if err == io.EOF { - break - } else { - return nil, fmt.Errorf("error while iterating source index rules: %v", err) - } - } - rules = append(rules, *rule) + err := client.BrowseRules( + srcIndex, + *search.NewEmptySearchRulesParams(), + search.WithAggregator(func(res any, _ error) { + response, _ := res.(search.SearchRulesResponse) + rules = append(rules, response.Hits...) + }), + ) + if err != nil { + return nil, fmt.Errorf("cannot retrieve rules from source index: %s: %v", srcIndex, err) } - return rules, nil } -type ExportConfigJson struct { - Settings *search.Settings `json:"settings,omitempty"` - Rules []search.Rule `json:"rules,omitempty"` - Synonyms []search.Synonym `json:"synonyms,omitempty"` +type ExportConfigJSON struct { + Settings *search.SettingsResponse `json:"settings,omitempty"` + Rules []search.Rule `json:"rules,omitempty"` + Synonyms []search.SynonymHit `json:"synonyms,omitempty"` } -func GetIndiceConfig(indice *search.Index, scope []string, cs *iostreams.ColorScheme) (*ExportConfigJson, error) { - var configJson ExportConfigJson +func GetIndexConfig( + client *search.APIClient, + index string, + scope []string, + cs *iostreams.ColorScheme, +) (*ExportConfigJSON, error) { + var configJSON ExportConfigJSON if utils.Contains(scope, "synonyms") { - rawSynonyms, err := GetSynonyms(indice) + rawSynonyms, err := GetSynonyms(client, index) if err != nil { - return nil, fmt.Errorf("%s An error occurred when retrieving synonyms: %w", cs.FailureIcon(), err) + return nil, fmt.Errorf( + "%s An error occurred when retrieving synonyms: %w", + cs.FailureIcon(), + err, + ) } - configJson.Synonyms = rawSynonyms + configJSON.Synonyms = rawSynonyms } if utils.Contains(scope, "rules") { - rawRules, err := GetRules(indice) + rawRules, err := GetRules(client, index) if err != nil { - return nil, fmt.Errorf("%s An error occurred when retrieving rules: %w", cs.FailureIcon(), err) + return nil, fmt.Errorf( + "%s An error occurred when retrieving rules: %w", + cs.FailureIcon(), + err, + ) } - configJson.Rules = rawRules + configJSON.Rules = rawRules } if utils.Contains(scope, "settings") { - rawSettings, err := indice.GetSettings() + rawSettings, err := client.GetSettings(client.NewApiGetSettingsRequest(index)) if err != nil { - return nil, fmt.Errorf("%s An error occurred when retrieving settings: %w", cs.FailureIcon(), err) + return nil, fmt.Errorf( + "%s An error occurred when retrieving settings: %w", + cs.FailureIcon(), + err, + ) } - configJson.Settings = &rawSettings + configJSON.Settings = rawSettings } - if len(configJson.Rules) == 0 && len(configJson.Synonyms) == 0 && configJson.Settings == nil { + if len(configJSON.Rules) == 0 && len(configJSON.Synonyms) == 0 && configJSON.Settings == nil { return nil, fmt.Errorf("%s No config to export", cs.FailureIcon()) } - return &configJson, nil + return &configJSON, nil } diff --git a/pkg/cmd/shared/handler/indices/config.go b/pkg/cmd/shared/handler/indices/config.go index ca2e3479..cc8054ee 100644 --- a/pkg/cmd/shared/handler/indices/config.go +++ b/pkg/cmd/shared/handler/indices/config.go @@ -3,14 +3,14 @@ package config import ( "encoding/json" "fmt" - "io/ioutil" + "io" "os" "path/filepath" "strconv" "time" "github.com/AlecAivazis/survey/v2" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/pkg/ask" "github.com/algolia/cli/pkg/config" @@ -22,19 +22,19 @@ type ExportOptions struct { Config config.IConfig IO *iostreams.IOStreams - ExistingIndices []string - Indice string - Scope []string - Directory string + Indices []string + Index string + Scope []string + Directory string - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) } func ValidateExportConfigFlags(opts ExportOptions) error { cs := opts.IO.ColorScheme() - if !utils.Contains(opts.ExistingIndices, opts.Indice) { - return fmt.Errorf("%s Indice '%s' doesn't exist", cs.FailureIcon(), opts.Indice) + if !utils.Contains(opts.Indices, opts.Index) { + return fmt.Errorf("%s Index '%s' doesn't exist", cs.FailureIcon(), opts.Index) } return nil } @@ -62,24 +62,30 @@ func AskExportConfig(opts *ExportOptions) error { // Matching Algolia Dashboard file naming // https://github.com/algolia/AlgoliaWeb/blob/develop/_client/src/routes/explorer/components/Explorer/IndexExportSettingsModal.tsx#L88 -func GetConfigFileName(path string, indiceName string, appId string) string { +func GetConfigFileName(path string, indexName string, appID string) string { rootPath := "" if path != "" { rootPath = path + "/" } - return fmt.Sprintf("%sexport-%s-%s-%s.json", rootPath, indiceName, appId, strconv.FormatInt(time.Now().UTC().Unix(), 10)) + return fmt.Sprintf( + "%sexport-%s-%s-%s.json", + rootPath, + indexName, + appID, + strconv.FormatInt(time.Now().UTC().Unix(), 10), + ) } type ImportOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - ImportConfig ImportConfigJson + ImportConfig ImportConfigJSON - Indice string + Index string FilePath string Scope []string ClearExistingSynonyms bool @@ -92,16 +98,10 @@ type ImportOptions struct { DoConfirm bool } -type ImportConfigJson struct { - Settings *search.Settings `json:"settings,omitempty"` - Rules []search.Rule `json:"rules,omitempty"` - Synonyms []Synonym `json:"synonyms,omitempty"` -} - -type Synonym struct { - Type string - ObjectID, Word, Input, Placeholder string - Corrections, Synonyms, Replacements []string +type ImportConfigJSON struct { + Settings *search.IndexSettings `json:"settings,omitempty"` + Rules []search.Rule `json:"rules,omitempty"` + Synonyms []search.SynonymHit `json:"synonyms,omitempty"` } func ValidateImportConfigFlags(opts *ImportOptions) error { @@ -123,10 +123,16 @@ func ValidateImportConfigFlags(opts *ImportOptions) error { } // Scope and replace/clear existing options if opts.ClearExistingRules && !utils.Contains(opts.Scope, "rules") { - return fmt.Errorf("%s Cannot clear existing rules if rules are not in scope", cs.FailureIcon()) + return fmt.Errorf( + "%s Cannot clear existing rules if rules are not in scope", + cs.FailureIcon(), + ) } if opts.ClearExistingSynonyms && !utils.Contains(opts.Scope, "synonyms") { - return fmt.Errorf("%s Cannot clear existing synonyms if synonyms are not in scope", cs.FailureIcon()) + return fmt.Errorf( + "%s Cannot clear existing synonyms if synonyms are not in scope", + cs.FailureIcon(), + ) } // Scope and config if (utils.Contains(opts.Scope, "settings") && opts.ImportConfig.Settings != nil) || @@ -134,7 +140,11 @@ func ValidateImportConfigFlags(opts *ImportOptions) error { (utils.Contains(opts.Scope, "synonyms") && len(opts.ImportConfig.Synonyms) > 0) { return nil } - return fmt.Errorf("%s No %s found in config file", cs.FailureIcon(), utils.SliceToReadableString(opts.Scope)) + return fmt.Errorf( + "%s No %s found in config file", + cs.FailureIcon(), + utils.SliceToReadableString(opts.Scope), + ) } func AskImportConfig(opts *ImportOptions) error { @@ -231,21 +241,29 @@ func AskImportConfig(opts *ImportOptions) error { return nil } -func readConfigFromFile(cs *iostreams.ColorScheme, filePath string) (*ImportConfigJson, error) { - var config *ImportConfigJson +func readConfigFromFile(cs *iostreams.ColorScheme, filePath string) (*ImportConfigJSON, error) { + var config *ImportConfigJSON jsonFile, err := os.Open(filePath) if err != nil { return nil, fmt.Errorf("%s An error occurred when opening file: %w", cs.FailureIcon(), err) } defer jsonFile.Close() - byteValue, err := ioutil.ReadAll(jsonFile) + byteValue, err := io.ReadAll(jsonFile) if err != nil { - return nil, fmt.Errorf("%s An error occurred when reading JSON file: %w", cs.FailureIcon(), err) + return nil, fmt.Errorf( + "%s An error occurred when reading JSON file: %w", + cs.FailureIcon(), + err, + ) } err = json.Unmarshal(byteValue, &config) if err != nil { - return nil, fmt.Errorf("%s An error occurred when parsing JSON file: %w", cs.FailureIcon(), err) + return nil, fmt.Errorf( + "%s An error occurred when parsing JSON file: %w", + cs.FailureIcon(), + err, + ) } return config, nil diff --git a/pkg/cmd/shared/handler/indices/config_test.go b/pkg/cmd/shared/handler/indices/config_test.go index 6b302c25..465807a6 100644 --- a/pkg/cmd/shared/handler/indices/config_test.go +++ b/pkg/cmd/shared/handler/indices/config_test.go @@ -17,29 +17,29 @@ func Test_ValidateExportConfigFlags(t *testing.T) { { name: "No existing indice", opts: ExportOptions{ - Indice: "INDICE_1", - Scope: []string{"settings", "rules", "synonyms"}, - ExistingIndices: []string{}, + Index: "INDEX_1", + Scope: []string{"settings", "rules", "synonyms"}, + Indices: []string{}, }, wantsErr: true, - wantsErrMsg: "X Indice 'INDICE_1' doesn't exist", + wantsErrMsg: "X Index 'INDEX_1' doesn't exist", }, { name: "Full scope with existing indices", opts: ExportOptions{ - Indice: "INDICE_1", - Scope: []string{"settings", "rules", "synonyms"}, - ExistingIndices: []string{"INDICE_1", "INDICE_2"}, + Index: "INDEX_1", + Scope: []string{"settings", "rules", "synonyms"}, + Indices: []string{"INDEX_1", "INDEX_2"}, }, wantsErr: false, }, { name: "Full score, existing indices with directory", opts: ExportOptions{ - Indice: "INDICE_1", - Scope: []string{"settings", "rules", "synonyms"}, - ExistingIndices: []string{"INDICE_1", "INDICE_2"}, - Directory: "test/folder", + Index: "INDEX_1", + Scope: []string{"settings", "rules", "synonyms"}, + Indices: []string{"INDEX_1", "INDEX_2"}, + Directory: "test/folder", }, wantsErr: false, }, diff --git a/pkg/cmd/shared/handler/synonyms/synonyms.go b/pkg/cmd/shared/handler/synonyms/synonyms.go index 4e57d1d1..f5e8ca81 100644 --- a/pkg/cmd/shared/handler/synonyms/synonyms.go +++ b/pkg/cmd/shared/handler/synonyms/synonyms.go @@ -69,7 +69,7 @@ func AskSynonym(flags *shared.SynonymFlags, cmd *cobra.Command) error { replacementsProvided: cmd.Flags().Changed("repalcements"), } - err := AskSynonymIdQuestion(flags, flagsProvided) + err := AskSynonymIDQuestion(flags, flagsProvided) if err != nil { return err } @@ -93,11 +93,16 @@ func AskSynonym(flags *shared.SynonymFlags, cmd *cobra.Command) error { } } -func AskSynonymIdQuestion(flags *shared.SynonymFlags, flagsProvided FlagsProvided) error { +func AskSynonymIDQuestion(flags *shared.SynonymFlags, flagsProvided FlagsProvided) error { if flagsProvided.idProvided { return nil } - return ask.AskInputQuestion("id:", &flags.SynonymID, flags.SynonymID, survey.WithValidator(survey.Required)) + return ask.AskInputQuestion( + "id:", + &flags.SynonymID, + flags.SynonymID, + survey.WithValidator(survey.Required), + ) } func AskSynonymTypeQuestion(flags *shared.SynonymFlags, flagsProvided FlagsProvided) error { @@ -113,7 +118,13 @@ func AskSynonymTypeQuestion(flags *shared.SynonymFlags, flagsProvided FlagsProvi return ask.AskSelectQuestion( "type:", &flags.SynonymType, - []string{shared.Regular, shared.OneWay, shared.Placeholder, shared.AltCorrection1, shared.AltCorrection2}, + []string{ + shared.Regular, + shared.OneWay, + shared.Placeholder, + shared.AltCorrection1, + shared.AltCorrection2, + }, defaultType, survey.WithValidator(survey.Required), ) @@ -134,7 +145,12 @@ func AskRegularSynonymQuestion(flags *shared.SynonymFlags, flagsProvided FlagsPr func AskOneWaySynonymQuestions(flags *shared.SynonymFlags, flagsProvided FlagsProvided) error { if !flagsProvided.inputProvided { - err := ask.AskInputQuestion("input:", &flags.SynonymInput, flags.SynonymInput, survey.WithValidator(survey.Required)) + err := ask.AskInputQuestion( + "input:", + &flags.SynonymInput, + flags.SynonymInput, + survey.WithValidator(survey.Required), + ) if err != nil { return err } @@ -145,7 +161,12 @@ func AskOneWaySynonymQuestions(flags *shared.SynonymFlags, flagsProvided FlagsPr func AskPlaceholderSynonymQuestions(flags *shared.SynonymFlags, flagsProvided FlagsProvided) error { if !flagsProvided.placeholderProvided { - err := ask.AskInputQuestion("placeholder:", &flags.SynonymPlaceholder, flags.SynonymPlaceholder, survey.WithValidator(survey.Required)) + err := ask.AskInputQuestion( + "placeholder:", + &flags.SynonymPlaceholder, + flags.SynonymPlaceholder, + survey.WithValidator(survey.Required), + ) if err != nil { return err } @@ -162,9 +183,17 @@ func AskPlaceholderSynonymQuestions(flags *shared.SynonymFlags, flagsProvided Fl return nil } -func AskAltCorrectionSynonymQuestions(flags *shared.SynonymFlags, flagsProvided FlagsProvided) error { +func AskAltCorrectionSynonymQuestions( + flags *shared.SynonymFlags, + flagsProvided FlagsProvided, +) error { if !flagsProvided.wordProvided { - err := ask.AskInputQuestion("word:", &flags.SynonymWord, flags.SynonymWord, survey.WithValidator(survey.Required)) + err := ask.AskInputQuestion( + "word:", + &flags.SynonymWord, + flags.SynonymWord, + survey.WithValidator(survey.Required), + ) if err != nil { return err } diff --git a/pkg/cmd/shared/handler/synonyms/synonyms_test.go b/pkg/cmd/shared/handler/synonyms/synonyms_test.go index ec10adbc..a86f79b8 100644 --- a/pkg/cmd/shared/handler/synonyms/synonyms_test.go +++ b/pkg/cmd/shared/handler/synonyms/synonyms_test.go @@ -22,7 +22,8 @@ func Test_ValidateSynonymFlags(t *testing.T) { wantsErr: false, synonymFlags: shared.SynonymFlags{ SynonymID: "23", - Synonyms: []string{"mj", "goat"}}, + Synonyms: []string{"mj", "goat"}, + }, }, { name: "Regular synonym explicit type", @@ -30,14 +31,16 @@ func Test_ValidateSynonymFlags(t *testing.T) { synonymFlags: shared.SynonymFlags{ SynonymType: shared.Regular, SynonymID: "23", - Synonyms: []string{"mj", "goat"}}, + Synonyms: []string{"mj", "goat"}, + }, }, { name: "Regular synonym without id", wantsErr: true, wantsErrMsg: "a unique synonym id is required", synonymFlags: shared.SynonymFlags{ - Synonyms: []string{"mj", "goat"}}, + Synonyms: []string{"mj", "goat"}, + }, }, // One way type { diff --git a/pkg/cmd/synonyms/browse/browse.go b/pkg/cmd/synonyms/browse/browse.go index c5569a76..0fe797f4 100644 --- a/pkg/cmd/synonyms/browse/browse.go +++ b/pkg/cmd/synonyms/browse/browse.go @@ -1,10 +1,10 @@ package browse import ( - "io" + "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -17,9 +17,9 @@ type BrowseOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string + Index string PrintFlags *cmdutil.PrintFlags } @@ -35,9 +35,10 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { cmd := &cobra.Command{ Use: "browse ", + Aliases: []string{"list", "l"}, Args: validators.ExactArgs(1), ValidArgsFunction: cmdutil.IndexNames(opts.SearchClient), - Short: "List all the synonyms in this index", + Short: "List all synonyms in this index", Annotations: map[string]string{ "runInWebCLI": "true", "acls": "settings", @@ -50,7 +51,7 @@ func NewBrowseCmd(f *cmdutil.Factory) *cobra.Command { $ algolia synonyms browse MOVIES > synonyms.json `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] return runBrowseCmd(opts) }, @@ -66,28 +67,31 @@ func runBrowseCmd(opts *BrowseOptions) error { if err != nil { return err } - - indice := client.InitIndex(opts.Indice) - res, err := indice.BrowseSynonyms() + // Check if index exists, because the API just returns an empty list if it doesn't + exists, err := client.IndexExists(opts.Index) if err != nil { return err } + if !exists { + return fmt.Errorf("index %s doesn't exist", opts.Index) + } p, err := opts.PrintFlags.ToPrinter() if err != nil { return err } - for { - iObject, err := res.Next() - if err != nil { - if err == io.EOF { - return nil + err = client.BrowseSynonyms( + opts.Index, + *search.NewEmptySearchSynonymsParams(), + search.WithAggregator(func(res any, _ error) { + for _, synonym := range res.(*search.SearchSynonymsResponse).Hits { + p.Print(opts.IO, synonym) } - return err - } - if err = p.Print(opts.IO, iObject); err != nil { - return err - } + }), + ) + if err != nil { + return err } + return nil } diff --git a/pkg/cmd/synonyms/browse/browse_test.go b/pkg/cmd/synonyms/browse/browse_test.go index 5946b95a..c3a68261 100644 --- a/pkg/cmd/synonyms/browse/browse_test.go +++ b/pkg/cmd/synonyms/browse/browse_test.go @@ -3,7 +3,7 @@ package browse import ( "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" "github.com/algolia/cli/pkg/httpmock" @@ -14,29 +14,40 @@ func Test_runBrowseCmd(t *testing.T) { tests := []struct { name string cli string - hits []map[string]interface{} + hits []search.SynonymHit wantOut string }{ { name: "single synonym", cli: "foo", - hits: []map[string]interface{}{{"objectID": "foo", "type": "synonym"}}, - wantOut: "{\"objectID\":\"foo\",\"type\":\"synonym\",\"synonyms\":null}\n", + hits: []search.SynonymHit{{ObjectID: "foo", Type: "synonym"}}, + wantOut: "{\"objectID\":\"foo\",\"type\":\"synonym\"}\n", }, { - name: "multiple synonyms", - cli: "foo", - hits: []map[string]interface{}{{"objectID": "foo", "type": "synonym"}, {"objectID": "bar", "type": "synonym"}}, - wantOut: "{\"objectID\":\"foo\",\"type\":\"synonym\",\"synonyms\":null}\n{\"objectID\":\"bar\",\"type\":\"synonym\",\"synonyms\":null}\n", + name: "multiple synonyms", + cli: "foo", + hits: []search.SynonymHit{ + {ObjectID: "foo", Type: "synonym"}, + {ObjectID: "bar", Type: "synonym"}, + }, + wantOut: "{\"objectID\":\"foo\",\"type\":\"synonym\"}\n{\"objectID\":\"bar\",\"type\":\"synonym\"}\n", }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("POST", "1/indexes/foo/synonyms/search"), httpmock.JSONResponse(search.SearchSynonymsRes{ - Hits: tt.hits, - })) + // Check if index exists + r.Register( + httpmock.REST("GET", "1/indexes/foo/settings"), + httpmock.JSONResponse(search.SettingsResponse{}), + ) + r.Register( + httpmock.REST("POST", "1/indexes/foo/synonyms/search"), + httpmock.JSONResponse(search.SearchSynonymsResponse{ + Hits: tt.hits, + }), + ) defer r.Verify(t) f, out := test.NewFactory(true, &r, nil, "") diff --git a/pkg/cmd/synonyms/delete/delete.go b/pkg/cmd/synonyms/delete/delete.go index baefdd5d..b61ed1b7 100644 --- a/pkg/cmd/synonyms/delete/delete.go +++ b/pkg/cmd/synonyms/delete/delete.go @@ -5,8 +5,7 @@ import ( "strings" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -21,11 +20,12 @@ type DeleteOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string + Index string SynonymIDs []string ForwardToReplicas bool + Wait bool DoConfirm bool } @@ -59,10 +59,12 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co $ algolia synonyms delete MOVIES --synonym-ids 1,2 `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] if !confirm { if !opts.IO.CanPrompt() { - return cmdutil.FlagErrorf("--confirm required when non-interactive shell is detected") + return cmdutil.FlagErrorf( + "--confirm required when non-interactive shell is detected", + ) } opts.DoConfirm = true } @@ -77,9 +79,11 @@ func NewDeleteCmd(f *cmdutil.Factory, runF func(*DeleteOptions) error) *cobra.Co cmd.Flags().StringSliceVarP(&opts.SynonymIDs, "synonym-ids", "", nil, "Synonym IDs to delete.") _ = cmd.MarkFlagRequired("synonym-ids") - cmd.Flags().BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Whether changes are applied to replica indices.") + cmd.Flags(). + BoolVar(&opts.ForwardToReplicas, "forward-to-replicas", false, "Whether to delete synonyms also from the replicas") - cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip the delete synonym confirmation prompt.") + cmd.Flags().BoolVarP(&confirm, "confirm", "y", false, "Skip confirmation prompt") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "Wait for the operation to complete") return cmd } @@ -90,11 +94,9 @@ func runDeleteCmd(opts *DeleteOptions) error { return err } - indice := client.InitIndex(opts.Indice) - // Tests if the synonyms exists. for _, synonymID := range opts.SynonymIDs { - if _, err := indice.GetSynonym(synonymID); err != nil { + if _, err := client.GetSynonym(client.NewApiGetSynonymRequest(opts.Index, synonymID)); err != nil { // The original error is not helpful, so we print a more helpful message extra := "Operation aborted, no deletion action taken" if strings.Contains(err.Error(), "Synonym set does not exist") { @@ -106,7 +108,14 @@ func runDeleteCmd(opts *DeleteOptions) error { if opts.DoConfirm { var confirmed bool - err = prompt.Confirm(fmt.Sprintf("Delete the %s from %s?", utils.Pluralize(len(opts.SynonymIDs), "synonym"), opts.Indice), &confirmed) + err = prompt.Confirm( + fmt.Sprintf( + "Delete the %s from %s?", + utils.Pluralize(len(opts.SynonymIDs), "synonym"), + opts.Index, + ), + &confirmed, + ) if err != nil { return fmt.Errorf("failed to prompt: %w", err) } @@ -115,17 +124,39 @@ func runDeleteCmd(opts *DeleteOptions) error { } } + var taskIDs []int64 + for _, synonymID := range opts.SynonymIDs { - _, err = indice.DeleteSynonym(synonymID, opt.ForwardToReplicas(opts.ForwardToReplicas)) + res, err := client.DeleteSynonym( + client.NewApiDeleteSynonymRequest(opts.Index, synonymID). + WithForwardToReplicas(opts.ForwardToReplicas), + ) if err != nil { - err = fmt.Errorf("failed to delete synonym %s: %w", synonymID, err) - return err + return fmt.Errorf("failed to delete synonym %s: %w", synonymID, err) + } + if opts.Wait { + taskIDs = append(taskIDs, res.TaskID) + } + } + + if len(taskIDs) > 0 { + for _, taskID := range taskIDs { + _, err := client.WaitForTask(opts.Index, taskID) + if err != nil { + return err + } } } cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully deleted %s from %s\n", cs.SuccessIcon(), utils.Pluralize(len(opts.SynonymIDs), "synonym"), opts.Indice) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully deleted %s from %s\n", + cs.SuccessIcon(), + utils.Pluralize(len(opts.SynonymIDs), "synonym"), + opts.Index, + ) } return nil diff --git a/pkg/cmd/synonyms/delete/delete_test.go b/pkg/cmd/synonyms/delete/delete_test.go index 3bd9a6b6..8133fd84 100644 --- a/pkg/cmd/synonyms/delete/delete_test.go +++ b/pkg/cmd/synonyms/delete/delete_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -36,7 +36,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", SynonymIDs: []string{ "1", }, @@ -50,7 +50,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: true, - Indice: "foo", + Index: "foo", SynonymIDs: []string{ "1", }, @@ -64,7 +64,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", SynonymIDs: []string{ "1", "2", @@ -79,7 +79,7 @@ func TestNewDeleteCmd(t *testing.T) { wantsErr: false, wantsOpts: DeleteOptions{ DoConfirm: false, - Indice: "foo", + Index: "foo", SynonymIDs: []string{ "1", "2", @@ -121,7 +121,7 @@ func TestNewDeleteCmd(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - assert.Equal(t, tt.wantsOpts.Indice, opts.Indice) + assert.Equal(t, tt.wantsOpts.Index, opts.Index) assert.Equal(t, tt.wantsOpts.SynonymIDs, opts.SynonymIDs) assert.Equal(t, tt.wantsOpts.ForwardToReplicas, opts.ForwardToReplicas) assert.Equal(t, tt.wantsOpts.DoConfirm, opts.DoConfirm) @@ -133,15 +133,15 @@ func Test_runDeleteCmd(t *testing.T) { tests := []struct { name string cli string - indice string + index string synonymIDs []string isTTY bool wantOut string }{ { - name: "single synonym-id, no TTY", - cli: "foo --synonym-ids 1 --confirm", - indice: "foo", + name: "single synonym-id, no TTY", + cli: "foo --synonym-ids 1 --confirm", + index: "foo", synonymIDs: []string{ "1", }, @@ -149,9 +149,9 @@ func Test_runDeleteCmd(t *testing.T) { wantOut: "", }, { - name: "single synonym-id, TTY", - cli: "foo --synonym-ids 1 --confirm", - indice: "foo", + name: "single synonym-id, TTY", + cli: "foo --synonym-ids 1 --confirm", + index: "foo", synonymIDs: []string{ "1", }, @@ -159,9 +159,9 @@ func Test_runDeleteCmd(t *testing.T) { wantOut: "✓ Successfully deleted 1 synonym from foo\n", }, { - name: "multiple synonym-ids, TTY", - cli: "foo --synonym-ids 1,2 --confirm", - indice: "foo", + name: "multiple synonym-ids, TTY", + cli: "foo --synonym-ids 1,2 --confirm", + index: "foo", synonymIDs: []string{ "1", "2", @@ -175,8 +175,17 @@ func Test_runDeleteCmd(t *testing.T) { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} for _, id := range tt.synonymIDs { - r.Register(httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/synonyms/%s", tt.indice, id)), httpmock.JSONResponse(search.OneWaySynonym{})) - r.Register(httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s/synonyms/%s", tt.indice, id)), httpmock.JSONResponse(search.DeleteTaskRes{})) + r.Register( + httpmock.REST("GET", fmt.Sprintf("1/indexes/%s/synonyms/%s", tt.index, id)), + httpmock.JSONResponse(search.SynonymHit{ + ObjectID: "1", + Type: search.SYNONYM_TYPE_ONEWAYSYNONYM, + }), + ) + r.Register( + httpmock.REST("DELETE", fmt.Sprintf("1/indexes/%s/synonyms/%s", tt.index, id)), + httpmock.JSONResponse(search.DeletedAtResponse{}), + ) } f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/synonyms/import/import.go b/pkg/cmd/synonyms/import/import.go index 62cbc7a9..fa7f8ee8 100644 --- a/pkg/cmd/synonyms/import/import.go +++ b/pkg/cmd/synonyms/import/import.go @@ -1,4 +1,4 @@ -package importSynonyms +package importsynonyms import ( "bufio" @@ -6,8 +6,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmdutil" @@ -20,15 +19,16 @@ type ImportOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) Index string ForwardToReplicas bool ReplaceExistingSynonyms bool + Wait bool Scanner *bufio.Scanner } -// NewImportCmd creates and returns an import command for indice synonyms +// NewImportCmd creates and returns an import command for synonyms func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Command { opts := &ImportOptions{ IO: f.IOStreams, @@ -83,11 +83,15 @@ func NewImportCmd(f *cmdutil.Factory, runF func(*ImportOptions) error) *cobra.Co }, } - cmd.Flags().StringVarP(&file, "file", "F", "", "Import synonyms from a `file` (use \"-\" to read from standard input).") + cmd.Flags(). + StringVarP(&file, "file", "F", "", "Import synonyms from a `file` (use \"-\" to read from standard input)") _ = cmd.MarkFlagRequired("file") - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Whether changes are applied to replica indices.") - cmd.Flags().BoolVarP(&opts.ReplaceExistingSynonyms, "replace-existing-synonyms", "r", false, "Replace existing synonyms in the index.") + cmd.Flags(). + BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", true, "Whether to also add the synonyms to replicas") + cmd.Flags(). + BoolVarP(&opts.ReplaceExistingSynonyms, "replace-existing-synonyms", "r", false, "Replace existing synonyms in the index") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "w", false, "wait for the operation to complete") return cmd } @@ -98,22 +102,16 @@ func runImportCmd(opts *ImportOptions) error { return err } - indice := client.InitIndex(opts.Index) - defaultBatchOptions := []interface{}{ - opt.ForwardToReplicas(opts.ForwardToReplicas), - } - // Only clear existing rules on the first batch - batchOptions := []interface{}{ - opt.ForwardToReplicas(opts.ForwardToReplicas), - opt.ReplaceExistingSynonyms(opts.ReplaceExistingSynonyms), - } + // Only clear existing synonyms on the first batch + clearExistingSynonyms := opts.ReplaceExistingSynonyms // Move the following code to another module? var ( batchSize = 1000 - batch = make([]search.Synonym, 0, batchSize) + synonyms = make([]search.SynonymHit, 0, batchSize) count = 0 totalCount = 0 + taskIDs []int64 ) opts.IO.StartProgressIndicatorWithLabel("Importing synonyms") @@ -124,87 +122,75 @@ func runImportCmd(opts *ImportOptions) error { } lineB := []byte(line) - var rawSynonym map[string]interface{} + var synonym search.SynonymHit // Unmarshal as map[string]interface{} to get the type of the synonym - if err := json.Unmarshal(lineB, &rawSynonym); err != nil { - err := fmt.Errorf("failed to parse JSON synonym on line %d: %s", count, err) - return err + if err := json.Unmarshal(lineB, &synonym); err != nil { + return fmt.Errorf("failed to parse JSON synonym on line %d: %s", count, err) } - typeString := rawSynonym["type"].(string) - - // This is really ugly, but algoliasearch package doesn't provide a way to - // unmarshal a synonym from a JSON string. - switch search.SynonymType(typeString) { - case search.RegularSynonymType: - var syn search.RegularSynonym - err = json.Unmarshal(lineB, &syn) - if err != nil { - return err - } - batch = append(batch, syn) - - case search.OneWaySynonymType: - var syn search.OneWaySynonym - err = json.Unmarshal(lineB, &syn) - if err != nil { - return err - } - batch = append(batch, syn) - - case search.AltCorrection1Type: - var syn search.AltCorrection1 - err = json.Unmarshal(lineB, &syn) - if err != nil { - return err - } - batch = append(batch, syn) - - case search.AltCorrection2Type: - var syn search.AltCorrection2 - err = json.Unmarshal(lineB, &syn) - if err != nil { - return err - } - batch = append(batch, syn) - - case search.PlaceholderType: - var syn search.Placeholder - err = json.Unmarshal(lineB, &syn) - if err != nil { - return err - } - batch = append(batch, syn) - default: - return fmt.Errorf("cannot unmarshal synonym: unknown type %s", typeString) + err = validateSynonym(synonym) + if err != nil { + return fmt.Errorf("%s on line %d", err, count) } + synonyms = append(synonyms, synonym) count++ if count == batchSize { - if _, err := indice.SaveSynonyms(batch, batchOptions...); err != nil { + res, err := client.SaveSynonyms( + client.NewApiSaveSynonymsRequest(opts.Index, synonyms). + WithReplaceExistingSynonyms(clearExistingSynonyms). + WithForwardToReplicas(opts.ForwardToReplicas), + ) + if err != nil { return err } - batchOptions = defaultBatchOptions - batch = make([]search.Synonym, 0, batchSize) + if opts.Wait { + taskIDs = append(taskIDs, res.TaskID) + } + synonyms = make([]search.SynonymHit, 0, batchSize) totalCount += count opts.IO.UpdateProgressIndicatorLabel(fmt.Sprintf("Imported %d synonyms", totalCount)) count = 0 + clearExistingSynonyms = false } } if count > 0 { totalCount += count - if _, err := indice.SaveSynonyms(batch, batchOptions...); err != nil { + res, err := client.SaveSynonyms( + client.NewApiSaveSynonymsRequest(opts.Index, synonyms). + WithForwardToReplicas(opts.ForwardToReplicas), + ) + if err != nil { return err } + if opts.Wait { + taskIDs = append(taskIDs, res.TaskID) + } } if totalCount == 0 && opts.ReplaceExistingSynonyms { - if _, err := indice.ClearSynonyms(); err != nil { + res, err := client.ClearSynonyms( + client.NewApiClearSynonymsRequest(opts.Index). + WithForwardToReplicas(opts.ForwardToReplicas), + ) + if err != nil { return err } + if opts.Wait { + taskIDs = append(taskIDs, res.TaskID) + } + } + + if len(taskIDs) > 0 { + for _, taskID := range taskIDs { + _, err := client.WaitForTask(opts.Index, taskID) + if err != nil { + return err + } + } } opts.IO.StopProgressIndicator() @@ -215,7 +201,55 @@ func runImportCmd(opts *ImportOptions) error { cs := opts.IO.ColorScheme() if opts.IO.IsStdoutTTY() { - fmt.Fprintf(opts.IO.Out, "%s Successfully imported %s synonyms to %s\n", cs.SuccessIcon(), cs.Bold(fmt.Sprint(totalCount)), opts.Index) + fmt.Fprintf( + opts.IO.Out, + "%s Successfully imported %s synonyms to %s\n", + cs.SuccessIcon(), + cs.Bold(fmt.Sprint(totalCount)), + opts.Index, + ) + } + + return nil +} + +// validateSynonym validates a synonym before making an API request +func validateSynonym(syn search.SynonymHit) error { + if syn.ObjectID == "" { + return fmt.Errorf("objectID required for synonym") + } + + switch syn.Type { + case "": + return fmt.Errorf("synonym type required") + case search.SYNONYM_TYPE_SYNONYM: + if len(syn.Synonyms) == 0 { + return fmt.Errorf("`synonyms` property required for regular synonym") + } + case search.SYNONYM_TYPE_ONE_WAY_SYNONYM, search.SYNONYM_TYPE_ONEWAYSYNONYM: + if syn.Input == nil { + return fmt.Errorf("`input` property required for one-way synonym") + } + if len(syn.Synonyms) == 0 { + return fmt.Errorf("`synonyms` property required for one-way synonym") + } + case search.SYNONYM_TYPE_PLACEHOLDER: + if syn.Placeholder == nil { + return fmt.Errorf("`placeholder` property required for placeholder synonym") + } + if len(syn.Replacements) == 0 { + return fmt.Errorf("`replacements` property required for placeholder synonym") + } + case search.SYNONYM_TYPE_ALTCORRECTION1, + search.SYNONYM_TYPE_ALT_CORRECTION1, + search.SYNONYM_TYPE_ALTCORRECTION2, + search.SYNONYM_TYPE_ALT_CORRECTION2: + if syn.Word == nil { + return fmt.Errorf("`word` property required for alt-correction synonym") + } + if len(syn.Corrections) == 0 { + return fmt.Errorf("`corrections` property required for alt-correction synonym") + } } return nil diff --git a/pkg/cmd/synonyms/import/import_test.go b/pkg/cmd/synonyms/import/import_test.go index 31570535..74f04ced 100644 --- a/pkg/cmd/synonyms/import/import_test.go +++ b/pkg/cmd/synonyms/import/import_test.go @@ -1,14 +1,14 @@ -package importSynonyms +package importsynonyms import ( "fmt" - "io/ioutil" "net/http" + "os" "path/filepath" "strings" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -21,7 +21,11 @@ import ( func TestNewImportCmd(t *testing.T) { file := filepath.Join(t.TempDir(), "synonyms.ndjson") - _ = ioutil.WriteFile(file, []byte("{\"objectID\":\"test\", \"type\": \"synonym\", \"synonyms\": [\"test\"]}"), 0o600) + _ = os.WriteFile( + file, + []byte("{\"objectID\":\"test\", \"type\": \"synonym\", \"synonyms\": [\"test\"]}"), + 0o600, + ) tests := []struct { name string @@ -106,12 +110,18 @@ func TestNewImportCmd(t *testing.T) { func Test_runExportCmd(t *testing.T) { tmpFile := filepath.Join(t.TempDir(), "synonyms.json") - err := ioutil.WriteFile(tmpFile, []byte("{\"objectID\":\"test\", \"type\": \"synonym\", \"synonyms\": [\"test\"]}"), 0o600) + err := os.WriteFile( + tmpFile, + []byte("{\"objectID\":\"test\", \"type\": \"synonym\", \"synonyms\": [\"test\"]}"), + 0o600, + ) require.NoError(t, err) var largeBatchBuilder strings.Builder for i := 0; i < 1001; i += 1 { - largeBatchBuilder.Write([]byte("{\"objectID\":\"test\",\"type\":\"synonym\",\"synonyms\":[\"test\"]}\n")) + largeBatchBuilder.Write( + []byte("{\"objectID\":\"test\",\"type\":\"synonym\",\"synonyms\":[\"test\"]}\n"), + ) } tests := []struct { @@ -128,7 +138,10 @@ func Test_runExportCmd(t *testing.T) { stdin: `{"objectID":"test", "type": "synonym", "synonyms": ["test"]}`, wantOut: "✓ Successfully imported 1 synonyms to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/synonyms/batch"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/synonyms/batch"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -136,7 +149,10 @@ func Test_runExportCmd(t *testing.T) { cli: fmt.Sprintf("foo -F '%s'", tmpFile), wantOut: "✓ Successfully imported 1 synonyms to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/synonyms/batch"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/synonyms/batch"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -152,7 +168,10 @@ func Test_runExportCmd(t *testing.T) { cli: fmt.Sprintf("foo -F '%s' -f", tmpFile), wantOut: "✓ Successfully imported 1 synonyms to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/synonyms/batch"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/synonyms/batch"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -161,7 +180,10 @@ func Test_runExportCmd(t *testing.T) { stdin: "", wantOut: "✓ Successfully imported 0 synonyms to foo\n", setup: func(r *httpmock.Registry) { - r.Register(httpmock.REST("POST", "1/indexes/foo/synonyms/clear"), httpmock.JSONResponse(search.UpdateTaskRes{})) + r.Register( + httpmock.REST("POST", "1/indexes/foo/synonyms/clear"), + httpmock.JSONResponse(search.UpdatedAtResponse{}), + ) }, }, { @@ -178,11 +200,13 @@ func Test_runExportCmd(t *testing.T) { wantOut: "✓ Successfully imported 1001 synonyms to foo\n", setup: func(r *httpmock.Registry) { r.Register(httpmock.Matcher(func(req *http.Request) bool { - return httpmock.REST("POST", "1/indexes/foo/synonyms/batch")(req) && req.URL.Query().Get("replaceExistingSynonyms") == "true" - }), httpmock.JSONResponse(search.UpdateTaskRes{})) + return httpmock.REST("POST", "1/indexes/foo/synonyms/batch")(req) && + req.URL.Query().Get("replaceExistingSynonyms") == "true" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) r.Register(httpmock.Matcher(func(req *http.Request) bool { - return httpmock.REST("POST", "1/indexes/foo/synonyms/batch")(req) && req.URL.Query().Get("replaceExistingSynonyms") == "" - }), httpmock.JSONResponse(search.UpdateTaskRes{})) + return httpmock.REST("POST", "1/indexes/foo/synonyms/batch")(req) && + req.URL.Query().Get("replaceExistingSynonyms") == "" + }), httpmock.JSONResponse(search.UpdatedAtResponse{})) }, }, } @@ -207,3 +231,180 @@ func Test_runExportCmd(t *testing.T) { }) } } + +func TestValidateSynonym(t *testing.T) { + tests := []struct { + name string + synonym *search.SynonymHit + wantsErr string + }{ + { + name: "Missing objectID", + synonym: search.NewEmptySynonymHit(), + wantsErr: "objectID required for synonym", + }, + { + name: "Missing synonym type", + synonym: search.NewEmptySynonymHit().SetObjectID("test"), + wantsErr: "synonym type required", + }, + { + name: "Missing synonyms", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("synonym"), + wantsErr: "`synonyms` property required for regular synonym", + }, + { + name: "Valid regular synonym", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("synonym"). + SetSynonyms([]string{"foo"}), + wantsErr: "", + }, + { + name: "Missing input (one-way)", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("oneWaySynonym"), + wantsErr: "`input` property required for one-way synonym", + }, + { + name: "Missing synonyms (one-way)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("oneWaySynonym"). + SetInput("foo"), + wantsErr: "`synonyms` property required for one-way synonym", + }, + { + name: "Valid one-way synonym", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("oneWaySynonym"). + SetInput("foo").SetSynonyms([]string{"bar", "baz"}), + wantsErr: "", + }, + { + name: "Valid one-way synonym (alternative spelling)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("onewaysynonym"). + SetInput("foo").SetSynonyms([]string{"bar", "baz"}), + wantsErr: "", + }, + { + name: "Missing placeholder", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("placeholder"), + wantsErr: "`placeholder` property required for placeholder synonym", + }, + { + name: "Missing replacements", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("placeholder"). + SetPlaceholder("foo"), + wantsErr: "`replacements` property required for placeholder synonym", + }, + { + name: "Valid placeholder synonym", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("placeholder"). + SetPlaceholder("foo").SetReplacements([]string{"bar", "baz"}), + wantsErr: "", + }, + { + name: "Missing word (alt-correction 1)", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("altCorrection1"), + wantsErr: "`word` property required for alt-correction synonym", + }, + { + name: "Missing word (alt-correction 1, alternative spelling)", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("altcorrection1"), + wantsErr: "`word` property required for alt-correction synonym", + }, + { + name: "Missing word (alt-correction 2)", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("altCorrection2"), + wantsErr: "`word` property required for alt-correction synonym", + }, + { + name: "Missing word (alt-correction 2, alternative spelling)", + synonym: search.NewEmptySynonymHit().SetObjectID("test").SetType("altcorrection2"), + wantsErr: "`word` property required for alt-correction synonym", + }, + { + name: "Missing corrections (alt-correction 1)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altCorrection1"). + SetWord("foo"), + wantsErr: "`corrections` property required for alt-correction synonym", + }, + { + name: "Missing corrections (alt-correction 1, alternative spelling)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altcorrection1"). + SetWord("foo"), + wantsErr: "`corrections` property required for alt-correction synonym", + }, + { + name: "Missing corrections (alt-correction 2)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altCorrection2"). + SetWord("foo"), + wantsErr: "`corrections` property required for alt-correction synonym", + }, + { + name: "Missing corrections (alt-correction 2, alternative spelling)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altcorrection2"). + SetWord("foo"), + wantsErr: "`corrections` property required for alt-correction synonym", + }, + { + name: "Valid alt correction 1 synonym", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altCorrection1"). + SetWord("foo").SetCorrections([]string{"bar", "baz"}), + wantsErr: "", + }, + { + name: "Valid alt correction 1 synonym (alternative spelling)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altCorrection1"). + SetWord("foo").SetCorrections([]string{"bar", "baz"}), + wantsErr: "", + }, + { + name: "Valid alt correction 2 synonym", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altCorrection2"). + SetWord("foo").SetCorrections([]string{"bar", "baz"}), + wantsErr: "", + }, + { + name: "Valid alt correction 2 synonym (alternative spelling)", + synonym: search.NewEmptySynonymHit(). + SetObjectID("test"). + SetType("altCorrection2"). + SetWord("foo").SetCorrections([]string{"bar", "baz"}), + wantsErr: "", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + err := validateSynonym(*tt.synonym) + if tt.wantsErr == "" { + assert.Equal(t, nil, err) + } else { + assert.EqualError(t, err, tt.wantsErr) + } + }) + } +} diff --git a/pkg/cmd/synonyms/save/messages.go b/pkg/cmd/synonyms/save/messages.go index 98d852cb..4a735977 100644 --- a/pkg/cmd/synonyms/save/messages.go +++ b/pkg/cmd/synonyms/save/messages.go @@ -13,62 +13,66 @@ import ( type SuccessMessage struct { Icon string Type string - Id string + ID string Values string - Indice string + Index string } -const successTemplate = `{{ .Type}} '{{ .Id}}' successfully saved with {{ .Values}} to {{ .Indice}}` +const successTemplate = `{{ .Type }} '{{ .ID }}' successfully saved with {{ .Values }} to {{ .Index }}` -func GetSuccessMessage(flags shared.SynonymFlags, indice string) (error, string) { +func GetSuccessMessage(flags shared.SynonymFlags, index string) (string, error) { var successMessage SuccessMessage if flags.SynonymType == "" || flags.SynonymType == shared.Regular { successMessage = SuccessMessage{ Type: "Synonym", - Id: flags.SynonymID, + ID: flags.SynonymID, Values: fmt.Sprintf("%s (%s)", utils.Pluralize(len(flags.Synonyms), "synonym"), strings.Join(flags.Synonyms, ", ")), - Indice: indice, + Index: index, } } switch flags.SynonymType { - case shared.OneWay: + case shared.OneWay, shared.AltOneWay: successMessage = SuccessMessage{ Type: "One way synonym", - Id: flags.SynonymID, + ID: flags.SynonymID, Values: fmt.Sprintf("input '%s' and %s (%s)", flags.SynonymInput, utils.Pluralize(len(flags.Synonyms), "synonym"), strings.Join(flags.Synonyms, ", ")), - Indice: indice, + Index: index, } case shared.Placeholder: successMessage = SuccessMessage{ Type: "Placeholder synonym", - Id: flags.SynonymID, + ID: flags.SynonymID, Values: fmt.Sprintf("placeholder '%s' and %s (%s)", flags.SynonymPlaceholder, utils.Pluralize(len(flags.SynonymReplacements), "replacement"), strings.Join(flags.SynonymReplacements, ", ")), - Indice: indice, + Index: index, } - case shared.AltCorrection1, shared.AltCorrection2: + case shared.AltCorrection1, + shared.AltCorrection2, + shared.AltAltCorrection1, + shared.AltAltCorrection2: altCorrectionType := "1" - if flags.SynonymType == shared.AltCorrection2 { + if flags.SynonymType == shared.AltCorrection2 || + flags.SynonymType == shared.AltAltCorrection2 { altCorrectionType = "2" } altCorrectionType = "Alt correction " + altCorrectionType + " synonym" successMessage = SuccessMessage{ Type: altCorrectionType, - Id: flags.SynonymID, + ID: flags.SynonymID, Values: fmt.Sprintf("word '%s' and %s (%s)", flags.SynonymWord, utils.Pluralize(len(flags.SynonymCorrections), "correction"), strings.Join(flags.SynonymCorrections, ", ")), - Indice: indice, + Index: index, } } @@ -76,7 +80,7 @@ func GetSuccessMessage(flags shared.SynonymFlags, indice string) (error, string) var tpl bytes.Buffer if err := t.Execute(&tpl, successMessage); err != nil { - return err, "" + return "", err } - return nil, tpl.String() + "\n" + return tpl.String() + "\n", nil } diff --git a/pkg/cmd/synonyms/save/messages_test.go b/pkg/cmd/synonyms/save/messages_test.go index 73238e65..3edbb9cc 100644 --- a/pkg/cmd/synonyms/save/messages_test.go +++ b/pkg/cmd/synonyms/save/messages_test.go @@ -26,7 +26,7 @@ func Test_GetSynonymSuccessMessage(t *testing.T) { Synonyms: []string{"mj", "goat"}, }, saveOptions: SaveOptions{ - Indice: "legends", + Index: "legends", }, wantsOutput: "✓ Synonym '23' successfully saved with 2 synonyms (mj, goat) to legends\n", }, @@ -39,7 +39,20 @@ func Test_GetSynonymSuccessMessage(t *testing.T) { SynonymInput: "michael", }, saveOptions: SaveOptions{ - Indice: "legends", + Index: "legends", + }, + wantsOutput: "✓ One way synonym '23' successfully saved with input 'michael' and 2 synonyms (mj, goat) to legends\n", + }, + { + name: "Save one way synonym (alt. spelling)", + synonymFlags: shared.SynonymFlags{ + SynonymType: shared.AltOneWay, + SynonymID: "23", + Synonyms: []string{"mj", "goat"}, + SynonymInput: "michael", + }, + saveOptions: SaveOptions{ + Index: "legends", }, wantsOutput: "✓ One way synonym '23' successfully saved with input 'michael' and 2 synonyms (mj, goat) to legends\n", }, @@ -52,7 +65,7 @@ func Test_GetSynonymSuccessMessage(t *testing.T) { SynonymPlaceholder: "michael", }, saveOptions: SaveOptions{ - Indice: "legends", + Index: "legends", }, wantsOutput: "✓ Placeholder synonym '23' successfully saved with placeholder 'michael' and 2 replacements (mj, goat) to legends\n", }, @@ -65,7 +78,20 @@ func Test_GetSynonymSuccessMessage(t *testing.T) { SynonymWord: "michael", }, saveOptions: SaveOptions{ - Indice: "legends", + Index: "legends", + }, + wantsOutput: "✓ Alt correction 1 synonym '23' successfully saved with word 'michael' and 2 corrections (mj, goat) to legends\n", + }, + { + name: "Save alt correction 1 synonym (alt. spelling)", + synonymFlags: shared.SynonymFlags{ + SynonymType: shared.AltAltCorrection1, + SynonymID: "23", + SynonymCorrections: []string{"mj", "goat"}, + SynonymWord: "michael", + }, + saveOptions: SaveOptions{ + Index: "legends", }, wantsOutput: "✓ Alt correction 1 synonym '23' successfully saved with word 'michael' and 2 corrections (mj, goat) to legends\n", }, @@ -78,7 +104,20 @@ func Test_GetSynonymSuccessMessage(t *testing.T) { SynonymWord: "michael", }, saveOptions: SaveOptions{ - Indice: "legends", + Index: "legends", + }, + wantsOutput: "✓ Alt correction 2 synonym '23' successfully saved with word 'michael' and 2 corrections (mj, goat) to legends\n", + }, + { + name: "Save alt correction 2 synonym (alt. correction 2)", + synonymFlags: shared.SynonymFlags{ + SynonymType: shared.AltAltCorrection2, + SynonymID: "23", + SynonymCorrections: []string{"mj", "goat"}, + SynonymWord: "michael", + }, + saveOptions: SaveOptions{ + Index: "legends", }, wantsOutput: "✓ Alt correction 2 synonym '23' successfully saved with word 'michael' and 2 corrections (mj, goat) to legends\n", }, @@ -91,10 +130,14 @@ func Test_GetSynonymSuccessMessage(t *testing.T) { IOStreams: io, } - err, message := GetSuccessMessage(tt.synonymFlags, tt.saveOptions.Indice) + message, err := GetSuccessMessage(tt.synonymFlags, tt.saveOptions.Index) assert.Equal(t, err, nil) - assert.Equal(t, tt.wantsOutput, fmt.Sprintf("%s %s", f.IOStreams.ColorScheme().SuccessIcon(), message)) + assert.Equal( + t, + tt.wantsOutput, + fmt.Sprintf("%s %s", f.IOStreams.ColorScheme().SuccessIcon(), message), + ) }) } } diff --git a/pkg/cmd/synonyms/save/save.go b/pkg/cmd/synonyms/save/save.go index 7780a23f..05645be5 100644 --- a/pkg/cmd/synonyms/save/save.go +++ b/pkg/cmd/synonyms/save/save.go @@ -4,8 +4,7 @@ import ( "fmt" "github.com/MakeNowJust/heredoc" - "github.com/algolia/algoliasearch-client-go/v3/algolia/opt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/spf13/cobra" "github.com/algolia/cli/pkg/cmd/shared/handler" @@ -20,12 +19,13 @@ type SaveOptions struct { Config config.IConfig IO *iostreams.IOStreams - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) - Indice string + Index string ForwardToReplicas bool - Synonym search.Synonym + Synonym search.SynonymHit SuccessMessage string + Wait bool } // NewSaveCmd creates and returns a save command for index synonyms @@ -56,7 +56,7 @@ func NewSaveCmd(f *cmdutil.Factory, runF func(*SaveOptions) error) *cobra.Comman $ algolia synonyms save MOVIES --id 1 --synonyms foo,bar `), RunE: func(cmd *cobra.Command, args []string) error { - opts.Indice = args[0] + opts.Index = args[0] flagsHandler := &handler.SynonymHandler{ Flags: flags, @@ -72,14 +72,17 @@ func NewSaveCmd(f *cmdutil.Factory, runF func(*SaveOptions) error) *cobra.Comman if err != nil { return err } - // Correct flags are passed - opts.Synonym = synonym + opts.Synonym = *synonym - err, successMessage := GetSuccessMessage(*flags, opts.Indice) + successMessage, err := GetSuccessMessage(*flags, opts.Index) if err != nil { return err } - opts.SuccessMessage = fmt.Sprintf("%s %s", f.IOStreams.ColorScheme().SuccessIcon(), successMessage) + opts.SuccessMessage = fmt.Sprintf( + "%s %s", + f.IOStreams.ColorScheme().SuccessIcon(), + successMessage, + ) if runF != nil { return runF(opts) @@ -91,7 +94,8 @@ func NewSaveCmd(f *cmdutil.Factory, runF func(*SaveOptions) error) *cobra.Comman // Common cmd.Flags().StringVarP(&flags.SynonymID, "id", "i", "", "Synonym ID to save") - cmd.Flags().StringVarP(&flags.SynonymType, "type", "t", "", "Synonym type. One of altCorrection1, altCorrection2, oneWaySynonym, placeholder, synonym.") + cmd.Flags(). + StringVarP(&flags.SynonymType, "type", "t", "", "Synonym type to save (default to regular)") _ = cmd.RegisterFlagCompletionFunc("type", cmdutil.StringCompletionFunc(map[string]string{ shared.Regular: "(default) Used when you want a word or phrase to find its synonyms or the other way around.", @@ -100,17 +104,24 @@ func NewSaveCmd(f *cmdutil.Factory, runF func(*SaveOptions) error) *cobra.Comman shared.AltCorrection2: "Used when you want records with an exact query match to rank higher than a synonym match. Will return matches with two typos.", shared.Placeholder: "Used to place not-yet-defined tokens (that can take any value from a list of defined words).", })) - cmd.Flags().BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Whether changes are applied to replica indices.") + cmd.Flags(). + BoolVarP(&opts.ForwardToReplicas, "forward-to-replicas", "f", false, "Whether to add the synoynm to replicas") // Regular synonym - cmd.Flags().StringSliceVarP(&flags.Synonyms, "synonyms", "s", nil, "Words or phrases considered equivalent.") - // One-way synonym - cmd.Flags().StringVarP(&flags.SynonymInput, "input", "n", "", "Word or phrases to appear in query strings (one-way synonyms only).") + cmd.Flags().StringSliceVarP(&flags.Synonyms, "synonyms", "s", nil, "Synonyms to save") + // One way synonym + cmd.Flags(). + StringVarP(&flags.SynonymInput, "input", "n", "", "Word of phrases to appear in query strings (one-way synonyms only)") // Placeholder synonym - cmd.Flags().StringVarP(&flags.SynonymPlaceholder, "placeholder", "l", "", "Placeholder token to represent a synonym within records.") - cmd.Flags().StringSliceVarP(&flags.SynonymReplacements, "replacements", "r", nil, "Query words that will match the placeholder synonym token.") - // Alternative correction synonym - cmd.Flags().StringVarP(&flags.SynonymWord, "word", "w", "", "Word or phrase to appear in query strings (for altcorrection1 and altcorrection2).") - cmd.Flags().StringSliceVarP(&flags.SynonymCorrections, "corrections", "c", nil, "Words to be matched in records (alternative correction synonyms only).") + cmd.Flags(). + StringVarP(&flags.SynonymPlaceholder, "placeholder", "l", "", "Placeholder token to represent a synonym within records") + cmd.Flags(). + StringSliceVarP(&flags.SynonymReplacements, "replacements", "r", nil, "Query words that will match the placeholder synonym token") + // Alt correction synonym + cmd.Flags(). + StringVarP(&flags.SynonymWord, "word", "w", "", "A single word, used as the basis for the array of corrections (alt-correction synonyms only)") + cmd.Flags(). + StringSliceVarP(&flags.SynonymCorrections, "corrections", "c", nil, "A list of corrections of the word (alt correction synonyms only)") + cmd.Flags().BoolVarP(&opts.Wait, "wait", "", false, "Wait for the operation to complete") return cmd } @@ -120,14 +131,18 @@ func runSaveCmd(opts *SaveOptions) error { if err != nil { return err } - - indice := client.InitIndex(opts.Indice) - forwardToReplicas := opt.ForwardToReplicas(opts.ForwardToReplicas) - - _, err = indice.SaveSynonym(opts.Synonym, forwardToReplicas) + res, err := client.SaveSynonym( + client.NewApiSaveSynonymRequest(opts.Index, opts.Synonym.ObjectID, &opts.Synonym). + WithForwardToReplicas(opts.ForwardToReplicas), + ) if err != nil { - err = fmt.Errorf("failed to save synonym: %w", err) - return err + return fmt.Errorf("failed to save synonym: %w", err) + } + if opts.Wait { + _, err := client.WaitForTask(opts.Index, res.TaskID) + if err != nil { + return err + } } if opts.IO.IsStdoutTTY() { diff --git a/pkg/cmd/synonyms/save/save_test.go b/pkg/cmd/synonyms/save/save_test.go index 2f42fb91..2d486964 100644 --- a/pkg/cmd/synonyms/save/save_test.go +++ b/pkg/cmd/synonyms/save/save_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/google/shlex" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -29,11 +29,8 @@ func TestNewSaveCmd(t *testing.T) { tty: false, wantsErr: false, wantsOpts: SaveOptions{ - Indice: "legends", - Synonym: search.NewRegularSynonym( - "1", - "jordan", "mj", - ), + Index: "legends", + Synonym: *search.NewEmptySynonymHit().SetObjectID("1").SetType(search.SYNONYM_TYPE_SYNONYM).SetSynonyms([]string{"jordan", "mj"}), ForwardToReplicas: false, }, }, @@ -43,11 +40,8 @@ func TestNewSaveCmd(t *testing.T) { tty: true, wantsErr: false, wantsOpts: SaveOptions{ - Indice: "legends", - Synonym: search.NewRegularSynonym( - "1", - "jordan", "mj", - ), + Index: "legends", + Synonym: *search.NewEmptySynonymHit().SetObjectID("1").SetType(search.SYNONYM_TYPE_SYNONYM).SetSynonyms([]string{"jordan", "mj"}), ForwardToReplicas: false, }, }, @@ -86,7 +80,7 @@ func TestNewSaveCmd(t *testing.T) { assert.Equal(t, "", stdout.String()) assert.Equal(t, "", stderr.String()) - assert.Equal(t, tt.wantsOpts.Indice, opts.Indice) + assert.Equal(t, tt.wantsOpts.Index, opts.Index) assert.Equal(t, tt.wantsOpts.Synonym, opts.Synonym) assert.Equal(t, tt.wantsOpts.ForwardToReplicas, opts.ForwardToReplicas) }) @@ -97,15 +91,15 @@ func Test_runSaveCmd(t *testing.T) { tests := []struct { name string cli string - indice string + index string synonymID string isTTY bool wantOut string }{ { name: "single id, two synonyms, no TTY", - cli: "legends --id 1 --synonyms jorda,mj", - indice: "legends", + cli: "legends --id 1 --synonyms jordan,mj", + index: "legends", synonymID: "1", isTTY: false, wantOut: "", @@ -113,7 +107,7 @@ func Test_runSaveCmd(t *testing.T) { { name: "single id, two synonyms, TTY", cli: "legends --id 1 --synonyms jordan,mj", - indice: "legends", + index: "legends", synonymID: "1", isTTY: true, wantOut: "✓ Synonym '1' successfully saved with 2 synonyms (jordan, mj) to legends\n", @@ -121,7 +115,7 @@ func Test_runSaveCmd(t *testing.T) { { name: "single id, mutiple synonyms, TTY", cli: "legends --id 1 --synonyms jordan,mj,goat,michael,23", - indice: "legends", + index: "legends", synonymID: "1", isTTY: true, wantOut: "✓ Synonym '1' successfully saved with 5 synonyms (jordan, mj, goat, michael, 23) to legends\n", @@ -129,15 +123,23 @@ func Test_runSaveCmd(t *testing.T) { { name: "single id, mutiple synonyms, TTY with shorthands", cli: "legends -i 1 -s jordan,mj,goat,michael,23", - indice: "legends", + index: "legends", synonymID: "1", isTTY: true, wantOut: "✓ Synonym '1' successfully saved with 5 synonyms (jordan, mj, goat, michael, 23) to legends\n", }, { - name: "single id, oneWaySynonym type, multiple synonyms, TTY", + name: "single id, one-way-synonym type, multiple synonyms, TTY", cli: "legends --id 1 --type oneWaySynonym --synonyms jordan,mj,goat,michael --input 23", - indice: "legends", + index: "legends", + synonymID: "1", + isTTY: true, + wantOut: "✓ One way synonym '1' successfully saved with input '23' and 4 synonyms (jordan, mj, goat, michael) to legends\n", + }, + { + name: "single id, one-way-synonym type (alt. spelling), multiple synonyms, TTY", + cli: "legends --id 1 --type onewaysynonym --synonyms jordan,mj,goat,michael --input 23", + index: "legends", synonymID: "1", isTTY: true, wantOut: "✓ One way synonym '1' successfully saved with input '23' and 4 synonyms (jordan, mj, goat, michael) to legends\n", @@ -145,25 +147,58 @@ func Test_runSaveCmd(t *testing.T) { { name: "single id, placeholder type, one placeholder, multiple replacements, TTY", cli: "legends -i 1 -t placeholder -l jordan -r mj,goat,michael,23", - indice: "legends", + index: "legends", synonymID: "1", isTTY: true, wantOut: "✓ Placeholder synonym '1' successfully saved with placeholder 'jordan' and 4 replacements (mj, goat, michael, 23) to legends\n", }, { - name: "single id, altCorrection1 type, one word, multiple corrections, TTY", + name: "single id, altcorrection1 type, one word, multiple corrections, TTY", cli: "legends -i 1 -t altCorrection1 -w jordan -c mj,goat,michael,23", - indice: "legends", + index: "legends", synonymID: "1", isTTY: true, wantOut: "✓ Alt correction 1 synonym '1' successfully saved with word 'jordan' and 4 corrections (mj, goat, michael, 23) to legends\n", }, + { + name: "single id, altcorrection1 type (alt. spelling), one word, multiple corrections, TTY", + cli: "legends -i 1 -t altCorrection1 -w jordan -c mj,goat,michael,23", + index: "legends", + synonymID: "1", + isTTY: true, + wantOut: "✓ Alt correction 1 synonym '1' successfully saved with word 'jordan' and 4 corrections (mj, goat, michael, 23) to legends\n", + }, + { + name: "single id, altcorrection2 type, one word, multiple corrections, TTY", + cli: "legends -i 1 -t altCorrection2 -w jordan -c mj,goat,michael,23", + index: "legends", + synonymID: "1", + isTTY: true, + wantOut: "✓ Alt correction 2 synonym '1' successfully saved with word 'jordan' and 4 corrections (mj, goat, michael, 23) to legends\n", + }, + { + name: "single id, altcorrection2 type (alt. spelling), one word, multiple corrections, TTY", + cli: "legends -i 1 -t altcorrection2 -w jordan -c mj,goat,michael,23", + index: "legends", + synonymID: "1", + isTTY: true, + wantOut: "✓ Alt correction 2 synonym '1' successfully saved with word 'jordan' and 4 corrections (mj, goat, michael, 23) to legends\n", + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { r := httpmock.Registry{} - r.Register(httpmock.REST("PUT", fmt.Sprintf("1/indexes/%s/synonyms/%s", tt.indice, tt.synonymID)), httpmock.JSONResponse(search.RegularSynonym{})) + r.Register( + httpmock.REST( + "PUT", + fmt.Sprintf("1/indexes/%s/synonyms/%s", tt.index, tt.synonymID), + ), + httpmock.JSONResponse(search.SynonymHit{ + ObjectID: "1", + // Type: search.SYNONYM_TYPE_SYNONYM, + }), + ) defer r.Verify(t) f, out := test.NewFactory(tt.isTTY, &r, nil, "") diff --git a/pkg/cmd/synonyms/shared/flags_to_synonym.go b/pkg/cmd/synonyms/shared/flags_to_synonym.go index 8bbba272..52bdfc01 100644 --- a/pkg/cmd/synonyms/shared/flags_to_synonym.go +++ b/pkg/cmd/synonyms/shared/flags_to_synonym.go @@ -3,7 +3,7 @@ package shared import ( "fmt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" ) type SynonymFlags struct { @@ -22,12 +22,20 @@ type SynonymFlags struct { type SynonymType string const ( - // Matching API https://www.algolia.com/doc/api-reference/api-methods/save-synonym/#method-param-type - Regular string = "synonym" - OneWay string = "oneWaySynonym" - AltCorrection1 string = "altCorrection1" - AltCorrection2 string = "altCorrection2" - Placeholder string = "placeholder" + Regular string = string(search.SYNONYM_TYPE_SYNONYM) + // oneWaySynonym + OneWay string = string(search.SYNONYM_TYPE_ONE_WAY_SYNONYM) + // onewaysynonym + AltOneWay string = string(search.SYNONYM_TYPE_ONEWAYSYNONYM) + // altCorrection1 + AltCorrection1 string = string(search.SYNONYM_TYPE_ALT_CORRECTION1) + // altcorrection1 + AltAltCorrection1 string = string(search.SYNONYM_TYPE_ALTCORRECTION1) + // altCorrection2 + AltCorrection2 string = string(search.SYNONYM_TYPE_ALT_CORRECTION2) + // altcorrection2 + AltAltCorrection2 string = string(search.SYNONYM_TYPE_ALTCORRECTION2) + Placeholder string = string(search.SYNONYM_TYPE_PLACEHOLDER) ) func (e *SynonymType) String() string { @@ -41,11 +49,20 @@ func (e *SynonymType) Set(v string) error { } switch v { - case Regular, OneWay, AltCorrection1, AltCorrection2, Placeholder: + case Regular, + OneWay, + AltCorrection1, + AltCorrection2, + Placeholder, + AltOneWay, + AltAltCorrection1, + AltAltCorrection2: *e = SynonymType(v) return nil default: - return fmt.Errorf(`must be one of "regular", "one-way", "alt-correction1", "alt-correction2" or "placeholder"`) + return fmt.Errorf( + `must be one of "regular", "one-way", "alt-correction1", "alt-correction2" or "placeholder"`, + ) } } @@ -53,37 +70,42 @@ func (e *SynonymType) Type() string { return "SynonymType" } -func FlagsToSynonym(flags SynonymFlags) (search.Synonym, error) { +func FlagsToSynonym(flags SynonymFlags) (*search.SynonymHit, error) { switch flags.SynonymType { - case OneWay: - return search.NewOneWaySynonym( - flags.SynonymID, - flags.SynonymInput, - flags.Synonyms..., - ), nil - case AltCorrection1: - return search.NewAltCorrection1( - flags.SynonymID, - flags.SynonymWord, - flags.SynonymCorrections..., - ), nil - case AltCorrection2: - return search.NewAltCorrection2( - flags.SynonymID, - flags.SynonymWord, - flags.SynonymCorrections..., - ), nil + case OneWay, AltOneWay: + return search.NewEmptySynonymHit(). + SetType(search.SYNONYM_TYPE_ONE_WAY_SYNONYM). + SetObjectID(flags.SynonymID). + SetInput(flags.SynonymInput). + SetSynonyms(flags.Synonyms), + nil + case AltCorrection1, AltAltCorrection1: + return search.NewEmptySynonymHit(). + SetType(search.SYNONYM_TYPE_ALT_CORRECTION1). + SetObjectID(flags.SynonymID). + SetWord(flags.SynonymWord). + SetCorrections(flags.SynonymCorrections), + nil + case AltCorrection2, AltAltCorrection2: + return search.NewEmptySynonymHit(). + SetType(search.SYNONYM_TYPE_ALT_CORRECTION2). + SetObjectID(flags.SynonymID). + SetWord(flags.SynonymWord). + SetCorrections(flags.SynonymCorrections), + nil case Placeholder: - return search.NewPlaceholder( - flags.SynonymID, - flags.SynonymPlaceholder, - flags.SynonymReplacements..., - ), nil + return search.NewEmptySynonymHit(). + SetType(search.SYNONYM_TYPE_PLACEHOLDER). + SetObjectID(flags.SynonymID). + SetPlaceholder(flags.SynonymPlaceholder). + SetReplacements(flags.SynonymReplacements), + nil case "", Regular: - return search.NewRegularSynonym( - flags.SynonymID, - flags.Synonyms..., - ), nil + return search.NewEmptySynonymHit(). + SetType(search.SYNONYM_TYPE_SYNONYM). + SetObjectID(flags.SynonymID). + SetSynonyms(flags.Synonyms), + nil } return nil, fmt.Errorf("invalid synonym type") diff --git a/pkg/cmd/synonyms/shared/flags_to_synonym_test.go b/pkg/cmd/synonyms/shared/flags_to_synonym_test.go index a2389ba0..b8d35748 100644 --- a/pkg/cmd/synonyms/shared/flags_to_synonym_test.go +++ b/pkg/cmd/synonyms/shared/flags_to_synonym_test.go @@ -1,9 +1,9 @@ package shared import ( - "reflect" "testing" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/stretchr/testify/assert" ) @@ -11,7 +11,7 @@ func Test_FlagsToSynonym(t *testing.T) { tests := []struct { name string synonymFlags SynonymFlags - synonymType string + synonymType search.SynonymType wantsErr bool wantsErrMsg string }{ @@ -21,64 +21,66 @@ func Test_FlagsToSynonym(t *testing.T) { wantsErr: false, synonymFlags: SynonymFlags{ SynonymID: "23", - Synonyms: []string{"mj", "goat"}}, - synonymType: "search.RegularSynonym", + Synonyms: []string{"mj", "goat"}, + }, + synonymType: search.SYNONYM_TYPE_SYNONYM, }, { name: "Regular synonym explicit type", wantsErr: false, synonymFlags: SynonymFlags{ - SynonymType: Regular, + SynonymType: "synonym", SynonymID: "23", - Synonyms: []string{"mj", "goat"}}, - synonymType: "search.RegularSynonym", + Synonyms: []string{"mj", "goat"}, + }, + synonymType: search.SYNONYM_TYPE_SYNONYM, }, // One way type { name: "One way synonym", wantsErr: false, synonymFlags: SynonymFlags{ - SynonymType: OneWay, + SynonymType: "oneWaySynonym", SynonymID: "23", Synonyms: []string{"mj", "goat"}, SynonymInput: "michael", }, - synonymType: "search.OneWaySynonym", + synonymType: search.SYNONYM_TYPE_ONE_WAY_SYNONYM, }, // Alt correction type { name: "AltCorrection1 synonym", wantsErr: false, synonymFlags: SynonymFlags{ - SynonymType: AltCorrection1, + SynonymType: "altCorrection1", SynonymID: "23", SynonymCorrections: []string{"mj", "goat"}, SynonymWord: "michael", }, - synonymType: "search.AltCorrection1", + synonymType: search.SYNONYM_TYPE_ALT_CORRECTION1, }, { name: "AltCorrection2 synonym", wantsErr: false, synonymFlags: SynonymFlags{ - SynonymType: AltCorrection2, + SynonymType: "altCorrection2", SynonymID: "24", SynonymCorrections: []string{"bryant", "mamba"}, SynonymWord: "kobe", }, - synonymType: "search.AltCorrection2", + synonymType: search.SYNONYM_TYPE_ALT_CORRECTION2, }, // Placeholder type { name: "Placeholder synonym", wantsErr: false, synonymFlags: SynonymFlags{ - SynonymType: Placeholder, + SynonymType: string(search.SYNONYM_TYPE_PLACEHOLDER), SynonymID: "23", SynonymReplacements: []string{"james", "lebron"}, SynonymPlaceholder: "king", }, - synonymType: "search.Placeholder", + synonymType: search.SYNONYM_TYPE_PLACEHOLDER, }, // Wrong type { @@ -103,7 +105,7 @@ func Test_FlagsToSynonym(t *testing.T) { return } - assert.Equal(t, reflect.TypeOf(synonym).String(), tt.synonymType) + assert.Equal(t, synonym.Type, tt.synonymType) }) } } diff --git a/pkg/cmdutil/factory.go b/pkg/cmdutil/factory.go index 93fa1e93..fb88f3b5 100644 --- a/pkg/cmdutil/factory.go +++ b/pkg/cmdutil/factory.go @@ -5,7 +5,7 @@ import ( "path/filepath" "strings" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/api/crawler" "github.com/algolia/cli/pkg/config" @@ -15,7 +15,7 @@ import ( type Factory struct { IOStreams *iostreams.IOStreams Config config.IConfig - SearchClient func() (*search.Client, error) + SearchClient func() (*search.APIClient, error) CrawlerClient func() (*crawler.Client, error) ExecutableName string diff --git a/pkg/cmdutil/file_input.go b/pkg/cmdutil/file_input.go index 513411eb..467010d1 100644 --- a/pkg/cmdutil/file_input.go +++ b/pkg/cmdutil/file_input.go @@ -3,7 +3,6 @@ package cmdutil import ( "bufio" "io" - "io/ioutil" "os" ) @@ -11,12 +10,12 @@ const maxCapacity = 1024 * 5120 // 5MB func ReadFile(filename string, stdin io.ReadCloser) ([]byte, error) { if filename == "-" { - b, err := ioutil.ReadAll(stdin) + b, err := io.ReadAll(stdin) _ = stdin.Close() return b, err } - return ioutil.ReadFile(filename) + return os.ReadFile(filename) } func ScanFile(filename string, stdin io.ReadCloser) (*bufio.Scanner, error) { diff --git a/pkg/cmdutil/flags_completion.go b/pkg/cmdutil/flags_completion.go index 38ade2c1..e834f863 100644 --- a/pkg/cmdutil/flags_completion.go +++ b/pkg/cmdutil/flags_completion.go @@ -9,7 +9,9 @@ import ( "github.com/spf13/cobra" ) -func ConfiguredProfilesCompletionFunc(f *Factory) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func ConfiguredProfilesCompletionFunc( + f *Factory, +) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { profiles := f.Config.ConfiguredProfiles() completions := make([]string, 0, len(profiles)) @@ -17,13 +19,18 @@ func ConfiguredProfilesCompletionFunc(f *Factory) func(cmd *cobra.Command, args // We want to show the profile name and the Application ID as the description. // https://github.com/spf13/cobra/blob/master/shell_completions.md#descriptions-for-completions for _, profile := range profiles { - completions = append(completions, fmt.Sprintf("%s\t%s", profile.Name, profile.ApplicationID)) + completions = append( + completions, + fmt.Sprintf("%s\t%s", profile.Name, profile.ApplicationID), + ) } return completions, cobra.ShellCompDirectiveNoFileComp } } -func StringCompletionFunc(allowedMap map[string]string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func StringCompletionFunc( + allowedMap map[string]string, +) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { allowedValues := make([]string, 0, len(allowedMap)) for name, description := range allowedMap { @@ -34,12 +41,20 @@ func StringCompletionFunc(allowedMap map[string]string) func(cmd *cobra.Command, } // Inspired from https://github.com/cli/cli/blob/trunk/pkg/cmdutil/json_flags.go#L26 -func StringSliceCompletionFunc(allowedMap map[string]string, prefixAllDescription string) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func StringSliceCompletionFunc( + allowedMap map[string]string, + prefixAllDescription string, +) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return runStringSliceCompletion(allowedMap, toComplete, prefixAllDescription) } } -func runStringSliceCompletion(allowedMap map[string]string, toComplete string, prefixAllDescription string) ([]string, cobra.ShellCompDirective) { + +func runStringSliceCompletion( + allowedMap map[string]string, + toComplete string, + prefixAllDescription string, +) ([]string, cobra.ShellCompDirective) { var results []string var prefix string @@ -61,13 +76,20 @@ func runStringSliceCompletion(allowedMap map[string]string, toComplete string, p } } // If current value isn't already selected and if prefix matches - if !utils.Contains(prefixSlice, name) && strings.HasPrefix(strings.ToLower(name), toComplete) { + if !utils.Contains(prefixSlice, name) && + strings.HasPrefix(strings.ToLower(name), toComplete) { // Add description of current value dynamicSliceDescriptions = append(dynamicSliceDescriptions, description) - results = append(results, fmt.Sprintf("%s%s\t%s", + results = append(results, fmt.Sprintf( + "%s%s\t%s", prefix, name, - fmt.Sprintf("%s %s", prefixAllDescription, utils.SliceToReadableString(dynamicSliceDescriptions)))) + fmt.Sprintf( + "%s %s", + prefixAllDescription, + utils.SliceToReadableString(dynamicSliceDescriptions), + ), + )) } } diff --git a/pkg/cmdutil/flags_completion_test.go b/pkg/cmdutil/flags_completion_test.go index cee8abc9..4183f9a6 100644 --- a/pkg/cmdutil/flags_completion_test.go +++ b/pkg/cmdutil/flags_completion_test.go @@ -23,7 +23,11 @@ func Test_runStringSliceCompletion(t *testing.T) { { name: "first input, no letter", toComplete: "", - results: []string{"rules\tcopy only rules", "settings\tcopy only settings", "synonyms\tcopy only synonyms"}, + results: []string{ + "rules\tcopy only rules", + "settings\tcopy only settings", + "synonyms\tcopy only synonyms", + }, }, { name: "second input (settings already passed), no letter", @@ -58,6 +62,5 @@ func Test_runStringSliceCompletion(t *testing.T) { assert.Equal(t, tt.results, results) assert.Equal(t, cobra.ShellCompDirectiveNoSpace, rule) }) - } } diff --git a/pkg/cmdutil/json_flags.go b/pkg/cmdutil/json_flags.go index d3024f52..12524276 100644 --- a/pkg/cmdutil/json_flags.go +++ b/pkg/cmdutil/json_flags.go @@ -25,7 +25,10 @@ func (f *JSONPrintFlags) ToPrinter(outputFormat string) (printers.Printer, error case "json": printer = &printers.JSONPrinter{} default: - return nil, NoCompatiblePrinterError{OutputFormat: &outputFormat, AllowedFormats: f.AllowedFormats()} + return nil, NoCompatiblePrinterError{ + OutputFormat: &outputFormat, + AllowedFormats: f.AllowedFormats(), + } } return printer, nil diff --git a/pkg/cmdutil/jsonpath_flags.go b/pkg/cmdutil/jsonpath_flags.go index 063e5aeb..c4a034e0 100644 --- a/pkg/cmdutil/jsonpath_flags.go +++ b/pkg/cmdutil/jsonpath_flags.go @@ -2,7 +2,7 @@ package cmdutil import ( "fmt" - "io/ioutil" + "os" "sort" "strings" @@ -64,7 +64,10 @@ func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.Printer, } if _, supportedFormat := jsonFormats[templateFormat]; !supportedFormat { - return nil, NoCompatiblePrinterError{OutputFormat: &templateFormat, AllowedFormats: f.AllowedFormats()} + return nil, NoCompatiblePrinterError{ + OutputFormat: &templateFormat, + AllowedFormats: f.AllowedFormats(), + } } if len(templateValue) == 0 { @@ -72,7 +75,7 @@ func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.Printer, } if templateFormat == "jsonpath-file" { - data, err := ioutil.ReadFile(templateValue) + data, err := os.ReadFile(templateValue) if err != nil { return nil, fmt.Errorf("error reading --template %s, %v", templateValue, err) } @@ -103,12 +106,14 @@ func (f *JSONPathPrintFlags) ToPrinter(templateFormat string) (printers.Printer, // flags related to template printing to it func (f *JSONPathPrintFlags) AddFlags(c *cobra.Command) { if f.TemplateArgument != nil { - c.Flags().StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to the template file to use when --output=jsonpath, --output=jsonpath-file.") + c.Flags(). + StringVar(f.TemplateArgument, "template", *f.TemplateArgument, "Template string or path to a template file to use when --output=jsonpath, --output=jsonpath-file.") _ = c.Flags().SetAnnotation("template", "IsPrint", []string{"true"}) _ = c.MarkFlagFilename("template") } if f.AllowMissingKeys != nil { - c.Flags().BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore template errors caused by missing fields or map keys. This only applies to golang and jsonpath output formats.") + c.Flags(). + BoolVar(f.AllowMissingKeys, "allow-missing-template-keys", *f.AllowMissingKeys, "If true, ignore errors in templates due to missing fields or map keys. This only applies to golang and jsonpath output formats.") _ = c.Flags().SetAnnotation("allow-missing-template-keys", "IsPrint", []string{"true"}) } } diff --git a/pkg/cmdutil/print_flags.go b/pkg/cmdutil/print_flags.go index b0cfb79e..ac58bbf4 100644 --- a/pkg/cmdutil/print_flags.go +++ b/pkg/cmdutil/print_flags.go @@ -44,7 +44,11 @@ func (e NoCompatiblePrinterError) Error() string { } sort.Strings(e.AllowedFormats) - return fmt.Sprintf("unable to match a printer suitable for the output format %q, allowed formats are: %s", output, strings.Join(e.AllowedFormats, ",")) + return fmt.Sprintf( + "unable to match a printer suitable for the output format %q, allowed formats are: %s", + output, + strings.Join(e.AllowedFormats, ","), + ) } func (f *PrintFlags) AllowedFormats() []string { @@ -67,12 +71,17 @@ func (f *PrintFlags) ToPrinter() (printers.Printer, error) { } if f.JSONPathPrintFlags != nil { - if p, err := f.JSONPathPrintFlags.ToPrinter(outputFormat); !IsNoCompatiblePrinterError(err) { + if p, err := f.JSONPathPrintFlags.ToPrinter(outputFormat); !IsNoCompatiblePrinterError( + err, + ) { return p, err } } - return nil, NoCompatiblePrinterError{OutputFormat: f.OutputFormat, AllowedFormats: f.AllowedFormats()} + return nil, NoCompatiblePrinterError{ + OutputFormat: f.OutputFormat, + AllowedFormats: f.AllowedFormats(), + } } func (f *PrintFlags) AddFlags(cmd *cobra.Command) { @@ -80,7 +89,8 @@ func (f *PrintFlags) AddFlags(cmd *cobra.Command) { f.JSONPathPrintFlags.AddFlags(cmd) if f.OutputFormat != nil { - cmd.Flags().StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: %s.`, strings.Join(f.AllowedFormats(), ", "))) + cmd.Flags(). + StringVarP(f.OutputFormat, "output", "o", *f.OutputFormat, fmt.Sprintf(`Output format. One of: (%s).`, strings.Join(f.AllowedFormats(), ", "))) _ = cmd.Flags().SetAnnotation("output", "IsPrint", []string{"true"}) if f.OutputFlagSpecified == nil { f.OutputFlagSpecified = func() bool { diff --git a/pkg/cmdutil/spec_flags.go b/pkg/cmdutil/spec_flags.go index 16fd8a80..7df3005c 100644 --- a/pkg/cmdutil/spec_flags.go +++ b/pkg/cmdutil/spec_flags.go @@ -248,27 +248,27 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumeri cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered an exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) - cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether to include this query in Algolia's search analytics. + cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether this search will be included in Analytics. See: https://www.algolia.com/doc/api-reference/api-parameters/analytics/`)) cmd.Flags().SetAnnotation("analytics", "Categories", []string{"Analytics"}) - cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Search analytics tags for query data segmentation. + cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Tags to apply to the query for segmenting analytics data. See: https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/`)) cmd.Flags().SetAnnotation("analyticsTags", "Categories", []string{"Analytics"}) - cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle: expressed as a comma-separated string of latitude and longitude values. + cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/`)) cmd.Flags().SetAnnotation("aroundLatLng", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to use the location computed from the user's IP address. + cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to obtain the coordinates from the request's IP address. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/`)) cmd.Flags().SetAnnotation("aroundLatLngViaIP", "Categories", []string{"Geo-Search"}) aroundPrecision := NewJSONVar([]string{"integer", "array"}...) - cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Groups similar distances into range bands. + cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Precision of a coordinate-based search in meters to group results with similar distances. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/`)) cmd.Flags().SetAnnotation("aroundPrecision", "Categories", []string{"Geo-Search"}) aroundRadius := NewJSONVar([]string{"integer", "string"}...) cmd.Flags().Var(aroundRadius, "aroundRadius", heredoc.Doc(`Maximum radius for a search around a central location. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/`)) cmd.Flags().SetAnnotation("aroundRadius", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. + cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. See: https://www.algolia.com/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity/`)) cmd.Flags().SetAnnotation("attributeCriteriaComputedByMinProximity", "Categories", []string{"Advanced"}) cmd.Flags().StringSlice("attributesToHighlight", []string{}, heredoc.Doc(`Attributes to highlight. @@ -297,7 +297,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleran cmd.Flags().Var(distinct, "distinct", heredoc.Doc(`Determines how many records of a group are included in the search results. See: https://www.algolia.com/doc/api-reference/api-parameters/distinct/`)) cmd.Flags().SetAnnotation("distinct", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to include this search in currently running A/B tests. + cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to enable A/B testing for this search. See: https://www.algolia.com/doc/api-reference/api-parameters/enableABTest/`)) cmd.Flags().SetAnnotation("enableABTest", "Categories", []string{"Advanced"}) cmd.Flags().Bool("enablePersonalization", false, heredoc.Doc(`Whether to enable Personalization. @@ -316,13 +316,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/exactOnSingleWordQ cmd.Flags().Var(facetFilters, "facetFilters", heredoc.Doc(`Filter the search by facet values, so that only records with the same facet values are retrieved. See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().SetAnnotation("facetFilters", "Categories", []string{"Filtering"}) - cmd.Flags().Bool("facetingAfterDistinct", false, heredoc.Doc(`Whether to apply faceting after deduplication with distinct. + cmd.Flags().Bool("facetingAfterDistinct", false, heredoc.Doc(`Whether faceting should be applied after deduplication with distinct. See: https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/`)) cmd.Flags().SetAnnotation("facetingAfterDistinct", "Categories", []string{"Faceting"}) - cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Retrieve the specified facets and their facet values. + cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Facets for which to retrieve facet values that match the search criteria and the number of matching facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/facets/`)) cmd.Flags().SetAnnotation("facets", "Categories", []string{"Faceting"}) - cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. + cmd.Flags().String("filters", "", heredoc.Doc(`Filter expression to only include items that match the filter criteria in the response. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) cmd.Flags().Bool("getRankingInfo", false, heredoc.Doc(`Whether the search response should include detailed ranking information. @@ -360,24 +360,24 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor1Typ cmd.Flags().Int("minWordSizefor2Typos", 8, heredoc.Doc(`Minimum number of characters a word in the search query must contain to accept matches with two typos. See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor2Typos/`)) cmd.Flags().SetAnnotation("minWordSizefor2Typos", "Categories", []string{"Typos"}) - cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`If aroundRadius isn't set, defines a [minimum radius] for aroundLatLng and aroundLatLngViaIP (in meters). + cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`Minimum radius (in meters) for a search around a location when aroundRadius isn't set. See: https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/`)) cmd.Flags().SetAnnotation("minimumAroundRadius", "Categories", []string{"Geo-Search"}) cmd.Flags().String("mode", "keywordSearch", heredoc.Doc(`Search mode the index will use to query for results. One of: neuralSearch, keywordSearch. See: https://www.algolia.com/doc/api-reference/api-parameters/mode/`)) cmd.Flags().SetAnnotation("mode", "Categories", []string{"Query strategy"}) - cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`Change the default settings for several natural language parameters in a single operation: ignorePlurals, removeStopWords, removeWordsIfNoResults, analyticsTags, and ruleContexts. + cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches). See: https://www.algolia.com/doc/api-reference/api-parameters/naturalLanguages/`)) cmd.Flags().SetAnnotation("naturalLanguages", "Categories", []string{"Languages"}) numericFilters := NewJSONVar([]string{"array", "string"}...) cmd.Flags().Var(numericFilters, "numericFilters", heredoc.Doc(`Filter by numeric facets. See: https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/`)) cmd.Flags().SetAnnotation("numericFilters", "Categories", []string{"Filtering"}) - cmd.Flags().Int("offset", 0, heredoc.Doc(`Out of the results list, indicate which one you want to show first. + cmd.Flags().Int("offset", 0, heredoc.Doc(`Position of the first hit to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/offset/`)) cmd.Flags().SetAnnotation("offset", "Categories", []string{"Pagination"}) optionalFilters := NewJSONVar([]string{"array", "string"}...) - cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Create filters for ranking purposes. Records that match the filter will rank higher (or lower for a negative filter). + cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Filters to promote or demote records in the search results. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/`)) cmd.Flags().SetAnnotation("optionalFilters", "Categories", []string{"Filtering"}) optionalWords := NewJSONVar([]string{"string", "null", "array"}...) @@ -386,16 +386,16 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) cmd.Flags().Int("page", 0, heredoc.Doc(`Page of search results to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/page/`)) cmd.Flags().SetAnnotation("page", "Categories", []string{"Pagination"}) - cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this query in the processing-time percentile computation. + cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this search when calculating processing-time percentiles. See: https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/`)) cmd.Flags().SetAnnotation("percentileComputation", "Categories", []string{"Advanced"}) - cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Determines the impact of the Personalization feature on results: from 0 (none) to 100 (maximum). + cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Impact that Personalization should have on this search. See: https://www.algolia.com/doc/api-reference/api-parameters/personalizationImpact/`)) cmd.Flags().SetAnnotation("personalizationImpact", "Categories", []string{"Personalization"}) - cmd.Flags().String("query", "", heredoc.Doc(`The text to search for in the index. + cmd.Flags().String("query", "", heredoc.Doc(`Search query. See: https://www.algolia.com/doc/api-reference/api-parameters/query/`)) cmd.Flags().SetAnnotation("query", "Categories", []string{"Search"}) - cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/`)) cmd.Flags().SetAnnotation("queryLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("queryType", "prefixLast", heredoc.Doc(`Determines if and how query words are interpreted as prefixes. One of: prefixLast, prefixAll, prefixNone. @@ -423,13 +423,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/renderingContent/` cmd.Flags().Bool("replaceSynonymsInHighlight", false, heredoc.Doc(`Whether to replace a highlighted word with the matched synonym. See: https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInHighlight/`)) cmd.Flags().SetAnnotation("replaceSynonymsInHighlight", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in search and browse API responses. + cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in the API response of search and browse requests. See: https://www.algolia.com/doc/api-reference/api-parameters/responseFields/`)) cmd.Flags().SetAnnotation("responseFields", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that partially or fully matched the search query. + cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that at least partially matched the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/`)) cmd.Flags().SetAnnotation("restrictHighlightAndSnippetArrays", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restrict the query to look at only the specified searchable attributes. + cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restricts a search to a subset of your searchable attributes. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/`)) cmd.Flags().SetAnnotation("restrictSearchableAttributes", "Categories", []string{"Filtering"}) cmd.Flags().StringSlice("ruleContexts", []string{}, heredoc.Doc(`Assigns a rule context to the search query. @@ -437,7 +437,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/`)) cmd.Flags().SetAnnotation("ruleContexts", "Categories", []string{"Rules"}) semanticSearch := NewJSONVar([]string{}...) cmd.Flags().Var(semanticSearch, "semanticSearch", heredoc.Doc(`Settings for the semantic search part of NeuralSearch.`)) - cmd.Flags().String("similarQuery", "", heredoc.Doc(`Overrides the query parameter and performs a more generic search to find "similar" results. + cmd.Flags().String("similarQuery", "", heredoc.Doc(`Keywords to be used instead of the search query to conduct a more broader search. See: https://www.algolia.com/doc/api-reference/api-parameters/similarQuery/`)) cmd.Flags().SetAnnotation("similarQuery", "Categories", []string{"Search"}) cmd.Flags().String("snippetEllipsisText", "…", heredoc.Doc(`String used as an ellipsis indicator when a snippet is truncated. @@ -446,10 +446,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisTex cmd.Flags().String("sortFacetValuesBy", "count", heredoc.Doc(`Order in which to retrieve facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/`)) cmd.Flags().SetAnnotation("sortFacetValuesBy", "Categories", []string{"Faceting"}) - cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`How to calculate the filtering score. Whether to sum the scores of each matched filter or use the highest score of the filters. + cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`Whether to sum all filter scores. See: https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/`)) cmd.Flags().SetAnnotation("sumOrFiltersScores", "Categories", []string{"Filtering"}) - cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to use or disregard an index's synonyms for this search. + cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to take into account an index's synonyms for this search. See: https://www.algolia.com/doc/api-reference/api-parameters/synonyms/`)) cmd.Flags().SetAnnotation("synonyms", "Categories", []string{"Advanced"}) tagFilters := NewJSONVar([]string{"array", "string"}...) @@ -460,13 +460,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/`)) cmd.Flags().Var(typoTolerance, "typoTolerance", heredoc.Doc(`Whether typo tolerance is enabled and how it is applied. See: https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/`)) cmd.Flags().SetAnnotation("typoTolerance", "Categories", []string{"Typos"}) - cmd.Flags().String("userToken", "", heredoc.Doc(`Link the current search to a specific user with a user token (a unique pseudonymous or anonymous identifier). + cmd.Flags().String("userToken", "", heredoc.Doc(`Unique pseudonymous or anonymous user identifier. See: https://www.algolia.com/doc/api-reference/api-parameters/userToken/`)) cmd.Flags().SetAnnotation("userToken", "Categories", []string{"Personalization"}) } func AddDeleteByParamsFlags(cmd *cobra.Command) { - cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle: expressed as a comma-separated string of latitude and longitude values. + cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/`)) cmd.Flags().SetAnnotation("aroundLatLng", "Categories", []string{"Geo-Search"}) aroundRadius := NewJSONVar([]string{"integer", "string"}...) @@ -477,7 +477,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/`)) cmd.Flags().Var(facetFilters, "facetFilters", heredoc.Doc(`Filter the search by facet values, so that only records with the same facet values are retrieved. See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().SetAnnotation("facetFilters", "Categories", []string{"Filtering"}) - cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. + cmd.Flags().String("filters", "", heredoc.Doc(`Filter expression to only include items that match the filter criteria in the response. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) insideBoundingBox := NewJSONVar([]string{"string", "null", "array"}...) @@ -510,7 +510,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumeri cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered an exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) - cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. + cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. See: https://www.algolia.com/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity/`)) cmd.Flags().SetAnnotation("attributeCriteriaComputedByMinProximity", "Categories", []string{"Advanced"}) cmd.Flags().String("attributeForDistinct", "", heredoc.Doc(`Attribute that should be used to establish groups of results. @@ -588,7 +588,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/hitsPerPage/`)) cmd.Flags().Var(ignorePlurals, "ignorePlurals", heredoc.Doc(`Treat singular, plurals, and other forms of declensions as equivalent. See: https://www.algolia.com/doc/api-reference/api-parameters/ignorePlurals/`)) cmd.Flags().SetAnnotation("ignorePlurals", "Categories", []string{"Languages"}) - cmd.Flags().StringSlice("indexLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("indexLanguages", []string{}, heredoc.Doc(`Languages for language-specific processing steps, such as word detection and dictionary settings. See: https://www.algolia.com/doc/api-reference/api-parameters/indexLanguages/`)) cmd.Flags().SetAnnotation("indexLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("keepDiacriticsOnCharacters", "", heredoc.Doc(`Characters for which diacritics should be preserved. @@ -620,7 +620,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/numericAttributesF See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) cmd.Flags().Int("paginationLimitedTo", 1000, heredoc.Doc(`Maximum number of search results that can be obtained through pagination. See: https://www.algolia.com/doc/api-reference/api-parameters/paginationLimitedTo/`)) - cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/`)) cmd.Flags().SetAnnotation("queryLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("queryType", "prefixLast", heredoc.Doc(`Determines if and how query words are interpreted as prefixes. One of: prefixLast, prefixAll, prefixNone. @@ -651,10 +651,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInH cmd.Flags().StringSlice("replicas", []string{}, heredoc.Doc(`Creates replica indices. See: https://www.algolia.com/doc/api-reference/api-parameters/replicas/`)) cmd.Flags().SetAnnotation("replicas", "Categories", []string{"Ranking"}) - cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in search and browse API responses. + cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in the API response of search and browse requests. See: https://www.algolia.com/doc/api-reference/api-parameters/responseFields/`)) cmd.Flags().SetAnnotation("responseFields", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that partially or fully matched the search query. + cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that at least partially matched the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/`)) cmd.Flags().SetAnnotation("restrictHighlightAndSnippetArrays", "Categories", []string{"Highlighting and Snippeting"}) cmd.Flags().StringSlice("searchableAttributes", []string{}, heredoc.Doc(`Attributes used for searching. Attribute names are case-sensitive. @@ -697,27 +697,27 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/allowTyposOnNumeri cmd.Flags().StringSlice("alternativesAsExact", []string{"ignorePlurals", "singleWordSynonym"}, heredoc.Doc(`Determine which plurals and synonyms should be considered an exact matches. See: https://www.algolia.com/doc/api-reference/api-parameters/alternativesAsExact/`)) cmd.Flags().SetAnnotation("alternativesAsExact", "Categories", []string{"Query strategy"}) - cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether to include this query in Algolia's search analytics. + cmd.Flags().Bool("analytics", true, heredoc.Doc(`Whether this search will be included in Analytics. See: https://www.algolia.com/doc/api-reference/api-parameters/analytics/`)) cmd.Flags().SetAnnotation("analytics", "Categories", []string{"Analytics"}) - cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Search analytics tags for query data segmentation. + cmd.Flags().StringSlice("analyticsTags", []string{}, heredoc.Doc(`Tags to apply to the query for segmenting analytics data. See: https://www.algolia.com/doc/api-reference/api-parameters/analyticsTags/`)) cmd.Flags().SetAnnotation("analyticsTags", "Categories", []string{"Analytics"}) - cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle: a comma-separated string of latitude and longitude values. + cmd.Flags().String("aroundLatLng", "", heredoc.Doc(`Coordinates for the center of a circle, expressed as a comma-separated string of latitude and longitude. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLng/`)) cmd.Flags().SetAnnotation("aroundLatLng", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to use the location computed from the user's IP address. + cmd.Flags().Bool("aroundLatLngViaIP", false, heredoc.Doc(`Whether to obtain the coordinates from the request's IP address. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundLatLngViaIP/`)) cmd.Flags().SetAnnotation("aroundLatLngViaIP", "Categories", []string{"Geo-Search"}) aroundPrecision := NewJSONVar([]string{"integer", "array"}...) - cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Groups similar distances into range bands. + cmd.Flags().Var(aroundPrecision, "aroundPrecision", heredoc.Doc(`Precision of a coordinate-based search in meters to group results with similar distances. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundPrecision/`)) cmd.Flags().SetAnnotation("aroundPrecision", "Categories", []string{"Geo-Search"}) aroundRadius := NewJSONVar([]string{"integer", "string"}...) cmd.Flags().Var(aroundRadius, "aroundRadius", heredoc.Doc(`Maximum radius for a search around a central location. See: https://www.algolia.com/doc/api-reference/api-parameters/aroundRadius/`)) cmd.Flags().SetAnnotation("aroundRadius", "Categories", []string{"Geo-Search"}) - cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. This setting only affects ranking if the Attribute ranking criterion comes before Proximity. If true, the best matching attribute is selected based on the minimum proximity of multiple matches. + cmd.Flags().Bool("attributeCriteriaComputedByMinProximity", false, heredoc.Doc(`Whether the best matching attribute should be determined by minimum proximity. See: https://www.algolia.com/doc/api-reference/api-parameters/attributeCriteriaComputedByMinProximity/`)) cmd.Flags().SetAnnotation("attributeCriteriaComputedByMinProximity", "Categories", []string{"Advanced"}) cmd.Flags().StringSlice("attributesToHighlight", []string{}, heredoc.Doc(`Attributes to highlight. @@ -745,7 +745,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/disableTypoToleran cmd.Flags().Var(distinct, "distinct", heredoc.Doc(`Determines how many records of a group are included in the search results. See: https://www.algolia.com/doc/api-reference/api-parameters/distinct/`)) cmd.Flags().SetAnnotation("distinct", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to include this search in currently running A/B tests. + cmd.Flags().Bool("enableABTest", true, heredoc.Doc(`Whether to enable A/B testing for this search. See: https://www.algolia.com/doc/api-reference/api-parameters/enableABTest/`)) cmd.Flags().SetAnnotation("enableABTest", "Categories", []string{"Advanced"}) cmd.Flags().Bool("enablePersonalization", false, heredoc.Doc(`Whether to enable Personalization. @@ -767,10 +767,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/facetFilters/`)) cmd.Flags().Bool("facetingAfterDistinct", false, heredoc.Doc(`Whether faceting should be applied after deduplication with distinct. See: https://www.algolia.com/doc/api-reference/api-parameters/facetingAfterDistinct/`)) cmd.Flags().SetAnnotation("facetingAfterDistinct", "Categories", []string{"Faceting"}) - cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Retrieve the specified facets and their facet values. + cmd.Flags().StringSlice("facets", []string{}, heredoc.Doc(`Facets for which to retrieve facet values that match the search criteria and the number of matching facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/facets/`)) cmd.Flags().SetAnnotation("facets", "Categories", []string{"Faceting"}) - cmd.Flags().String("filters", "", heredoc.Doc(`Only include items that match the filter. + cmd.Flags().String("filters", "", heredoc.Doc(`Filter expression to only include items that match the filter criteria in the response. See: https://www.algolia.com/doc/api-reference/api-parameters/filters/`)) cmd.Flags().SetAnnotation("filters", "Categories", []string{"Filtering"}) cmd.Flags().Bool("getRankingInfo", false, heredoc.Doc(`Whether the search response should include detailed ranking information. @@ -808,24 +808,24 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor1Typ cmd.Flags().Int("minWordSizefor2Typos", 8, heredoc.Doc(`Minimum number of characters a word in the search query must contain to accept matches with two typos. See: https://www.algolia.com/doc/api-reference/api-parameters/minWordSizefor2Typos/`)) cmd.Flags().SetAnnotation("minWordSizefor2Typos", "Categories", []string{"Typos"}) - cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`If aroundRadius isn't set, defines a [minimum radius] for aroundLatLng and aroundLatLngViaIP (in meters). + cmd.Flags().Int("minimumAroundRadius", 0, heredoc.Doc(`Minimum radius (in meters) for a search around a location when aroundRadius isn't set. See: https://www.algolia.com/doc/api-reference/api-parameters/minimumAroundRadius/`)) cmd.Flags().SetAnnotation("minimumAroundRadius", "Categories", []string{"Geo-Search"}) cmd.Flags().String("mode", "keywordSearch", heredoc.Doc(`Search mode the index will use to query for results. One of: neuralSearch, keywordSearch. See: https://www.algolia.com/doc/api-reference/api-parameters/mode/`)) cmd.Flags().SetAnnotation("mode", "Categories", []string{"Query strategy"}) - cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`Change the default settings for several natural language parameters in a single operation: ignorePlurals, removeStopWords, removeWordsIfNoResults, analyticsTags, and ruleContexts. + cmd.Flags().StringSlice("naturalLanguages", []string{}, heredoc.Doc(`ISO language codes that adjust settings that are useful for processing natural language queries (as opposed to keyword searches). See: https://www.algolia.com/doc/api-reference/api-parameters/naturalLanguages/`)) cmd.Flags().SetAnnotation("naturalLanguages", "Categories", []string{"Languages"}) numericFilters := NewJSONVar([]string{"array", "string"}...) cmd.Flags().Var(numericFilters, "numericFilters", heredoc.Doc(`Filter by numeric facets. See: https://www.algolia.com/doc/api-reference/api-parameters/numericFilters/`)) cmd.Flags().SetAnnotation("numericFilters", "Categories", []string{"Filtering"}) - cmd.Flags().Int("offset", 0, heredoc.Doc(`Out of the results list, indicate which one you want to show first. + cmd.Flags().Int("offset", 0, heredoc.Doc(`Position of the first hit to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/offset/`)) cmd.Flags().SetAnnotation("offset", "Categories", []string{"Pagination"}) optionalFilters := NewJSONVar([]string{"array", "string"}...) - cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Create filters for ranking purposes. Records that match the filter will rank higher (or lower for a negative filter). + cmd.Flags().Var(optionalFilters, "optionalFilters", heredoc.Doc(`Filters to promote or demote records in the search results. See: https://www.algolia.com/doc/api-reference/api-parameters/optionalFilters/`)) cmd.Flags().SetAnnotation("optionalFilters", "Categories", []string{"Filtering"}) optionalWords := NewJSONVar([]string{"string", "null", "array"}...) @@ -834,16 +834,16 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/optionalWords/`)) cmd.Flags().Int("page", 0, heredoc.Doc(`Page of search results to retrieve. See: https://www.algolia.com/doc/api-reference/api-parameters/page/`)) cmd.Flags().SetAnnotation("page", "Categories", []string{"Pagination"}) - cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this query in the processing-time percentile computation. + cmd.Flags().Bool("percentileComputation", true, heredoc.Doc(`Whether to include this search when calculating processing-time percentiles. See: https://www.algolia.com/doc/api-reference/api-parameters/percentileComputation/`)) cmd.Flags().SetAnnotation("percentileComputation", "Categories", []string{"Advanced"}) - cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Determines the impact of the Personalization feature on results: from 0 (none) to 100 (maximum). + cmd.Flags().Int("personalizationImpact", 100, heredoc.Doc(`Impact that Personalization should have on this search. See: https://www.algolia.com/doc/api-reference/api-parameters/personalizationImpact/`)) cmd.Flags().SetAnnotation("personalizationImpact", "Categories", []string{"Personalization"}) - cmd.Flags().String("query", "", heredoc.Doc(`The text to search for in the index. + cmd.Flags().String("query", "", heredoc.Doc(`Search query. See: https://www.algolia.com/doc/api-reference/api-parameters/query/`)) cmd.Flags().SetAnnotation("query", "Categories", []string{"Search"}) - cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Define languages for which to apply language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. + cmd.Flags().StringSlice("queryLanguages", []string{}, heredoc.Doc(`Languages for language-specific query processing steps such as plurals, stop-word removal, and word-detection dictionaries. See: https://www.algolia.com/doc/api-reference/api-parameters/queryLanguages/`)) cmd.Flags().SetAnnotation("queryLanguages", "Categories", []string{"Languages"}) cmd.Flags().String("queryType", "prefixLast", heredoc.Doc(`Determines if and how query words are interpreted as prefixes. One of: prefixLast, prefixAll, prefixNone. @@ -871,13 +871,13 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/renderingContent/` cmd.Flags().Bool("replaceSynonymsInHighlight", false, heredoc.Doc(`Whether to replace a highlighted word with the matched synonym. See: https://www.algolia.com/doc/api-reference/api-parameters/replaceSynonymsInHighlight/`)) cmd.Flags().SetAnnotation("replaceSynonymsInHighlight", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in search and browse API responses. + cmd.Flags().StringSlice("responseFields", []string{"*"}, heredoc.Doc(`Properties to include in the API response of search and browse requests. See: https://www.algolia.com/doc/api-reference/api-parameters/responseFields/`)) cmd.Flags().SetAnnotation("responseFields", "Categories", []string{"Advanced"}) - cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that partially or fully matched the search query. + cmd.Flags().Bool("restrictHighlightAndSnippetArrays", false, heredoc.Doc(`Whether to restrict highlighting and snippeting to items that at least partially matched the search query. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictHighlightAndSnippetArrays/`)) cmd.Flags().SetAnnotation("restrictHighlightAndSnippetArrays", "Categories", []string{"Highlighting and Snippeting"}) - cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restrict the query to look at only the specified searchable attributes. + cmd.Flags().StringSlice("restrictSearchableAttributes", []string{}, heredoc.Doc(`Restricts a search to a subset of your searchable attributes. See: https://www.algolia.com/doc/api-reference/api-parameters/restrictSearchableAttributes/`)) cmd.Flags().SetAnnotation("restrictSearchableAttributes", "Categories", []string{"Filtering"}) cmd.Flags().StringSlice("ruleContexts", []string{}, heredoc.Doc(`Assigns a rule context to the search query. @@ -885,7 +885,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/ruleContexts/`)) cmd.Flags().SetAnnotation("ruleContexts", "Categories", []string{"Rules"}) semanticSearch := NewJSONVar([]string{}...) cmd.Flags().Var(semanticSearch, "semanticSearch", heredoc.Doc(`Settings for the semantic search part of NeuralSearch.`)) - cmd.Flags().String("similarQuery", "", heredoc.Doc(`Overrides the query parameter and performs a more generic search to find "similar" results. + cmd.Flags().String("similarQuery", "", heredoc.Doc(`Keywords to be used instead of the search query to conduct a more broader search. See: https://www.algolia.com/doc/api-reference/api-parameters/similarQuery/`)) cmd.Flags().SetAnnotation("similarQuery", "Categories", []string{"Search"}) cmd.Flags().String("snippetEllipsisText", "…", heredoc.Doc(`String used as an ellipsis indicator when a snippet is truncated. @@ -894,10 +894,10 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/snippetEllipsisTex cmd.Flags().String("sortFacetValuesBy", "count", heredoc.Doc(`Order in which to retrieve facet values. See: https://www.algolia.com/doc/api-reference/api-parameters/sortFacetValuesBy/`)) cmd.Flags().SetAnnotation("sortFacetValuesBy", "Categories", []string{"Faceting"}) - cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`How to calculate the filtering score. Whether to sum the scores of each matched filter or use the highest score of the filters. + cmd.Flags().Bool("sumOrFiltersScores", false, heredoc.Doc(`Whether to sum all filter scores. See: https://www.algolia.com/doc/api-reference/api-parameters/sumOrFiltersScores/`)) cmd.Flags().SetAnnotation("sumOrFiltersScores", "Categories", []string{"Filtering"}) - cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to use or disregard an index's synonyms for this search. + cmd.Flags().Bool("synonyms", true, heredoc.Doc(`Whether to take into account an index's synonyms for this search. See: https://www.algolia.com/doc/api-reference/api-parameters/synonyms/`)) cmd.Flags().SetAnnotation("synonyms", "Categories", []string{"Advanced"}) tagFilters := NewJSONVar([]string{"array", "string"}...) @@ -908,7 +908,7 @@ See: https://www.algolia.com/doc/api-reference/api-parameters/tagFilters/`)) cmd.Flags().Var(typoTolerance, "typoTolerance", heredoc.Doc(`Whether typo tolerance is enabled and how it is applied. See: https://www.algolia.com/doc/api-reference/api-parameters/typoTolerance/`)) cmd.Flags().SetAnnotation("typoTolerance", "Categories", []string{"Typos"}) - cmd.Flags().String("userToken", "", heredoc.Doc(`Link the current search to a specific user with a user token (a unique pseudonymous or anonymous identifier). + cmd.Flags().String("userToken", "", heredoc.Doc(`Unique pseudonymous or anonymous user identifier. See: https://www.algolia.com/doc/api-reference/api-parameters/userToken/`)) cmd.Flags().SetAnnotation("userToken", "Categories", []string{"Personalization"}) } diff --git a/pkg/cmdutil/usage.go b/pkg/cmdutil/usage.go index 6cd10764..97f85f09 100644 --- a/pkg/cmdutil/usage.go +++ b/pkg/cmdutil/usage.go @@ -47,12 +47,17 @@ func (u *UsageEntries) AddBasicUsage(IOStreams *iostreams.IOStreams, command *co u.AddEntry(UsageEntry{ cs.Bold("Available commands:"), - strings.Join(commands, "\n")}, + strings.Join(commands, "\n"), + }, ) } } -func (u *UsageEntries) AddFlags(IOStreams *iostreams.IOStreams, command *cobra.Command, flagUsages string) { +func (u *UsageEntries) AddFlags( + IOStreams *iostreams.IOStreams, + command *cobra.Command, + flagUsages string, +) { cs := IOStreams.ColorScheme() if flagUsages != "" { @@ -60,11 +65,19 @@ func (u *UsageEntries) AddFlags(IOStreams *iostreams.IOStreams, command *cobra.C } } -func (u *UsageEntries) AddAllFlags(IOStreams *iostreams.IOStreams, command *cobra.Command, flagUsages string) { +func (u *UsageEntries) AddAllFlags( + IOStreams *iostreams.IOStreams, + command *cobra.Command, + flagUsages string, +) { u.AddFlags(IOStreams, command, command.LocalFlags().FlagUsages()) } -func (u *UsageEntries) AddFilteredFlags(IOStreams *iostreams.IOStreams, command *cobra.Command, flagsToDisplay []string) { +func (u *UsageEntries) AddFilteredFlags( + IOStreams *iostreams.IOStreams, + command *cobra.Command, + flagsToDisplay []string, +) { filteredFlags := filterFlagSet(*command.LocalFlags(), flagsToDisplay) u.AddFlags(IOStreams, command, filteredFlags.FlagUsages()) @@ -97,7 +110,11 @@ func (u *UsageEntries) DisplayEntries(out io.Writer) { } } -func UsageFunc(IOStreams *iostreams.IOStreams, command *cobra.Command, flagUsages string) func(cmd *cobra.Command) error { +func UsageFunc( + IOStreams *iostreams.IOStreams, + command *cobra.Command, + flagUsages string, +) func(cmd *cobra.Command) error { return func(cmd *cobra.Command) error { entries := UsageEntries{} @@ -109,20 +126,29 @@ func UsageFunc(IOStreams *iostreams.IOStreams, command *cobra.Command, flagUsage } } -func UsageFuncDefault(IOStreams *iostreams.IOStreams, command *cobra.Command) func(cmd *cobra.Command) error { +func UsageFuncDefault( + IOStreams *iostreams.IOStreams, + command *cobra.Command, +) func(cmd *cobra.Command) error { return UsageFunc(IOStreams, command, command.LocalFlags().FlagUsages()) } -func UsageFuncWithFilteredFlags(IOStreams *iostreams.IOStreams, command *cobra.Command, flagsToDisplay []string) func(cmd *cobra.Command) error { +func UsageFuncWithFilteredFlags( + IOStreams *iostreams.IOStreams, + command *cobra.Command, + flagsToDisplay []string, +) func(cmd *cobra.Command) error { filteredFlags := filterFlagSet(*command.LocalFlags(), flagsToDisplay) return UsageFunc(IOStreams, command, filteredFlags.FlagUsages()) - } -func UsageFuncWithFilteredAndInheritedFlags(IOStreams *iostreams.IOStreams, command *cobra.Command, flagsToDisplay []string) func(cmd *cobra.Command) error { +func UsageFuncWithFilteredAndInheritedFlags( + IOStreams *iostreams.IOStreams, + command *cobra.Command, + flagsToDisplay []string, +) func(cmd *cobra.Command) error { return func(cmd *cobra.Command) error { - entries := UsageEntries{} entries.AddBasicUsage(IOStreams, command) entries.AddFilteredFlags(IOStreams, command, flagsToDisplay) @@ -133,7 +159,10 @@ func UsageFuncWithFilteredAndInheritedFlags(IOStreams *iostreams.IOStreams, comm } } -func UsageFuncWithInheritedFlagsOnly(IOStreams *iostreams.IOStreams, command *cobra.Command) func(cmd *cobra.Command) error { +func UsageFuncWithInheritedFlagsOnly( + IOStreams *iostreams.IOStreams, + command *cobra.Command, +) func(cmd *cobra.Command) error { return func(cmd *cobra.Command) error { entries := UsageEntries{} entries.AddBasicUsage(IOStreams, command) @@ -142,7 +171,6 @@ func UsageFuncWithInheritedFlagsOnly(IOStreams *iostreams.IOStreams, command *co entries.DisplayEntries(IOStreams.Out) return nil } - } func Dedent(s string) string { diff --git a/pkg/cmdutil/valid_args.go b/pkg/cmdutil/valid_args.go index 93825061..a7e809b7 100644 --- a/pkg/cmdutil/valid_args.go +++ b/pkg/cmdutil/valid_args.go @@ -3,19 +3,21 @@ package cmdutil import ( "fmt" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" "github.com/algolia/cli/api/crawler" "github.com/spf13/cobra" ) // IndexNames returns a function to list the index names from the given search client. -func IndexNames(clientF func() (*search.Client, error)) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func IndexNames( + clientF func() (*search.APIClient, error), +) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { client, err := clientF() if err != nil { return nil, cobra.ShellCompDirectiveError } - res, err := client.ListIndices() + res, err := client.ListIndices(client.NewApiListIndicesRequest()) if err != nil { return nil, cobra.ShellCompDirectiveError } @@ -29,7 +31,9 @@ func IndexNames(clientF func() (*search.Client, error)) func(cmd *cobra.Command, } // CrawlerIDs returns a function to list the crawler IDs from the given crawler client. -func CrawlerIDs(clientF func() (*crawler.Client, error)) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { +func CrawlerIDs( + clientF func() (*crawler.Client, error), +) func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { return func(cmd *cobra.Command, args []string, toComplete string) ([]string, cobra.ShellCompDirective) { client, err := clientF() if err != nil { diff --git a/pkg/config/config.go b/pkg/config/config.go index 91d867a0..0e36cc24 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -48,11 +48,11 @@ func (c *Config) InitConfig() { c.File = configFile viper.SetConfigType("toml") viper.SetConfigFile(configFile) - viper.SetConfigPermissions(os.FileMode(0600)) + viper.SetConfigPermissions(os.FileMode(0o600)) // Try to change permissions manually, because we used to create files // with default permissions (0644) - err := os.Chmod(configFile, os.FileMode(0600)) + err := os.Chmod(configFile, os.FileMode(0o600)) if err != nil && !os.IsNotExist(err) { log.Fatalf("%s", err) } diff --git a/pkg/config/profile.go b/pkg/config/profile.go index a04d8d78..42f7457f 100644 --- a/pkg/config/profile.go +++ b/pkg/config/profile.go @@ -47,9 +47,9 @@ func (p *Profile) GetApplicationID() (string, error) { } if err := viper.ReadInConfig(); err == nil { - appId := viper.GetString(p.GetFieldName("application_id")) - if appId != "" { - return appId, nil + appID := viper.GetString(p.GetFieldName("application_id")) + if appID != "" { + return appID, nil } } diff --git a/pkg/gen/gen_flags.go b/pkg/gen/gen_flags.go index b93daa71..1601273b 100644 --- a/pkg/gen/gen_flags.go +++ b/pkg/gen/gen_flags.go @@ -82,7 +82,7 @@ func main() { // Write the formatted source code to disk fmt.Printf("writing %s\n", pathOutput) - err = ioutil.WriteFile(pathOutput, formatted, 0644) + err = ioutil.WriteFile(pathOutput, formatted, 0o644) if err != nil { panic(err) } diff --git a/pkg/httpmock/registry.go b/pkg/httpmock/registry.go index cf32c520..3e942097 100644 --- a/pkg/httpmock/registry.go +++ b/pkg/httpmock/registry.go @@ -4,6 +4,7 @@ import ( "fmt" "net/http" "sync" + "time" ) type Registry struct { @@ -38,7 +39,11 @@ func (r *Registry) Verify(t Testing) { } // Request satisfies Requester interface -func (r *Registry) Request(req *http.Request) (*http.Response, error) { +func (r *Registry) Request( + req *http.Request, + _ time.Duration, + _ time.Duration, +) (*http.Response, error) { var stub *Stub r.mu.Lock() diff --git a/pkg/httpmock/stub.go b/pkg/httpmock/stub.go index 25648215..3b9066b2 100644 --- a/pkg/httpmock/stub.go +++ b/pkg/httpmock/stub.go @@ -8,8 +8,10 @@ import ( "strings" ) -type Matcher func(req *http.Request) bool -type Responder func(req *http.Request) (*http.Response, error) +type ( + Matcher func(req *http.Request) bool + Responder func(req *http.Request) (*http.Response, error) +) type Stub struct { matched bool diff --git a/pkg/iostreams/iostreams.go b/pkg/iostreams/iostreams.go index 3d23bb49..49f3a169 100644 --- a/pkg/iostreams/iostreams.go +++ b/pkg/iostreams/iostreams.go @@ -5,7 +5,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "os" "os/exec" "strconv" @@ -253,7 +252,12 @@ func (s *IOStreams) StartProgressIndicatorWithLabel(label string) { // https://github.com/briandowns/spinner#available-character-sets dotStyle := spinner.CharSets[11] - sp := spinner.New(dotStyle, 120*time.Millisecond, spinner.WithWriter(s.ErrOut), spinner.WithColor("fgCyan")) + sp := spinner.New( + dotStyle, + 120*time.Millisecond, + spinner.WithWriter(s.ErrOut), + spinner.WithColor("fgCyan"), + ) if label != "" { sp.Prefix = label + " " } @@ -370,7 +374,7 @@ func (s *IOStreams) TempFile(dir, pattern string) (*os.File, error) { if s.TempFileOverride != nil { return s.TempFileOverride, nil } - return ioutil.TempFile(dir, pattern) + return os.CreateTemp(dir, pattern) } func System() *IOStreams { @@ -411,7 +415,7 @@ func Test() (*IOStreams, *bytes.Buffer, *bytes.Buffer, *bytes.Buffer) { out := &bytes.Buffer{} errOut := &bytes.Buffer{} return &IOStreams{ - In: ioutil.NopCloser(in), + In: io.NopCloser(in), Out: out, ErrOut: errOut, ttySize: func() (int, int, error) { diff --git a/pkg/iostreams/tty_size.go b/pkg/iostreams/tty_size.go index 767000ce..c6cbd9c6 100644 --- a/pkg/iostreams/tty_size.go +++ b/pkg/iostreams/tty_size.go @@ -1,4 +1,5 @@ -//+build !windows +//go:build !windows +// +build !windows package iostreams diff --git a/pkg/iostreams/tty_size_windows.go b/pkg/iostreams/tty_size_windows.go index 57fe397c..50c7c3cf 100644 --- a/pkg/iostreams/tty_size_windows.go +++ b/pkg/iostreams/tty_size_windows.go @@ -2,6 +2,7 @@ package iostreams import ( "errors" + "golang.org/x/term" ) diff --git a/pkg/open/open.go b/pkg/open/open.go index e1e70659..b77088df 100644 --- a/pkg/open/open.go +++ b/pkg/open/open.go @@ -37,7 +37,6 @@ func CanOpenBrowser() bool { } output, err := execCommand("xdg-settings", "get", "default-web-browser").Output() - if err != nil { return false } diff --git a/pkg/printers/table_printer.go b/pkg/printers/table_printer.go index bc4ed226..b019804d 100644 --- a/pkg/printers/table_printer.go +++ b/pkg/printers/table_printer.go @@ -65,7 +65,11 @@ func (t ttyTablePrinter) IsTTY() bool { return true } -func (t *ttyTablePrinter) AddField(s string, truncateFunc func(int, string) string, colorFunc func(string) string) { +func (t *ttyTablePrinter) AddField( + s string, + truncateFunc func(int, string) string, + colorFunc func(string) string, +) { if truncateFunc == nil { truncateFunc = text.Truncate } diff --git a/pkg/printers/template.go b/pkg/printers/template.go index 63c15907..56264841 100644 --- a/pkg/printers/template.go +++ b/pkg/printers/template.go @@ -52,7 +52,11 @@ func (p *GoTemplatePrinter) Print(ios *iostreams.IOStreams, data interface{}) er // It is way easier to debug this stuff when it shows up in // stdout instead of just stdin. So in addition to returning // a nice error, also print useful stuff with the writer. - fmt.Fprintf(ios.ErrOut, "Error executing template: %v. Printing more information for debugging the template:\n", err) + fmt.Fprintf( + ios.ErrOut, + "Error executing template: %v. Printing more information for debugging the template:\n", + err, + ) fmt.Fprintf(ios.ErrOut, "\ttemplate was:\n\t\t%v\n", p.rawTemplate) fmt.Fprintf(ios.ErrOut, "\traw data was:\n\t\t%v\n", string(dataM)) fmt.Fprintf(ios.ErrOut, "\tobject given to template engine was:\n\t\t%+v\n\n", out) diff --git a/pkg/telemetry/telemetry.go b/pkg/telemetry/telemetry.go index 37227156..42bce595 100644 --- a/pkg/telemetry/telemetry.go +++ b/pkg/telemetry/telemetry.go @@ -2,23 +2,25 @@ package telemetry import ( "context" - "crypto/md5" + "crypto/md5" // nolint:gosec "fmt" "log" "net" "runtime" + "github.com/segmentio/analytics-go/v3" "github.com/spf13/cobra" "github.com/spf13/pflag" "github.com/xtgo/uuid" - "gopkg.in/segmentio/analytics-go.v3" "github.com/algolia/cli/pkg/utils" "github.com/algolia/cli/pkg/version" ) -const AppName = "cli" -const telemetryAnalyticsURL = "https://telemetry-proxy.algolia.com/" +const ( + AppName = "cli" + telemetryAnalyticsURL = "https://telemetry-proxy.algolia.com/" +) type telemetryMetadataKey struct{} @@ -80,7 +82,7 @@ func anonymousID() string { } a := a.HardwareAddr.String() if a != "" { - return fmt.Sprintf("%x", md5.Sum([]byte(a))) + return fmt.Sprintf("%x", md5.Sum([]byte(a))) // nolint: gosec } } return "" @@ -90,7 +92,7 @@ type NoOpTelemetryClient struct{} type CLIAnalyticsEventMetadata struct { AnonymousID string // the anonymous id is the hash of the mac address of the machine - UserId string // TODO: Once we implement OAuth + UserID string // TODO: Once we implement OAuth InvocationID string // the invocation id is unique to each context object and represents all events coming from one command ConfiguredApplicationsNb int // the number of configured applications AppID string // the app id with which the command was called diff --git a/pkg/telemetry/telemetry_test.go b/pkg/telemetry/telemetry_test.go index 03e7e6be..01cb4349 100644 --- a/pkg/telemetry/telemetry_test.go +++ b/pkg/telemetry/telemetry_test.go @@ -4,16 +4,16 @@ import ( "context" "testing" + "github.com/segmentio/analytics-go/v3" "github.com/spf13/cobra" "github.com/stretchr/testify/require" - "gopkg.in/segmentio/analytics-go.v3" ) // Context-related tests. func TestEventMetadataWithGet(t *testing.T) { ctx := context.Background() event := &CLIAnalyticsEventMetadata{ - UserId: "user-id", + UserID: "user-id", InvocationID: "invocation-id", OS: "os", CLIVersion: "cli-version", diff --git a/pkg/text/truncate.go b/pkg/text/truncate.go index 2de01612..0cf07410 100644 --- a/pkg/text/truncate.go +++ b/pkg/text/truncate.go @@ -29,6 +29,14 @@ func Truncate(maxWidth int, s string) string { tail = ellipsis } + // Guard against overflow + if maxWidth < 0 { + maxWidth = 0 + } + + // Seems to be a false positive from gosec + // since max(uint) > max(int) && maxWidth >= 0 at this point + // nolint:gosec r := truncate.StringWithTail(s, uint(maxWidth), tail) if DisplayWidth(r) < maxWidth { r += " " diff --git a/pkg/utils/utils.go b/pkg/utils/utils.go index abeabc4e..0b0438b4 100644 --- a/pkg/utils/utils.go +++ b/pkg/utils/utils.go @@ -57,8 +57,8 @@ func Differences(a, b []string) []string { // ToKebabCase converts a string to kebab case func ToKebabCase(str string) string { - var matchFirstCap = regexp.MustCompile("(.)([A-Z][a-z]+)") - var matchAllCap = regexp.MustCompile("([a-z0-9])([A-Z])") + matchFirstCap := regexp.MustCompile("(.)([A-Z][a-z]+)") + matchAllCap := regexp.MustCompile("([a-z0-9])([A-Z])") snake := matchFirstCap.ReplaceAllString(str, "${1}-${2}") snake = matchAllCap.ReplaceAllString(snake, "${1}-${2}") @@ -78,7 +78,9 @@ func SliceToString(str []string) string { // based on https://github.com/watson/ci-info/blob/HEAD/index.js func IsCI() bool { - return os.Getenv("CI") != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari + return os.Getenv( + "CI", + ) != "" || // GitHub Actions, Travis CI, CircleCI, Cirrus CI, GitLab CI, AppVeyor, CodeShip, dsari os.Getenv("CONTINUOUS_INTEGRATION") != "" || // Travis CI, Cirrus CI os.Getenv("BUILD_NUMBER") != "" || // Jenkins, TeamCity os.Getenv("CI_APP_ID") != "" || // Appflow diff --git a/pkg/validators/cmd.go b/pkg/validators/cmd.go index 260cae11..52564c3f 100644 --- a/pkg/validators/cmd.go +++ b/pkg/validators/cmd.go @@ -13,7 +13,7 @@ import ( func ExactArgsWithMsg(n int, msg string) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) != n { - return cmdutil.FlagErrorf(msg) + return cmdutil.FlagErrorf("%s", msg) } return nil @@ -49,7 +49,6 @@ func ExactArgs(n int) cobra.PositionalArgs { return extractArgs(cmd, args) } - } // AtLeastNArgs is a validator for commands to print an error with a custom message @@ -62,11 +61,11 @@ func AtLeastNArgs(n int) cobra.PositionalArgs { return func(cmd *cobra.Command, args []string) error { if len(args) < n { - return cmdutil.FlagErrorf( - fmt.Sprintf("`%s` requires at least %d %s.", cmd.CommandPath(), n, argument)) + msg := fmt.Sprintf("`%s` requires at least %d %s.", cmd.CommandPath(), n, argument) + + return cmdutil.FlagErrorf("%s", msg) } return nil } - } diff --git a/staticcheck.conf b/staticcheck.conf deleted file mode 100644 index 4df14903..00000000 --- a/staticcheck.conf +++ /dev/null @@ -1 +0,0 @@ -checks = ["all", "-ST1005"] \ No newline at end of file diff --git a/test/helpers.go b/test/helpers.go index 98580726..039335ad 100644 --- a/test/helpers.go +++ b/test/helpers.go @@ -8,7 +8,8 @@ import ( "github.com/google/shlex" "github.com/spf13/cobra" - "github.com/algolia/algoliasearch-client-go/v3/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/search" + "github.com/algolia/algoliasearch-client-go/v4/algolia/transport" "github.com/algolia/cli/api/crawler" "github.com/algolia/cli/pkg/cmdutil" "github.com/algolia/cli/pkg/config" @@ -49,7 +50,12 @@ func (s OutputStub) Run() error { return nil } -func NewFactory(isTTY bool, r *httpmock.Registry, cfg config.IConfig, in string) (*cmdutil.Factory, *CmdInOut) { +func NewFactory( + isTTY bool, + r *httpmock.Registry, + cfg config.IConfig, + in string, +) (*cmdutil.Factory, *CmdInOut) { io, stdin, stdout, stderr := iostreams.Test() io.SetStdoutTTY(isTTY) io.SetStdinTTY(isTTY) @@ -64,10 +70,15 @@ func NewFactory(isTTY bool, r *httpmock.Registry, cfg config.IConfig, in string) } if r != nil { - f.SearchClient = func() (*search.Client, error) { - return search.NewClientWithConfig(search.Configuration{ - Requester: r, - }), nil + f.SearchClient = func() (*search.APIClient, error) { + cfg := search.SearchConfiguration{ + Configuration: transport.Configuration{ + AppID: "default", + ApiKey: "default", + Requester: r, + }, + } + return search.NewClientWithConfig(cfg) } f.CrawlerClient = func() (*crawler.Client, error) { return crawler.NewClientWithHTTPClient("id", "key", &http.Client{ From 330ca28292629dac477158c892143b4456915d46 Mon Sep 17 00:00:00 2001 From: Kai Welke Date: Thu, 27 Mar 2025 10:19:52 +0100 Subject: [PATCH 12/12] feat: add end-to-end tests (#179) * feat(e2e): test for version Setup the end-to-end test machinery and add a simple test for the version command * feat(e2e): index commands * feat(e2e): settings tests * feat(e2e): objects commands * feat(e2e): synonyms commands * feat(e2e): rules commands * fixuxp synoynms * feat(e2e): search commands * chore: go mod tidy * chore: update e2e README * fix(e2e): replace hard-coded index name --- Taskfile.yml | 13 +- devbox.json | 3 +- e2e/README.md | 66 +++++++++++ e2e/e2e_test.go | 150 ++++++++++++++++++++++++ e2e/testscripts/indices/indices.txtar | 64 ++++++++++ e2e/testscripts/indices/replicas.txtar | 17 +++ e2e/testscripts/objects/objects.txtar | 42 +++++++ e2e/testscripts/rules/rules.txtar | 41 +++++++ e2e/testscripts/search/search.txtar | 18 +++ e2e/testscripts/settings/settings.txtar | 42 +++++++ e2e/testscripts/synonyms/synonyms.txtar | 38 ++++++ e2e/testscripts/version/version.txtar | 3 + go.mod | 2 + go.sum | 4 + pkg/cmd/synonyms/import/import.go | 6 + 15 files changed, 506 insertions(+), 3 deletions(-) create mode 100644 e2e/README.md create mode 100644 e2e/e2e_test.go create mode 100644 e2e/testscripts/indices/indices.txtar create mode 100644 e2e/testscripts/indices/replicas.txtar create mode 100644 e2e/testscripts/objects/objects.txtar create mode 100644 e2e/testscripts/rules/rules.txtar create mode 100644 e2e/testscripts/search/search.txtar create mode 100644 e2e/testscripts/settings/settings.txtar create mode 100644 e2e/testscripts/synonyms/synonyms.txtar create mode 100644 e2e/testscripts/version/version.txtar diff --git a/Taskfile.yml b/Taskfile.yml index ff07aa38..830edbce 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -18,14 +18,23 @@ tasks: desc: Run unit tests run: always cmd: go test ./... + e2e: + desc: Run end-to-end tests + summary: | + Run tests that mimic how user enters commands and flags. + These tests make real requests to the Algolia API. + To run them, create a `.env` file with the `ALGOLIA_APPLICATION_ID` + and `ALGOLIA_API_KEY` credentials. + cmd: go test ./e2e -tags=e2e + dotenv: [.env] lint: desc: Lint code cmd: golangci-lint run format: desc: Format code cmds: - - gofumpt -w pkg cmd test internal api - - golines -w pkg cmd test internal api + - gofumpt -w pkg cmd test internal api e2e + - golines -w pkg cmd test internal api e2e ci: desc: Test, lint, and format aliases: diff --git a/devbox.json b/devbox.json index 4717bfef..91e2ea4a 100644 --- a/devbox.json +++ b/devbox.json @@ -8,5 +8,6 @@ "golines@latest", "gh@latest", "curl@latest" - ] + ], + "env_from": ".env" } diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 00000000..4a143693 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,66 @@ +# End-to-end tests + +These tests run CLI commands like a user would, +built on top of the [`go-internal/testscript`](https://pkg.go.dev/github.com/rogpeppe/go-internal/testscript) package. + +They make real API requests, +so they work best in an empty Algolia application. +To run these tests, +you need to set the `ALGOLIA_APPLICATION_ID` and `ALGOLIA_API_KEY` environment variables. +If you're using `devbox`, create a `.env` file in the project root directory with these variables. +If you start a development environment with `devbox shell`, +the environment variables will be available to you. + +## New tests + +The tests use a simple format. +For more information, run `go doc testscript`. + +To add a new scenario, create a new directory under the `testscripts` directory, +and add your files with the extension `txtar`. +Each test directory can have multiple test files. +Multiple directories are tested in parallel. + +### Example + +A simple 'hello world' testscript may look like this: + +```txt +# Test if output is hello +exec echo 'hello' +! stderr . +stdout '^hello\n$' +``` + +Read the documentation of the `testscript` package for more information. + +To add the new directory to the test suite, +add a new function to the file `./e2e/e2e_test.go`. +The function name must begin with `Test`. + +```go +// TestHello is a basic example +func TestHello(t *testing.T) { + RunTestsInDir(t, "testscripts/hello") +} +``` + +## Notes + +Since this makes real real requests to the same Algolia application, +these tests aren't fully isolated from each other. + +To make tests interfere less, follow these guidelines: + +- Use a unique index name in each `txtar` file. + For example, use `test-index` in `indices.txtar` and `test-settings` in `settings.txtar` + +- Delete indices at the end of your test with `defer`. + For an example, see `indices.txtar`. + +- Don't test for number of indices, or empty lists. + As other tests might create their own indices and objects, + checks that expect a certain number of items might fail. + You can ensure that the index with a given name exists or doesn't exist + by searching for the index name's pattern in the standard output. + Again, see `indices.txtar`. diff --git a/e2e/e2e_test.go b/e2e/e2e_test.go new file mode 100644 index 00000000..50324a1e --- /dev/null +++ b/e2e/e2e_test.go @@ -0,0 +1,150 @@ +//go:build e2e + +package e2e_test + +import ( + "fmt" + "os" + "strings" + "testing" + + "github.com/algolia/cli/pkg/cmd/root" + "github.com/cli/go-internal/testscript" +) + +// algolia runs the root command of the Algolia CLI +func algolia() int { + return int(root.Execute()) +} + +// TestMain sets the executable program so that we don't depend on the compiled binary +func TestMain(m *testing.M) { + os.Exit(testscript.RunMain(m, map[string]func() int{ + "algolia": algolia, + })) +} + +// testEnvironment stores the environment variables we need to setup for the tests +type testEnvironment struct { + AppID string + ApiKey string +} + +// getEnv reads the environment variables and prints errors for missing ones +func (e *testEnvironment) getEnv() error { + env := map[string]string{} + + required := []string{ + // The CLI testing Algolia app + "ALGOLIA_APPLICATION_ID", + // API key with sufficient permissions to run all tests + "ALGOLIA_API_KEY", + } + + var missing []string + + for _, envVar := range required { + val, ok := os.LookupEnv(envVar) + if val == "" || !ok { + missing = append(missing, envVar) + continue + } + + env[envVar] = val + } + + if len(missing) > 0 { + return fmt.Errorf("missing environment variables: %s", strings.Join(missing, ", ")) + } + + e.AppID = env["ALGOLIA_APPLICATION_ID"] + e.ApiKey = env["ALGOLIA_API_KEY"] + + return nil +} + +// For the `defer` function +var keyT struct{} + +// setupEnv sets up the environment variables for the test +func setupEnv(testEnv testEnvironment) func(ts *testscript.Env) error { + return func(ts *testscript.Env) error { + ts.Setenv("ALGOLIA_APPLICATION_ID", testEnv.AppID) + ts.Setenv("ALGOLIA_API_KEY", testEnv.ApiKey) + + ts.Values[keyT] = ts.T() + return nil + } +} + +// setupCmds sets up custom commands we want to make available in the test scripts +func setupCmds( + testEnv testEnvironment, +) map[string]func(ts *testscript.TestScript, neg bool, args []string) { + return map[string]func(ts *testscript.TestScript, neg bool, args []string){ + "defer": func(ts *testscript.TestScript, neg bool, args []string) { + if neg { + ts.Fatalf("unsupported ! defer") + } + tt, ok := ts.Value(keyT).(testscript.T) + if !ok { + ts.Fatalf("%v is not a testscript.T", ts.Value(keyT)) + } + ts.Defer(func() { + if err := ts.Exec(args[0], args[1:]...); err != nil { + tt.FailNow() + } + }) + }, + } +} + +// runTestsInDir runs all test scripts from a directory +func runTestsInDir(t *testing.T, dirName string) { + var testEnv testEnvironment + if err := testEnv.getEnv(); err != nil { + t.Fatal(err) + } + t.Parallel() + t.Log("Running e2e tests in", dirName) + testscript.Run(t, testscript.Params{ + Dir: dirName, + Setup: setupEnv(testEnv), + Cmds: setupCmds(testEnv), + }) +} + +// TestVersion tests the version option +func TestVersion(t *testing.T) { + runTestsInDir(t, "testscripts/version") +} + +// TestIndices test `algolia indices` commands +func TestIndices(t *testing.T) { + runTestsInDir(t, "testscripts/indices") +} + +// TestSettings tests `algolia settings` commands +func TestSettings(t *testing.T) { + runTestsInDir(t, "testscripts/settings") +} + +// TestObjects tests `algolia objects` commands +func TestObjects(t *testing.T) { + runTestsInDir(t, "testscripts/objects") +} + +// TestSynonyms tests `algolia synonyms` commands +func TestSynonyms(t *testing.T) { + runTestsInDir(t, "testscripts/synonyms") +} + +// TestRules tests `algolia rules` commands +func TestRules(t *testing.T) { + runTestsInDir(t, "testscripts/rules") +} + +// TestSearch tests `algolia search` +func TestSearch(t *testing.T) { + runTestsInDir(t, "testscripts/search") +} diff --git a/e2e/testscripts/indices/indices.txtar b/e2e/testscripts/indices/indices.txtar new file mode 100644 index 00000000..9418b478 --- /dev/null +++ b/e2e/testscripts/indices/indices.txtar @@ -0,0 +1,64 @@ +env INDEX_NAME=test-index +env COPY_NAME=test-copy + +# Create a new index +exec algolia settings set ${INDEX_NAME} --searchableAttributes "foo" --wait +! stderr . + +# Cleanup +defer algolia indices delete ${INDEX_NAME} --confirm + +# Confirm that the index setting is set +exec algolia settings get ${INDEX_NAME} +stdout -count=1 '"searchableAttributes":\["foo"\]' + +# Test that index is listed +exec algolia indices list +stdout -count=1 ^${INDEX_NAME} + +# Copy the index +exec algolia indices copy ${INDEX_NAME} ${COPY_NAME} --wait --confirm +! stderr . + +# Confirm that there are 2 indices now +exec algolia indices list +stdout -count=1 ^${INDEX_NAME} +stdout -count=1 ^${COPY_NAME} + +# Add replica indices to the copy +exec algolia settings set ${COPY_NAME} --replicas 'test-replica1,test-replica2' --wait +! stderr . + +# Confirm that there are 4 indices now +exec algolia indices list +stdout -count=1 ^${INDEX_NAME} +stdout -count=1 ^${COPY_NAME} +stdout -count=1 ^test-replica1 +stdout -count=1 ^test-replica2 + +# Delete one of the replica indices +exec algolia indices delete test-replica1 --confirm --wait +! stderr . + +# Confirm that there are 3 indices now +exec algolia indices list +stdout -count=1 ^${INDEX_NAME} +stdout -count=1 ^${COPY_NAME} +! stdout ^test-replica1 +stdout -count=1 ^test-replica2 + +# Confirm that the test-copy index still has 1 replica index +exec algolia settings get ${COPY_NAME} +stdout -count=1 test-replica2 +! stdout test-replica1 + +# Delete the copy index including its replicas +exec algolia indices delete ${COPY_NAME} --include-replicas --confirm --wait +! stderr . + +# Confirm that there is 1 index now +exec algolia indices list +stdout -count=1 ^${INDEX_NAME} +! stdout ^${COPY_NAME} +! stdout ^test-replica1 +! stdout ^test-replica2 diff --git a/e2e/testscripts/indices/replicas.txtar b/e2e/testscripts/indices/replicas.txtar new file mode 100644 index 00000000..391ea082 --- /dev/null +++ b/e2e/testscripts/indices/replicas.txtar @@ -0,0 +1,17 @@ +env INDEX_NAME=test-can-delete-index +env REPLICA_NAME=test-can-delete-replica + +# Create a new index with one replica index +exec algolia settings set ${INDEX_NAME} --replicas ${REPLICA_NAME} --wait +! stderr . + +# Check that you can delete both manually +exec algolia index delete ${INDEX_NAME} ${REPLICA_NAME} --confirm +! stderr . +! stdout . + +# Check that both indices have been deleted +exec algolia index list +! stderr . +! stdout ${INDEX_NAME} +! stdout ${REPLICA_NAME} diff --git a/e2e/testscripts/objects/objects.txtar b/e2e/testscripts/objects/objects.txtar new file mode 100644 index 00000000..6e3e75f3 --- /dev/null +++ b/e2e/testscripts/objects/objects.txtar @@ -0,0 +1,42 @@ +env INDEX_NAME=test-objects + +# Import a record without objectID from a file +! exec algolia objects import ${INDEX_NAME} --file record.jsonl --wait +! stdout . +stderr '^missing objectID on line 0$' + +# Defer cleanup +defer algolia index delete ${INDEX_NAME} --confirm + +# Import a record with autogenerated objectID +exec algolia objects import ${INDEX_NAME} --file record.jsonl --wait --auto-generate-object-id-if-not-exist +! stderr . + +# Check that record exists (use aliases) +exec algolia records list ${INDEX_NAME} +! stderr . +stdout -count=1 '"name":"foo"' +stdout -count=1 'objectID' + +# Add another record from stdin with objectID +stdin objectID.jsonl +exec algolia records import ${INDEX_NAME} --wait --file - +! stderr . + +# Update a record +exec algolia objects update ${INDEX_NAME} --file update.jsonl --wait --create-if-not-exists --continue-on-error +! stderr . + +# Check that record has that new attribute +exec algolia objects browse ${INDEX_NAME} +! stderr . +stdout -count=1 '"level":1' + +-- record.jsonl -- +{"name": "foo"} + +-- objectID.jsonl -- +{"objectID": "test-record-1", "name": "test"} + +-- update.jsonl -- +{"objectID": "test-record-1", "level": 1} diff --git a/e2e/testscripts/rules/rules.txtar b/e2e/testscripts/rules/rules.txtar new file mode 100644 index 00000000..033446da --- /dev/null +++ b/e2e/testscripts/rules/rules.txtar @@ -0,0 +1,41 @@ +env INDEX_NAME=test-rules + +# List rules (empty index should return error) +! exec algolia rules browse ${INDEX_NAME} +! stdout . +stderr -count=1 'index test-rules doesn''t exist' + +# Importing a rule without objectID should fail +stdin without-objectID.json +! exec algolia rules import ${INDEX_NAME} --file - +! stdout . +stderr objectID + +# Importing a rule without consequence should also fail +stdin without-consequence.json +! exec algolia rules import ${INDEX_NAME} --file - +! stdout . +stderr consequence + +# Import rule +exec algolia rules import ${INDEX_NAME} --file rules.json --wait +! stderr . +! stdout . + +# Delete the rule +exec algolia rules delete ${INDEX_NAME} --rule-ids "test-rule-1" --wait --confirm +! stderr . +! stdout . + +# Defer cleanup +defer algolia index delete ${INDEX_NAME} --confirm +! stderr . + +-- without-objectID.json -- +{} + +-- without-consequence.json -- +{"objectID": "foo"} + +-- rules.json -- +{"conditions":[{"anchoring":"contains","pattern":"foo"}],"consequence":{"promote":[{"objectID":"foo","position":0}]},"objectID":"test-rule-1"} diff --git a/e2e/testscripts/search/search.txtar b/e2e/testscripts/search/search.txtar new file mode 100644 index 00000000..3de0b0f1 --- /dev/null +++ b/e2e/testscripts/search/search.txtar @@ -0,0 +1,18 @@ +env INDEX_NAME=test-search + +# Add a record to an index +exec algolia records import ${INDEX_NAME} --file record.json --wait +! stderr . +! stdout . + +# Defer cleanup +defer algolia index delete ${INDEX_NAME} --confirm +! stderr . + +# Search for something +exec algolia search ${INDEX_NAME} --query "test" +! stderr . +stdout -count=1 '"nbHits":1' + +-- record.json -- +{"objectID": "test-record-1", "name": "Test record"} diff --git a/e2e/testscripts/settings/settings.txtar b/e2e/testscripts/settings/settings.txtar new file mode 100644 index 00000000..685483b7 --- /dev/null +++ b/e2e/testscripts/settings/settings.txtar @@ -0,0 +1,42 @@ +# Test importing settings from a file +exec algolia settings import test-settings --file settings.json --wait +! stderr . +! stdout . + +# Defer deleting the test index +defer algolia indices delete test-settings --confirm --include-replicas + +# Check that settings are applied +exec algolia settings get test-settings +stdout -count=1 '"searchableAttributes":\["foo"\]' + +# Test applying some settings from flags +exec algolia settings set test-settings --attributesToRetrieve "foo" --searchableAttributes "bar" --unretrievableAttributes "baz" --attributesForFaceting "searchable(bar)" --replicas "test-settings-replica" --wait +! stderr . + +# Test that the correct settings are applied +exec algolia settings get test-settings +stdout -count=1 '"attributesToRetrieve":\["foo"\]' +stdout -count=1 '"searchableAttributes":\["bar"\]' +stdout -count=1 '"unretrievableAttributes":\["baz"\]' +stdout -count=1 '"attributesForFaceting":\["searchable\(bar\)"\]' +stdout -count=1 '"replicas":\["test-settings-replica"\]' + +# Change a setting +exec algolia settings set test-settings --searchableAttributes "not-changed" --wait +! stderr . + +# Check that change is not applied to replica +exec algolia settings get test-settings-replica +! stdout not-changed + +# Change another setting and forward change to replica +exec algolia settings set test-settings --searchableAttributes "changed" --forward-to-replicas --wait +! stderr . + +# Check that change is also applied to replica +exec algolia settings get test-settings-replica +stdout -count=1 changed + +-- settings.json -- +{"searchableAttributes": ["foo"]} diff --git a/e2e/testscripts/synonyms/synonyms.txtar b/e2e/testscripts/synonyms/synonyms.txtar new file mode 100644 index 00000000..bb22588f --- /dev/null +++ b/e2e/testscripts/synonyms/synonyms.txtar @@ -0,0 +1,38 @@ +env INDEX_NAME=test-synonyms + +# List synonyms (empty index should return error) +! exec algolia synonyms browse ${INDEX_NAME} +! stdout . +stderr -count=1 'index test-synonyms doesn''t exist' + +# Import synonyms from a file +exec algolia synonyms import ${INDEX_NAME} --file synonyms.jsonl --wait +! stderr . +! stdout . + +# Defer cleanup +defer algolia index delete ${INDEX_NAME} --confirm +! stderr . + +# Import a synonym from the command line +stdin stdin.json +exec algolia synonyms import ${INDEX_NAME} --file - --wait +! stderr . +! stdout . + +# Save a synonym using flags +exec algolia synonyms save ${INDEX_NAME} --id 'test-synonym-4' --type altCorrection1 --word foo --corrections bar --wait +! stderr . +! stdout . + +# List synonyms +exec algolia synonyms browse ${INDEX_NAME} +! stderr . +stdout -count=4 'objectID' + +-- synonyms.jsonl -- +{"objectID": "test-synonym-1", "type": "synonym", "synonyms": ["foo", "bar"]} +{"objectID": "test-synonym-2", "type": "synonym", "synonyms": ["bar", "baz"]} + +-- stdin.json -- +{"objectID": "test-synonym-3", "type": "onewaysynonym", "input": "add", "synonyms": ["save"]} diff --git a/e2e/testscripts/version/version.txtar b/e2e/testscripts/version/version.txtar new file mode 100644 index 00000000..4b6d3e27 --- /dev/null +++ b/e2e/testscripts/version/version.txtar @@ -0,0 +1,3 @@ +# Check that we're using the correct version +exec algolia --version +stdout '^algolia version main$' diff --git a/go.mod b/go.mod index d2ddafd3..40423fc7 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/algolia/algoliasearch-client-go/v4 v4.13.0 github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 github.com/briandowns/spinner v1.23.2 + github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 github.com/cli/safeexec v1.0.1 github.com/dustin/go-humanize v1.0.1 github.com/getkin/kin-openapi v0.100.0 @@ -76,5 +77,6 @@ require ( golang.org/x/net v0.37.0 // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/text v0.23.0 // indirect + golang.org/x/tools v0.31.0 // indirect gopkg.in/ini.v1 v1.67.0 // indirect ) diff --git a/go.sum b/go.sum index ec369a71..8cdd9979 100644 --- a/go.sum +++ b/go.sum @@ -14,6 +14,8 @@ github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869 h1:DDGfHa7BWjL4Yn github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= github.com/briandowns/spinner v1.23.2 h1:Zc6ecUnI+YzLmJniCfDNaMbW0Wid1d5+qcTq4L2FW8w= github.com/briandowns/spinner v1.23.2/go.mod h1:LaZeM4wm2Ywy6vO571mvhQNRcWfRUnXOs0RcKV0wYKM= +github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24 h1:QDrhR4JA2n3ij9YQN0u5ZeuvRIIvsUGmf5yPlTS0w8E= +github.com/cli/go-internal v0.0.0-20241025142207-6c48bcd5ce24/go.mod h1:rr9GNING0onuVw8MnracQHn7PcchnFlP882Y0II2KZk= github.com/cli/safeexec v1.0.1 h1:e/C79PbXF4yYTN/wauC4tviMxEV13BwljGj0N9j+N00= github.com/cli/safeexec v1.0.1/go.mod h1:Z/D4tTN8Vs5gXYHDCbaM1S/anmEDnJb1iW0+EJ5zx3Q= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= @@ -194,6 +196,8 @@ golang.org/x/text v0.23.0/go.mod h1:/BLNzu4aZCJ1+kcD0DNRotWKage4q2rGVAg4o22unh4= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.31.0 h1:0EedkvKDbh+qistFTd0Bcwe/YLh4vHwWEkiI0toFIBU= +golang.org/x/tools v0.31.0/go.mod h1:naFTU+Cev749tSJRXJlna0T3WxKvb1kWEx15xA4SdmQ= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/pkg/cmd/synonyms/import/import.go b/pkg/cmd/synonyms/import/import.go index fa7f8ee8..246074d5 100644 --- a/pkg/cmd/synonyms/import/import.go +++ b/pkg/cmd/synonyms/import/import.go @@ -126,11 +126,13 @@ func runImportCmd(opts *ImportOptions) error { // Unmarshal as map[string]interface{} to get the type of the synonym if err := json.Unmarshal(lineB, &synonym); err != nil { + opts.IO.StopProgressIndicator() return fmt.Errorf("failed to parse JSON synonym on line %d: %s", count, err) } err = validateSynonym(synonym) if err != nil { + opts.IO.StopProgressIndicator() return fmt.Errorf("%s on line %d", err, count) } @@ -144,6 +146,7 @@ func runImportCmd(opts *ImportOptions) error { WithForwardToReplicas(opts.ForwardToReplicas), ) if err != nil { + opts.IO.StopProgressIndicator() return err } if opts.Wait { @@ -164,6 +167,7 @@ func runImportCmd(opts *ImportOptions) error { WithForwardToReplicas(opts.ForwardToReplicas), ) if err != nil { + opts.IO.StopProgressIndicator() return err } if opts.Wait { @@ -177,6 +181,7 @@ func runImportCmd(opts *ImportOptions) error { WithForwardToReplicas(opts.ForwardToReplicas), ) if err != nil { + opts.IO.StopProgressIndicator() return err } if opts.Wait { @@ -188,6 +193,7 @@ func runImportCmd(opts *ImportOptions) error { for _, taskID := range taskIDs { _, err := client.WaitForTask(opts.Index, taskID) if err != nil { + opts.IO.StopProgressIndicator() return err } }