From 343abf395b5493f216dac59d51382d5cd505f032 Mon Sep 17 00:00:00 2001 From: curquiza Date: Fri, 23 Oct 2026 18:41:10 +0200 Subject: [PATCH 1/2] The three index write commands: add, update and delete `meili add indexes --id [--primary-key ]` on `POST /indexes`, `meili update indexes --id [--primary-key ] [--new-id ]` on `PATCH /indexes/{index_uid}`, and `meili delete indexes --id --yes` on `DELETE /indexes/{index_uid}`. With `get indexes`, already shipped, the CRUD of the resource is complete; `compact indexes` and `swap indexes` stay out on purpose and keep their rows in `pnpm api:coverage`. `--new-id` is rule 5 of `commands.md` applied: on `update indexes` the path parameter takes `--id`, so the `uid` of the body is `--new-id`, with the API spelling `--uid` as a hidden alias. `add indexes` takes the API spelling the same way, on `--id`. The three routes are asynchronous all the way: an index that already exists, or one that does not, comes back as HTTP 202 with a task that then fails, never as an HTTP error. The integration test of `update indexes` pins that down, exit 7 with the failed task on stdout, because it is the first thing an agent gets wrong. `readJsonObjectBody` joins `src/lib/input.ts`, the one shared piece this change needed: `add` and `update` send a body whose fields are flags, so the body must be an object, and a body that is not one is `cli_input_invalid` before anything is sent. The body travels whole, so a field with no flag reaches the API, which refuses it rather than the CLI dropping it in silence. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/first-version.md | 1 + oclif.manifest.json | 985 +++++++++++++++++- src/commands/add/indexes.ts | 47 + src/commands/delete/indexes.ts | 35 + src/commands/update/indexes.ts | 50 + src/lib/input.ts | 23 +- .../add/__snapshots__/indexes.test.ts.snap | 63 ++ test/commands/add/indexes.test.ts | 85 ++ .../delete/__snapshots__/indexes.test.ts.snap | 60 ++ test/commands/delete/indexes.test.ts | 65 ++ .../update/__snapshots__/indexes.test.ts.snap | 67 ++ test/commands/update/indexes.test.ts | 88 ++ .../__snapshots__/commands.test.ts.snap | 349 +++++++ test/helpers/meilisearch-handlers.ts | 12 + .../meilisearch/add-indexes.test.ts | 32 + .../meilisearch/delete-indexes.test.ts | 42 + .../meilisearch/update-indexes.test.ts | 69 ++ test/lib/input.test.ts | 21 +- 18 files changed, 2040 insertions(+), 54 deletions(-) create mode 100644 src/commands/add/indexes.ts create mode 100644 src/commands/delete/indexes.ts create mode 100644 src/commands/update/indexes.ts create mode 100644 test/commands/add/__snapshots__/indexes.test.ts.snap create mode 100644 test/commands/add/indexes.test.ts create mode 100644 test/commands/delete/__snapshots__/indexes.test.ts.snap create mode 100644 test/commands/delete/indexes.test.ts create mode 100644 test/commands/update/__snapshots__/indexes.test.ts.snap create mode 100644 test/commands/update/indexes.test.ts create mode 100644 test/integration/meilisearch/add-indexes.test.ts create mode 100644 test/integration/meilisearch/delete-indexes.test.ts create mode 100644 test/integration/meilisearch/update-indexes.test.ts diff --git a/.changeset/first-version.md b/.changeset/first-version.md index c3fb834..7a6f7b1 100644 --- a/.changeset/first-version.md +++ b/.changeset/first-version.md @@ -6,6 +6,7 @@ First version of `meili`, the Meilisearch CLI built for coding agents. - Commands derive from the routes of the Meilisearch API and of the Management API, verb first and resource plural: `meili get health`, `meili get indexes`, `meili get indexes --id movies`. - Documents: `meili get documents --index `, narrowed by `--ids` or `--filter`; `meili add documents --index --file ` and `meili update documents --index --file `, which read the documents from a file, from stdin with `--file -` or from `--data`; `meili delete documents --index (--ids | --filter | --all) --yes`. +- Indexes: `meili get indexes [--id ]`; `meili add indexes --id [--primary-key ]`, `meili update indexes --id [--primary-key ] [--new-id ]`, which renames the index, and `meili delete indexes --id --yes`. - `meili schema` prints the index of the command tree as JSON, one entry per command with its routes, and `meili schema --command "get indexes"` prints one command in full, with its key actions and whether it returns a task. - Connection through `--project-url` and `--project-api-key`, or `MEILI_PROJECT_URL` and `MEILI_PROJECT_API_KEY`; `MEILI_MANAGEMENT_TOKEN` and `MEILI_MANAGEMENT_URL` for the Management API. - Several projects in a config file, `~/.config/meili/config.json` or `MEILI_CONFIG`, chosen with `--project` on the command line and never deduced; `meili config list`, `config set` and `config unset` manage the file. diff --git a/oclif.manifest.json b/oclif.manifest.json index 358601b..fc6147b 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -234,6 +234,227 @@ "documents.js" ] }, + "add:indexes": { + "aliases": [], + "args": {}, + "description": "Create an empty index, named by --id, with --primary-key as its primary key.", + "examples": [ + "<%= config.bin %> <%= command.id %> --id movies", + "<%= config.bin %> <%= command.id %> --id movies --primary-key id --output json", + "<%= config.bin %> <%= command.id %> --id movies --wait" + ], + "flags": { + "output": { + "char": "o", + "description": "json (default): one document, the API body as it is, indented in a terminal, compact when piped. ndjson: one item per line, list commands only, streams with --all.", + "name": "output", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "options": [ + "json", + "ndjson" + ], + "type": "option" + }, + "debug": { + "description": "Print every HTTP request and response on stderr, secrets redacted.", + "name": "debug", + "allowNo": false, + "type": "boolean" + }, + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "name": "project-url", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "name": "project-api-key", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "name": "project", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "id": { + "aliases": [ + "uid" + ], + "description": "The index to create.", + "name": "id", + "required": true, + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "primary-key": { + "description": "The field that holds the identifier of each document. Without it, Meilisearch infers it from the first documents.", + "name": "primary-key", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "exclusive": [ + "data" + ], + "name": "file", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "name": "data", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "dependsOn": [ + "wait" + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "name": "timeout", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "add:indexes", + "pluginAlias": "@meilisearch/cli", + "pluginName": "@meilisearch/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "api": "meilisearch", + "routes": [ + "POST /indexes" + ], + "keyActions": [ + "indexes.create" + ], + "connectionFlags": { + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "waitFlags": { + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "dependsOn": [ + "wait" + ], + "input": [], + "multiple": false, + "type": "option" + } + }, + "paginationFlags": { + "limit": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "offset": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "all": { + "description": "Fetch every page. Streams with ndjson, buffers into one array with json.", + "allowNo": false, + "type": "boolean" + } + }, + "bodyInputFlags": { + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "helpValue": "", + "exclusive": [ + "data" + ], + "input": [], + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "confirmFlags": { + "yes": { + "required": true, + "description": "Confirm the deletion. Nothing is sent to the API without it.", + "allowNo": false, + "type": "boolean" + } + }, + "isESM": true, + "relativePath": [ + "dist", + "commands", + "add", + "indexes.js" + ] + }, "auth:login": { "aliases": [], "args": {}, @@ -1254,16 +1475,16 @@ "unset.js" ] }, - "delete:documents": { + "create:documents": { "aliases": [], "args": {}, - "description": "Delete the documents of an index named by --ids, matched by --filter, or every one of them with --all.", + "description": "Add documents to an index, replacing the ones that already have the same primary key.", "examples": [ - "<%= config.bin %> <%= command.id %> --index movies --ids 42,1344 --yes", - "<%= config.bin %> <%= command.id %> --index movies --ids 42 --yes --output json", - "<%= config.bin %> <%= command.id %> --index movies --filter 'rating < 2' --yes", - "<%= config.bin %> <%= command.id %> --index movies --file ids.json --yes", - "<%= config.bin %> <%= command.id %> --index movies --all --yes --wait" + "<%= config.bin %> <%= command.id %> --index movies --file movies.json", + "<%= config.bin %> <%= command.id %> --index movies --file movies.json --output json", + "cat movies.json | <%= config.bin %> <%= command.id %> --index movies --file -", + "<%= config.bin %> <%= command.id %> --index movies --data '[{\"id\":1,\"title\":\"Nosferatu\"}]' --primary-key id", + "<%= config.bin %> <%= command.id %> --index movies --file movies.json --wait" ], "flags": { "output": { @@ -1310,7 +1531,7 @@ "type": "option" }, "index": { - "description": "The index to delete the documents from.", + "description": "The index to add the documents to.", "name": "index", "required": true, "hasDynamicHelp": false, @@ -1318,36 +1539,270 @@ "multiple": false, "type": "option" }, - "ids": { - "aliases": [ - "id" - ], - "description": "Document identifiers to delete, comma-separated.", - "exclusive": [ - "filter", - "all" - ], - "name": "ids", + "primary-key": { + "description": "Primary key of the index. Only taken into account on its first documents.", + "name": "primary-key", "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, - "filter": { - "description": "Delete the documents matching this filter expression, for example `rating < 2`.", - "exclusive": [ - "ids", - "all" - ], - "name": "filter", + "custom-metadata": { + "description": "String attached to the task, returned in the task object.", + "name": "custom-metadata", "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, - "all": { - "description": "Delete every document of the index.", - "exclusive": [ + "skip-creation": { + "description": "Only update documents that already exist, ignore the new ones.", + "name": "skip-creation", + "allowNo": false, + "type": "boolean" + }, + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "exclusive": [ + "data" + ], + "name": "file", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "name": "data", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "dependsOn": [ + "wait" + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "name": "timeout", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "create:documents", + "pluginAlias": "@meilisearch/cli", + "pluginName": "@meilisearch/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "api": "meilisearch", + "routes": [ + "POST /indexes/{index_uid}/documents" + ], + "keyActions": [ + "documents.add" + ], + "connectionFlags": { + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "waitFlags": { + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "dependsOn": [ + "wait" + ], + "input": [], + "multiple": false, + "type": "option" + } + }, + "paginationFlags": { + "limit": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "offset": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "all": { + "description": "Fetch every page. Streams with ndjson, buffers into one array with json.", + "allowNo": false, + "type": "boolean" + } + }, + "bodyInputFlags": { + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "helpValue": "", + "exclusive": [ + "data" + ], + "input": [], + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "confirmFlags": { + "yes": { + "required": true, + "description": "Confirm the deletion. Nothing is sent to the API without it.", + "allowNo": false, + "type": "boolean" + } + }, + "isESM": true, + "relativePath": [ + "dist", + "commands", + "create", + "documents.js" + ] + }, + "delete:documents": { + "aliases": [], + "args": {}, + "description": "Delete the documents of an index named by --ids, matched by --filter, or every one of them with --all.", + "examples": [ + "<%= config.bin %> <%= command.id %> --index movies --ids 42,1344 --yes", + "<%= config.bin %> <%= command.id %> --index movies --ids 42 --yes --output json", + "<%= config.bin %> <%= command.id %> --index movies --filter 'rating < 2' --yes", + "<%= config.bin %> <%= command.id %> --index movies --file ids.json --yes", + "<%= config.bin %> <%= command.id %> --index movies --all --yes --wait" + ], + "flags": { + "output": { + "char": "o", + "description": "json (default): one document, the API body as it is, indented in a terminal, compact when piped. ndjson: one item per line, list commands only, streams with --all.", + "name": "output", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "options": [ + "json", + "ndjson" + ], + "type": "option" + }, + "debug": { + "description": "Print every HTTP request and response on stderr, secrets redacted.", + "name": "debug", + "allowNo": false, + "type": "boolean" + }, + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "name": "project-url", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "name": "project-api-key", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "name": "project", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "index": { + "description": "The index to delete the documents from.", + "name": "index", + "required": true, + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "ids": { + "aliases": [ + "id" + ], + "description": "Document identifiers to delete, comma-separated.", + "exclusive": [ + "filter", + "all" + ], + "name": "ids", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "filter": { + "description": "Delete the documents matching this filter expression, for example `rating < 2`.", + "exclusive": [ + "ids", + "all" + ], + "name": "filter", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "all": { + "description": "Delete every document of the index.", + "exclusive": [ "ids", "filter", "file", @@ -1357,30 +1812,230 @@ "allowNo": false, "type": "boolean" }, - "custom-metadata": { - "description": "String attached to the task, returned in the task object.", - "name": "custom-metadata", + "custom-metadata": { + "description": "String attached to the task, returned in the task object.", + "name": "custom-metadata", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "exclusive": [ + "data" + ], + "name": "file", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "name": "data", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "dependsOn": [ + "wait" + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "name": "timeout", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "yes": { + "description": "Confirm the deletion. Nothing is sent to the API without it.", + "name": "yes", + "required": true, + "allowNo": false, + "type": "boolean" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "delete:documents", + "pluginAlias": "@meilisearch/cli", + "pluginName": "@meilisearch/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "api": "meilisearch", + "routes": [ + "DELETE /indexes/{index_uid}/documents", + "POST /indexes/{index_uid}/documents/delete", + "POST /indexes/{index_uid}/documents/delete-batch" + ], + "keyActions": [ + "documents.delete" + ], + "connectionFlags": { + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "waitFlags": { + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "dependsOn": [ + "wait" + ], + "input": [], + "multiple": false, + "type": "option" + } + }, + "paginationFlags": { + "limit": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "offset": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "all": { + "description": "Fetch every page. Streams with ndjson, buffers into one array with json.", + "allowNo": false, + "type": "boolean" + } + }, + "bodyInputFlags": { + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "helpValue": "", + "exclusive": [ + "data" + ], + "input": [], + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "confirmFlags": { + "yes": { + "required": true, + "description": "Confirm the deletion. Nothing is sent to the API without it.", + "allowNo": false, + "type": "boolean" + } + }, + "isESM": true, + "relativePath": [ + "dist", + "commands", + "delete", + "documents.js" + ] + }, + "delete:indexes": { + "aliases": [], + "args": {}, + "description": "Delete an index, with its documents, its settings and its task history.", + "examples": [ + "<%= config.bin %> <%= command.id %> --id movies --yes", + "<%= config.bin %> <%= command.id %> --id movies --yes --output json", + "<%= config.bin %> <%= command.id %> --id movies --yes --wait" + ], + "flags": { + "output": { + "char": "o", + "description": "json (default): one document, the API body as it is, indented in a terminal, compact when piped. ndjson: one item per line, list commands only, streams with --all.", + "name": "output", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "options": [ + "json", + "ndjson" + ], + "type": "option" + }, + "debug": { + "description": "Print every HTTP request and response on stderr, secrets redacted.", + "name": "debug", + "allowNo": false, + "type": "boolean" + }, + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "name": "project-url", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "name": "project-api-key", "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, - "file": { - "description": "Read the body from a file. `-` reads from stdin.", - "exclusive": [ - "data" - ], - "name": "file", + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "name": "project", "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, - "data": { - "description": "Inline JSON body.", - "name": "data", + "id": { + "description": "The index to delete.", + "name": "id", + "required": true, "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, @@ -1411,7 +2066,7 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "delete:documents", + "id": "delete:indexes", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -1419,12 +2074,10 @@ "enableJsonFlag": false, "api": "meilisearch", "routes": [ - "DELETE /indexes/{index_uid}/documents", - "POST /indexes/{index_uid}/documents/delete", - "POST /indexes/{index_uid}/documents/delete-batch" + "DELETE /indexes/{index_uid}" ], "keyActions": [ - "documents.delete" + "indexes.delete" ], "connectionFlags": { "project-url": { @@ -1519,7 +2172,7 @@ "dist", "commands", "delete", - "documents.js" + "indexes.js" ] }, "get:documents": { @@ -2519,6 +3172,236 @@ "update", "documents.js" ] + }, + "update:indexes": { + "aliases": [], + "args": {}, + "description": "Change the primary key of an index with --primary-key, or rename it with --new-id.", + "examples": [ + "<%= config.bin %> <%= command.id %> --id movies --primary-key movie_id", + "<%= config.bin %> <%= command.id %> --id movies --primary-key movie_id --output json", + "<%= config.bin %> <%= command.id %> --id movies --new-id films", + "<%= config.bin %> <%= command.id %> --id movies --new-id films --wait" + ], + "flags": { + "output": { + "char": "o", + "description": "json (default): one document, the API body as it is, indented in a terminal, compact when piped. ndjson: one item per line, list commands only, streams with --all.", + "name": "output", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "options": [ + "json", + "ndjson" + ], + "type": "option" + }, + "debug": { + "description": "Print every HTTP request and response on stderr, secrets redacted.", + "name": "debug", + "allowNo": false, + "type": "boolean" + }, + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "name": "project-url", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "name": "project-api-key", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "name": "project", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "id": { + "description": "The index to update.", + "name": "id", + "required": true, + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "new-id": { + "aliases": [ + "uid" + ], + "description": "New uid of the index, which renames it.", + "name": "new-id", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "primary-key": { + "description": "The field that holds the identifier of each document.", + "name": "primary-key", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "exclusive": [ + "data" + ], + "name": "file", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "name": "data", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "dependsOn": [ + "wait" + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "name": "timeout", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + } + }, + "hasDynamicHelp": false, + "hiddenAliases": [], + "id": "update:indexes", + "pluginAlias": "@meilisearch/cli", + "pluginName": "@meilisearch/cli", + "pluginType": "core", + "strict": true, + "enableJsonFlag": false, + "api": "meilisearch", + "routes": [ + "PATCH /indexes/{index_uid}" + ], + "keyActions": [ + "indexes.update" + ], + "connectionFlags": { + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "project": { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "waitFlags": { + "wait": { + "description": "Poll the task until it reaches a final status, then print the final task.", + "allowNo": false, + "type": "boolean" + }, + "timeout": { + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "dependsOn": [ + "wait" + ], + "input": [], + "multiple": false, + "type": "option" + } + }, + "paginationFlags": { + "limit": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "offset": { + "description": "Passed to the API.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + }, + "all": { + "description": "Fetch every page. Streams with ndjson, buffers into one array with json.", + "allowNo": false, + "type": "boolean" + } + }, + "bodyInputFlags": { + "file": { + "description": "Read the body from a file. `-` reads from stdin.", + "helpValue": "", + "exclusive": [ + "data" + ], + "input": [], + "multiple": false, + "type": "option" + }, + "data": { + "description": "Inline JSON body.", + "helpValue": "", + "input": [], + "multiple": false, + "type": "option" + } + }, + "confirmFlags": { + "yes": { + "required": true, + "description": "Confirm the deletion. Nothing is sent to the API without it.", + "allowNo": false, + "type": "boolean" + } + }, + "isESM": true, + "relativePath": [ + "dist", + "commands", + "update", + "indexes.js" + ] } }, "version": "0.0.0" diff --git a/src/commands/add/indexes.ts b/src/commands/add/indexes.ts new file mode 100644 index 0000000..7542344 --- /dev/null +++ b/src/commands/add/indexes.ts @@ -0,0 +1,47 @@ +import { Flags } from "@oclif/core"; +import type { IndexOptions } from "meilisearch"; +import { type Api, BaseCommand } from "../../lib/base-command.js"; +import { readJsonObjectBody } from "../../lib/input.js"; + +export default class AddIndexes extends BaseCommand { + static api: Api = "meilisearch"; + + static routes = ["POST /indexes"]; + + static keyActions = ["indexes.create"]; + + static override description = + "Create an empty index, named by --id, with --primary-key as its primary key."; + + static override examples = [ + "<%= config.bin %> <%= command.id %> --id movies", + "<%= config.bin %> <%= command.id %> --id movies --primary-key id --output json", + "<%= config.bin %> <%= command.id %> --id movies --wait", + ]; + + static override flags = { + ...BaseCommand.connectionFlags, + id: Flags.string({ + required: true, + aliases: ["uid"], + description: "The index to create.", + helpValue: "", + }), + "primary-key": Flags.string({ + description: + "The field that holds the identifier of each document. Without it, Meilisearch infers it from the first documents.", + helpValue: "", + }), + ...BaseCommand.bodyInputFlags, + ...BaseCommand.waitFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(AddIndexes); + // The body goes out whole, so a field this command declares no flag for + // reaches the API, which refuses it rather than the CLI dropping it. + const options = { ...readJsonObjectBody(flags) } as IndexOptions; + if (flags["primary-key"] !== undefined) options.primaryKey = flags["primary-key"]; + await this.outputTask(await this.meilisearch.createIndex(flags.id, options), flags); + } +} diff --git a/src/commands/delete/indexes.ts b/src/commands/delete/indexes.ts new file mode 100644 index 0000000..fee5084 --- /dev/null +++ b/src/commands/delete/indexes.ts @@ -0,0 +1,35 @@ +import { Flags } from "@oclif/core"; +import { type Api, BaseCommand } from "../../lib/base-command.js"; + +export default class DeleteIndexes extends BaseCommand { + static api: Api = "meilisearch"; + + static routes = ["DELETE /indexes/{index_uid}"]; + + static keyActions = ["indexes.delete"]; + + static override description = + "Delete an index, with its documents, its settings and its task history."; + + static override examples = [ + "<%= config.bin %> <%= command.id %> --id movies --yes", + "<%= config.bin %> <%= command.id %> --id movies --yes --output json", + "<%= config.bin %> <%= command.id %> --id movies --yes --wait", + ]; + + static override flags = { + ...BaseCommand.connectionFlags, + id: Flags.string({ + required: true, + description: "The index to delete.", + helpValue: "", + }), + ...BaseCommand.waitFlags, + ...BaseCommand.confirmFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(DeleteIndexes); + await this.outputTask(await this.meilisearch.deleteIndex(flags.id), flags); + } +} diff --git a/src/commands/update/indexes.ts b/src/commands/update/indexes.ts new file mode 100644 index 0000000..82a4c96 --- /dev/null +++ b/src/commands/update/indexes.ts @@ -0,0 +1,50 @@ +import { Flags } from "@oclif/core"; +import type { IndexOptions } from "meilisearch"; +import { type Api, BaseCommand } from "../../lib/base-command.js"; +import { readJsonObjectBody } from "../../lib/input.js"; + +export default class UpdateIndexes extends BaseCommand { + static api: Api = "meilisearch"; + + static routes = ["PATCH /indexes/{index_uid}"]; + + static keyActions = ["indexes.update"]; + + static override description = + "Change the primary key of an index with --primary-key, or rename it with --new-id."; + + static override examples = [ + "<%= config.bin %> <%= command.id %> --id movies --primary-key movie_id", + "<%= config.bin %> <%= command.id %> --id movies --primary-key movie_id --output json", + "<%= config.bin %> <%= command.id %> --id movies --new-id films", + "<%= config.bin %> <%= command.id %> --id movies --new-id films --wait", + ]; + + static override flags = { + ...BaseCommand.connectionFlags, + id: Flags.string({ + required: true, + description: "The index to update.", + helpValue: "", + }), + "new-id": Flags.string({ + aliases: ["uid"], + description: "New uid of the index, which renames it.", + helpValue: "", + }), + "primary-key": Flags.string({ + description: "The field that holds the identifier of each document.", + helpValue: "", + }), + ...BaseCommand.bodyInputFlags, + ...BaseCommand.waitFlags, + }; + + async run(): Promise { + const { flags } = await this.parse(UpdateIndexes); + const options = { ...readJsonObjectBody(flags) } as IndexOptions; + if (flags["primary-key"] !== undefined) options.primaryKey = flags["primary-key"]; + if (flags["new-id"] !== undefined) options.uid = flags["new-id"]; + await this.outputTask(await this.meilisearch.updateIndex(flags.id, options), flags); + } +} diff --git a/src/lib/input.ts b/src/lib/input.ts index e4a1502..ca315f6 100644 --- a/src/lib/input.ts +++ b/src/lib/input.ts @@ -27,14 +27,33 @@ export interface BodyInputFlagValues { export function readJsonBody(flags: BodyInputFlagValues): unknown { const text = flags.data ?? (flags.file === undefined ? undefined : readBodyFile(flags.file)); if (text === undefined) return undefined; - const source = flags.data === undefined ? `\`${flags.file}\`` : "--data"; try { return JSON.parse(text); } catch (cause) { - throw new MeiliCliError("cli_input_invalid", `${source} is not valid JSON.`, { cause }); + throw new MeiliCliError("cli_input_invalid", `${bodySource(flags)} is not valid JSON.`, { + cause, + }); } } +/** + * The same body, for a route whose body is an object of named fields. A body + * that is not an object is `cli_input_invalid`, exit 2, before anything is sent. + */ +export function readJsonObjectBody(flags: BodyInputFlagValues): Record { + const body = readJsonBody(flags); + if (body === undefined) return {}; + if (!isJsonObject(body)) { + throw new MeiliCliError("cli_input_invalid", `${bodySource(flags)} must be a JSON object.`); + } + return body; +} + +/** Which of the two flags carried the body, for the message of an error about it. */ +function bodySource(flags: BodyInputFlagValues): string { + return flags.data === undefined ? `\`${flags.file}\`` : "--data"; +} + function readBodyFile(path: string): string { if (path === "-") return readStdin(); try { diff --git a/test/commands/add/__snapshots__/indexes.test.ts.snap b/test/commands/add/__snapshots__/indexes.test.ts.snap new file mode 100644 index 0000000..53d0e24 --- /dev/null +++ b/test/commands/add/__snapshots__/indexes.test.ts.snap @@ -0,0 +1,63 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`meili add indexes > passes an authentication error through with a hint and exits 3 1`] = ` +{ + "exitCode": 3, + "stderr": "{"error":{"code":"invalid_api_key","type":"auth","message":"The provided API key is invalid.","link":"https://docs.meilisearch.com/errors#invalid_api_key","hint":"Provide an API key with \`--project-api-key \` or \`MEILI_PROJECT_API_KEY\`."}} +", + "stdout": "", +} +`; + +exports[`meili add indexes > prints its help without any network call 1`] = ` +"Create an empty index, named by --id, with --primary-key as its primary key. + +USAGE + $ meili add indexes --id [-o ] [--debug] [--project-url + ] [--project-api-key ] [--project ] [--primary-key ] + [--file | --data ] [--timeout --wait] + +FLAGS + -o, --output= json (default): one document, the API body as it + is, indented in a terminal, compact when piped. + ndjson: one item per line, list commands only, + streams with --all. + --data= Inline JSON body. + --debug Print every HTTP request and response on stderr, + secrets redacted. + --file= Read the body from a file. \`-\` reads from stdin. + --id= (required) The index to create. + --primary-key= The field that holds the identifier of each + document. Without it, Meilisearch infers it from + the first documents. + --project= A project of the config file. Cannot be combined + with --project-url. Required when the file has + several projects. + --project-api-key= Project API key. \`-\` reads it from stdin. + --project-url= Project URL, the Meilisearch instance to talk to. + --timeout= Maximum wait time, for example 30s, 2m. Default + 60s. Only with --wait. + --wait Poll the task until it reaches a final status, + then print the final task. + +DESCRIPTION + Create an empty index, named by --id, with --primary-key as its primary key. + +EXAMPLES + $ meili add indexes --id movies + + $ meili add indexes --id movies --primary-key id --output json + + $ meili add indexes --id movies --wait + +" +`; + +exports[`meili add indexes > prints the enqueued task as the API returned it, without waiting 1`] = ` +{ + "exitCode": 0, + "stderr": "", + "stdout": "{"taskUid":11,"indexUid":"movies","status":"enqueued","type":"indexCreation","enqueuedAt":"2026-09-24T10:00:00Z"} +", +} +`; diff --git a/test/commands/add/indexes.test.ts b/test/commands/add/indexes.test.ts new file mode 100644 index 0000000..84f42b4 --- /dev/null +++ b/test/commands/add/indexes.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from "vitest"; +import AddIndexes from "../../../src/commands/add/indexes.js"; +import { + invalidApiKeyError, + MEILISEARCH_URL, + makeEnqueuedTask, + meilisearchHandlers, +} from "../../helpers/meilisearch-handlers.js"; +import { countRequests, recordRequests, server, useMockServer } from "../../helpers/msw.js"; +import { runCli, runCommand } from "../../helpers/run-command.js"; + +useMockServer(); + +const env = { MEILI_PROJECT_URL: MEILISEARCH_URL }; +const created = makeEnqueuedTask({ taskUid: 11, type: "indexCreation" }); + +/** The body the command sent to `POST /indexes`. */ +async function sentBody(argv: string[]): Promise { + server.use(meilisearchHandlers.indexCreate(created)); + const requests = await recordRequests(async () => { + await runCommand(AddIndexes, argv, { env }); + }); + expect(requests).toHaveLength(1); + expect(requests[0]?.method).toBe("POST"); + expect(requests[0]?.url.pathname).toBe("/indexes"); + return JSON.parse(requests[0]?.body ?? ""); +} + +describe("meili add indexes", () => { + it("prints its help without any network call", async () => { + const result = await runCli(["add", "indexes", "--help"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toMatchSnapshot(); + }); + + it("prints the enqueued task as the API returned it, without waiting", async () => { + server.use(meilisearchHandlers.indexCreate(created)); + const result = await runCommand(AddIndexes, ["--id", "movies", "--output", "json"], { env }); + expect(result).toMatchSnapshot(); + }); + + it("sends the index and its primary key in the body", async () => { + expect(await sentBody(["--id", "movies", "--primary-key", "id"])).toEqual({ + uid: "movies", + primaryKey: "id", + }); + }); + + it("sends the index alone when no flag gives a primary key", async () => { + expect(await sentBody(["--id", "movies"])).toEqual({ uid: "movies" }); + }); + + it("accepts --uid, the API spelling of --id", async () => { + expect(await sentBody(["--uid", "movies"])).toEqual({ uid: "movies" }); + }); + + it("sends the fields of --data, and lets --primary-key override the one it carries", async () => { + const body = await sentBody([ + "--id", + "movies", + "--data", + '{"primaryKey":"isbn","primarykey":"typo"}', + "--primary-key", + "id", + ]); + expect(body).toEqual({ uid: "movies", primaryKey: "id", primarykey: "typo" }); + }); + + it("refuses a body that is not a JSON object, before any network call", async () => { + let result: Awaited> | undefined; + const requests = await countRequests(async () => { + result = await runCommand(AddIndexes, ["--id", "movies", "--data", "42"], { env }); + }); + expect(requests).toBe(0); + expect(result?.exitCode).toBe(2); + expect(JSON.parse(result?.stderr ?? "").error.code).toBe("cli_input_invalid"); + }); + + it("passes an authentication error through with a hint and exits 3", async () => { + server.use(meilisearchHandlers.apiError("/indexes", 403, invalidApiKeyError)); + const result = await runCommand(AddIndexes, ["--id", "movies", "--output", "json"], { env }); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/test/commands/delete/__snapshots__/indexes.test.ts.snap b/test/commands/delete/__snapshots__/indexes.test.ts.snap new file mode 100644 index 0000000..f1ef4ae --- /dev/null +++ b/test/commands/delete/__snapshots__/indexes.test.ts.snap @@ -0,0 +1,60 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`meili delete indexes > passes an authentication error through with a hint and exits 3 1`] = ` +{ + "exitCode": 3, + "stderr": "{"error":{"code":"invalid_api_key","type":"auth","message":"The provided API key is invalid.","link":"https://docs.meilisearch.com/errors#invalid_api_key","hint":"Provide an API key with \`--project-api-key \` or \`MEILI_PROJECT_API_KEY\`."}} +", + "stdout": "", +} +`; + +exports[`meili delete indexes > prints its help without any network call 1`] = ` +"Delete an index, with its documents, its settings and its task history. + +USAGE + $ meili delete indexes --id --yes [-o ] [--debug] + [--project-url ] [--project-api-key ] [--project ] + [--timeout --wait] + +FLAGS + -o, --output= json (default): one document, the API body as it + is, indented in a terminal, compact when piped. + ndjson: one item per line, list commands only, + streams with --all. + --debug Print every HTTP request and response on stderr, + secrets redacted. + --id= (required) The index to delete. + --project= A project of the config file. Cannot be combined + with --project-url. Required when the file has + several projects. + --project-api-key= Project API key. \`-\` reads it from stdin. + --project-url= Project URL, the Meilisearch instance to talk to. + --timeout= Maximum wait time, for example 30s, 2m. Default + 60s. Only with --wait. + --wait Poll the task until it reaches a final status, + then print the final task. + --yes (required) Confirm the deletion. Nothing is sent + to the API without it. + +DESCRIPTION + Delete an index, with its documents, its settings and its task history. + +EXAMPLES + $ meili delete indexes --id movies --yes + + $ meili delete indexes --id movies --yes --output json + + $ meili delete indexes --id movies --yes --wait + +" +`; + +exports[`meili delete indexes > prints the enqueued task as the API returned it, without waiting 1`] = ` +{ + "exitCode": 0, + "stderr": "", + "stdout": "{"taskUid":13,"indexUid":"movies","status":"enqueued","type":"indexDeletion","enqueuedAt":"2026-09-24T10:00:00Z"} +", +} +`; diff --git a/test/commands/delete/indexes.test.ts b/test/commands/delete/indexes.test.ts new file mode 100644 index 0000000..04846ec --- /dev/null +++ b/test/commands/delete/indexes.test.ts @@ -0,0 +1,65 @@ +import { describe, expect, it } from "vitest"; +import DeleteIndexes from "../../../src/commands/delete/indexes.js"; +import { + invalidApiKeyError, + MEILISEARCH_URL, + makeEnqueuedTask, + meilisearchHandlers, +} from "../../helpers/meilisearch-handlers.js"; +import { countRequests, recordRequests, server, useMockServer } from "../../helpers/msw.js"; +import { runCli, runCommand } from "../../helpers/run-command.js"; + +useMockServer(); + +const env = { MEILI_PROJECT_URL: MEILISEARCH_URL }; +const deleted = makeEnqueuedTask({ taskUid: 13, type: "indexDeletion" }); + +describe("meili delete indexes", () => { + it("prints its help without any network call", async () => { + const result = await runCli(["delete", "indexes", "--help"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toMatchSnapshot(); + }); + + it("prints the enqueued task as the API returned it, without waiting", async () => { + server.use(meilisearchHandlers.indexDelete(deleted)); + const result = await runCommand( + DeleteIndexes, + ["--id", "movies", "--yes", "--output", "json"], + { env }, + ); + expect(result).toMatchSnapshot(); + }); + + it("names the index in the path", async () => { + server.use(meilisearchHandlers.indexDelete(deleted)); + const requests = await recordRequests(async () => { + await runCommand(DeleteIndexes, ["--id", "movies", "--yes"], { env }); + }); + expect(requests).toHaveLength(1); + expect(requests[0]?.method).toBe("DELETE"); + expect(requests[0]?.url.pathname).toBe("/indexes/movies"); + }); + + it("refuses to run without --yes, before any network call", async () => { + let result: Awaited> | undefined; + const requests = await countRequests(async () => { + result = await runCommand(DeleteIndexes, ["--id", "movies"], { env }); + }); + expect(requests).toBe(0); + expect(result?.stdout).toBe(""); + expect(result?.exitCode).toBe(2); + expect(JSON.parse(result?.stderr ?? "").error.code).toBe("cli_usage"); + }); + + it("passes an authentication error through with a hint and exits 3", async () => { + server.use(meilisearchHandlers.apiError("/indexes/movies", 403, invalidApiKeyError)); + const result = await runCommand( + DeleteIndexes, + ["--id", "movies", "--yes", "--output", "json"], + { env }, + ); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/test/commands/update/__snapshots__/indexes.test.ts.snap b/test/commands/update/__snapshots__/indexes.test.ts.snap new file mode 100644 index 0000000..69a9fec --- /dev/null +++ b/test/commands/update/__snapshots__/indexes.test.ts.snap @@ -0,0 +1,67 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`meili update indexes > passes an authentication error through with a hint and exits 3 1`] = ` +{ + "exitCode": 3, + "stderr": "{"error":{"code":"invalid_api_key","type":"auth","message":"The provided API key is invalid.","link":"https://docs.meilisearch.com/errors#invalid_api_key","hint":"Provide an API key with \`--project-api-key \` or \`MEILI_PROJECT_API_KEY\`."}} +", + "stdout": "", +} +`; + +exports[`meili update indexes > prints its help without any network call 1`] = ` +"Change the primary key of an index with --primary-key, or rename it with --new-id. + +USAGE + $ meili update indexes --id [-o ] [--debug] [--project-url + ] [--project-api-key ] [--project ] [--new-id ] + [--primary-key ] [--file | --data ] [--timeout + --wait] + +FLAGS + -o, --output= json (default): one document, the API body as it + is, indented in a terminal, compact when piped. + ndjson: one item per line, list commands only, + streams with --all. + --data= Inline JSON body. + --debug Print every HTTP request and response on stderr, + secrets redacted. + --file= Read the body from a file. \`-\` reads from stdin. + --id= (required) The index to update. + --new-id= New uid of the index, which renames it. + --primary-key= The field that holds the identifier of each + document. + --project= A project of the config file. Cannot be combined + with --project-url. Required when the file has + several projects. + --project-api-key= Project API key. \`-\` reads it from stdin. + --project-url= Project URL, the Meilisearch instance to talk to. + --timeout= Maximum wait time, for example 30s, 2m. Default + 60s. Only with --wait. + --wait Poll the task until it reaches a final status, + then print the final task. + +DESCRIPTION + Change the primary key of an index with --primary-key, or rename it with + --new-id. + +EXAMPLES + $ meili update indexes --id movies --primary-key movie_id + + $ meili update indexes --id movies --primary-key movie_id --output json + + $ meili update indexes --id movies --new-id films + + $ meili update indexes --id movies --new-id films --wait + +" +`; + +exports[`meili update indexes > prints the enqueued task as the API returned it, without waiting 1`] = ` +{ + "exitCode": 0, + "stderr": "", + "stdout": "{"taskUid":12,"indexUid":"movies","status":"enqueued","type":"indexUpdate","enqueuedAt":"2026-09-24T10:00:00Z"} +", +} +`; diff --git a/test/commands/update/indexes.test.ts b/test/commands/update/indexes.test.ts new file mode 100644 index 0000000..7ad329f --- /dev/null +++ b/test/commands/update/indexes.test.ts @@ -0,0 +1,88 @@ +import { describe, expect, it } from "vitest"; +import UpdateIndexes from "../../../src/commands/update/indexes.js"; +import { + invalidApiKeyError, + MEILISEARCH_URL, + makeEnqueuedTask, + meilisearchHandlers, +} from "../../helpers/meilisearch-handlers.js"; +import { recordRequests, server, useMockServer } from "../../helpers/msw.js"; +import { runCli, runCommand } from "../../helpers/run-command.js"; + +useMockServer(); + +const env = { MEILI_PROJECT_URL: MEILISEARCH_URL }; +const updated = makeEnqueuedTask({ taskUid: 12, type: "indexUpdate" }); + +/** The path and the body the command sent to `PATCH /indexes/{uid}`. */ +async function sent(argv: string[]): Promise<{ pathname: string; body: unknown }> { + server.use(meilisearchHandlers.indexUpdate(updated)); + const requests = await recordRequests(async () => { + await runCommand(UpdateIndexes, argv, { env }); + }); + expect(requests).toHaveLength(1); + expect(requests[0]?.method).toBe("PATCH"); + return { + pathname: requests[0]?.url.pathname ?? "", + body: JSON.parse(requests[0]?.body ?? ""), + }; +} + +describe("meili update indexes", () => { + it("prints its help without any network call", async () => { + const result = await runCli(["update", "indexes", "--help"]); + expect(result.exitCode).toBe(0); + expect(result.stderr).toBe(""); + expect(result.stdout).toMatchSnapshot(); + }); + + it("prints the enqueued task as the API returned it, without waiting", async () => { + server.use(meilisearchHandlers.indexUpdate(updated)); + const result = await runCommand( + UpdateIndexes, + ["--id", "movies", "--primary-key", "movie_id", "--output", "json"], + { env }, + ); + expect(result).toMatchSnapshot(); + }); + + it("names the index in the path and sends the new primary key in the body", async () => { + expect(await sent(["--id", "movies", "--primary-key", "movie_id"])).toEqual({ + pathname: "/indexes/movies", + body: { primaryKey: "movie_id" }, + }); + }); + + it("renames the index with --new-id, which carries the new uid", async () => { + expect(await sent(["--id", "movies", "--new-id", "films"])).toEqual({ + pathname: "/indexes/movies", + body: { uid: "films" }, + }); + }); + + it("accepts --uid, the API spelling of --new-id", async () => { + expect((await sent(["--id", "movies", "--uid", "films"])).body).toEqual({ uid: "films" }); + }); + + it("sends the fields of --data, and lets the flags override the ones it carries", async () => { + const { body } = await sent([ + "--id", + "movies", + "--data", + '{"primaryKey":"isbn","uid":"books"}', + "--new-id", + "films", + ]); + expect(body).toEqual({ primaryKey: "isbn", uid: "films" }); + }); + + it("passes an authentication error through with a hint and exits 3", async () => { + server.use(meilisearchHandlers.apiError("/indexes/movies", 403, invalidApiKeyError)); + const result = await runCommand( + UpdateIndexes, + ["--id", "movies", "--primary-key", "id", "--output", "json"], + { env }, + ); + expect(result).toMatchSnapshot(); + }); +}); diff --git a/test/contract/__snapshots__/commands.test.ts.snap b/test/contract/__snapshots__/commands.test.ts.snap index bc80d88..529c08a 100644 --- a/test/contract/__snapshots__/commands.test.ts.snap +++ b/test/contract/__snapshots__/commands.test.ts.snap @@ -95,6 +95,85 @@ exports[`oclif.manifest.json > \`meili schema\` is a snapshot, the index and eve "POST /indexes/{index_uid}/documents", ], }, + { + "api": "meilisearch", + "async": true, + "description": "Create an empty index, named by --id, with --primary-key as its primary key.", + "examples": [ + "meili add indexes --id movies", + "meili add indexes --id movies --primary-key id --output json", + "meili add indexes --id movies --wait", + ], + "flags": [ + { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "name": "project-url", + "type": "option", + }, + { + "description": "Project API key. \`-\` reads it from stdin.", + "helpValue": "", + "name": "project-api-key", + "type": "option", + }, + { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "name": "project", + "type": "option", + }, + { + "description": "The index to create.", + "helpValue": "", + "name": "id", + "required": true, + "type": "option", + }, + { + "description": "The field that holds the identifier of each document. Without it, Meilisearch infers it from the first documents.", + "helpValue": "", + "name": "primary-key", + "type": "option", + }, + { + "description": "Read the body from a file. \`-\` reads from stdin.", + "exclusive": [ + "data", + ], + "helpValue": "", + "name": "file", + "type": "option", + }, + { + "description": "Inline JSON body.", + "helpValue": "", + "name": "data", + "type": "option", + }, + { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "type": "boolean", + }, + { + "dependsOn": [ + "wait", + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "name": "timeout", + "type": "option", + }, + ], + "id": "add indexes", + "keyActions": [ + "indexes.create", + ], + "routes": [ + "POST /indexes", + ], + }, { "api": "none", "description": "Log in to Meilisearch Cloud with a browser, and store the token for the management commands.", @@ -225,6 +304,98 @@ The file holds API keys and decides where every later command goes, for every pr ], "id": "config unset", }, + { + "api": "meilisearch", + "async": true, + "description": "Add documents to an index, replacing the ones that already have the same primary key.", + "examples": [ + "meili create documents --index movies --file movies.json", + "meili create documents --index movies --file movies.json --output json", + "cat movies.json | meili create documents --index movies --file -", + "meili create documents --index movies --data '[{"id":1,"title":"Nosferatu"}]' --primary-key id", + "meili create documents --index movies --file movies.json --wait", + ], + "flags": [ + { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "name": "project-url", + "type": "option", + }, + { + "description": "Project API key. \`-\` reads it from stdin.", + "helpValue": "", + "name": "project-api-key", + "type": "option", + }, + { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "name": "project", + "type": "option", + }, + { + "description": "The index to add the documents to.", + "helpValue": "", + "name": "index", + "required": true, + "type": "option", + }, + { + "description": "Primary key of the index. Only taken into account on its first documents.", + "helpValue": "", + "name": "primary-key", + "type": "option", + }, + { + "description": "String attached to the task, returned in the task object.", + "helpValue": "", + "name": "custom-metadata", + "type": "option", + }, + { + "description": "Only update documents that already exist, ignore the new ones.", + "name": "skip-creation", + "type": "boolean", + }, + { + "description": "Read the body from a file. \`-\` reads from stdin.", + "exclusive": [ + "data", + ], + "helpValue": "", + "name": "file", + "type": "option", + }, + { + "description": "Inline JSON body.", + "helpValue": "", + "name": "data", + "type": "option", + }, + { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "type": "boolean", + }, + { + "dependsOn": [ + "wait", + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "name": "timeout", + "type": "option", + }, + ], + "id": "create documents", + "keyActions": [ + "documents.add", + ], + "routes": [ + "POST /indexes/{index_uid}/documents", + ], + }, { "api": "meilisearch", "async": true, @@ -345,6 +516,70 @@ The file holds API keys and decides where every later command goes, for every pr "POST /indexes/{index_uid}/documents/delete-batch", ], }, + { + "api": "meilisearch", + "async": true, + "description": "Delete an index, with its documents, its settings and its task history.", + "examples": [ + "meili delete indexes --id movies --yes", + "meili delete indexes --id movies --yes --output json", + "meili delete indexes --id movies --yes --wait", + ], + "flags": [ + { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "name": "project-url", + "type": "option", + }, + { + "description": "Project API key. \`-\` reads it from stdin.", + "helpValue": "", + "name": "project-api-key", + "type": "option", + }, + { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "name": "project", + "type": "option", + }, + { + "description": "The index to delete.", + "helpValue": "", + "name": "id", + "required": true, + "type": "option", + }, + { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "type": "boolean", + }, + { + "dependsOn": [ + "wait", + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "name": "timeout", + "type": "option", + }, + { + "description": "Confirm the deletion. Nothing is sent to the API without it.", + "name": "yes", + "required": true, + "type": "boolean", + }, + ], + "id": "delete indexes", + "keyActions": [ + "indexes.delete", + ], + "routes": [ + "DELETE /indexes/{index_uid}", + ], + }, { "api": "meilisearch", "description": "List the documents of an index, narrowed by --ids or by --filter.", @@ -657,6 +892,92 @@ The file holds API keys and decides where every later command goes, for every pr "PUT /indexes/{index_uid}/documents", ], }, + { + "api": "meilisearch", + "async": true, + "description": "Change the primary key of an index with --primary-key, or rename it with --new-id.", + "examples": [ + "meili update indexes --id movies --primary-key movie_id", + "meili update indexes --id movies --primary-key movie_id --output json", + "meili update indexes --id movies --new-id films", + "meili update indexes --id movies --new-id films --wait", + ], + "flags": [ + { + "description": "Project URL, the Meilisearch instance to talk to.", + "helpValue": "", + "name": "project-url", + "type": "option", + }, + { + "description": "Project API key. \`-\` reads it from stdin.", + "helpValue": "", + "name": "project-api-key", + "type": "option", + }, + { + "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", + "helpValue": "", + "name": "project", + "type": "option", + }, + { + "description": "The index to update.", + "helpValue": "", + "name": "id", + "required": true, + "type": "option", + }, + { + "description": "New uid of the index, which renames it.", + "helpValue": "", + "name": "new-id", + "type": "option", + }, + { + "description": "The field that holds the identifier of each document.", + "helpValue": "", + "name": "primary-key", + "type": "option", + }, + { + "description": "Read the body from a file. \`-\` reads from stdin.", + "exclusive": [ + "data", + ], + "helpValue": "", + "name": "file", + "type": "option", + }, + { + "description": "Inline JSON body.", + "helpValue": "", + "name": "data", + "type": "option", + }, + { + "description": "Poll the task until it reaches a final status, then print the final task.", + "name": "wait", + "type": "boolean", + }, + { + "dependsOn": [ + "wait", + ], + "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", + "helpValue": "", + "name": "timeout", + "type": "option", + }, + ], + "id": "update indexes", + "keyActions": [ + "indexes.update", + ], + "routes": [ + "PATCH /indexes/{index_uid}", + ], + }, ], "index": { "commands": [ @@ -667,6 +988,13 @@ The file holds API keys and decides where every later command goes, for every pr "POST /indexes/{index_uid}/documents", ], }, + { + "description": "Create an empty index, named by --id, with --primary-key as its primary key.", + "id": "add indexes", + "routes": [ + "POST /indexes", + ], + }, { "description": "Log in to Meilisearch Cloud with a browser, and store the token for the management commands.", "id": "auth login", @@ -695,6 +1023,13 @@ The file holds API keys and decides where every later command goes, for every pr "description": "Remove a project from the config file.", "id": "config unset", }, + { + "description": "Add documents to an index, replacing the ones that already have the same primary key.", + "id": "create documents", + "routes": [ + "POST /indexes/{index_uid}/documents", + ], + }, { "description": "Delete the documents of an index named by --ids, matched by --filter, or every one of them with --all.", "id": "delete documents", @@ -704,6 +1039,13 @@ The file holds API keys and decides where every later command goes, for every pr "POST /indexes/{index_uid}/documents/delete-batch", ], }, + { + "description": "Delete an index, with its documents, its settings and its task history.", + "id": "delete indexes", + "routes": [ + "DELETE /indexes/{index_uid}", + ], + }, { "description": "List the documents of an index, narrowed by --ids or by --filter.", "id": "get documents", @@ -737,6 +1079,13 @@ The file holds API keys and decides where every later command goes, for every pr "PUT /indexes/{index_uid}/documents", ], }, + { + "description": "Change the primary key of an index with --primary-key, or rename it with --new-id.", + "id": "update indexes", + "routes": [ + "PATCH /indexes/{index_uid}", + ], + }, ], "globalFlags": [ { diff --git a/test/helpers/meilisearch-handlers.ts b/test/helpers/meilisearch-handlers.ts index 9c39c2b..4125bbc 100644 --- a/test/helpers/meilisearch-handlers.ts +++ b/test/helpers/meilisearch-handlers.ts @@ -44,6 +44,18 @@ export const meilisearchHandlers = { : HttpResponse.json(found); }), + /** `POST /indexes`, the creation route. */ + indexCreate: (task: EnqueuedTask) => + http.post(`${MEILISEARCH_URL}/indexes`, () => HttpResponse.json(task)), + + /** `PATCH /indexes/{uid}`, the update route. */ + indexUpdate: (task: EnqueuedTask) => + http.patch(`${MEILISEARCH_URL}/indexes/:uid`, () => HttpResponse.json(task)), + + /** `DELETE /indexes/{uid}`, the deletion route. */ + indexDelete: (task: EnqueuedTask) => + http.delete(`${MEILISEARCH_URL}/indexes/:uid`, () => HttpResponse.json(task)), + /** * `POST /indexes/{uid}/documents/fetch` paginating over `all` like the API * does: `limit` defaults to 20, `offset` to 0, and `ids` narrows the list. diff --git a/test/integration/meilisearch/add-indexes.test.ts b/test/integration/meilisearch/add-indexes.test.ts new file mode 100644 index 0000000..c5dabe5 --- /dev/null +++ b/test/integration/meilisearch/add-indexes.test.ts @@ -0,0 +1,32 @@ +import { randomUUID } from "node:crypto"; +import { Meilisearch } from "meilisearch"; +import { afterAll, describe, expect, it } from "vitest"; +import { runBinary } from "../../helpers/run-binary.js"; +import { MASTER_KEY, MEILISEARCH_URL, meilisearchAvailable } from "./setup.js"; + +const meilisearchUp = await meilisearchAvailable(); + +const env = { MEILI_PROJECT_URL: MEILISEARCH_URL, MEILI_PROJECT_API_KEY: MASTER_KEY }; +const uid = `meili-cli-test-add-indexes-${randomUUID().slice(0, 8)}`; + +describe.skipIf(!meilisearchUp)("meili add indexes against a real Meilisearch", () => { + const client = new Meilisearch({ host: MEILISEARCH_URL, apiKey: MASTER_KEY }); + + afterAll(async () => { + await client.deleteIndex(uid).waitTask(); + }); + + it("creates the index with its primary key", async () => { + const result = await runBinary( + ["add", "indexes", "--id", uid, "--primary-key", "id", "--wait", "--output", "json"], + env, + ); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + indexUid: uid, + type: "indexCreation", + status: "succeeded", + }); + expect(await client.index(uid).getRawInfo()).toMatchObject({ uid, primaryKey: "id" }); + }); +}); diff --git a/test/integration/meilisearch/delete-indexes.test.ts b/test/integration/meilisearch/delete-indexes.test.ts new file mode 100644 index 0000000..ea1ab6c --- /dev/null +++ b/test/integration/meilisearch/delete-indexes.test.ts @@ -0,0 +1,42 @@ +import { randomUUID } from "node:crypto"; +import { Meilisearch } from "meilisearch"; +import { beforeAll, describe, expect, it } from "vitest"; +import { runBinary } from "../../helpers/run-binary.js"; +import { MASTER_KEY, MEILISEARCH_URL, meilisearchAvailable } from "./setup.js"; + +const meilisearchUp = await meilisearchAvailable(); + +const env = { MEILI_PROJECT_URL: MEILISEARCH_URL, MEILI_PROJECT_API_KEY: MASTER_KEY }; +const uid = `meili-cli-test-delete-indexes-${randomUUID().slice(0, 8)}`; + +// The two tests share one index, in order: the first leaves it standing, the +// second deletes it, so the command under test is also the teardown. +describe.skipIf(!meilisearchUp)("meili delete indexes against a real Meilisearch", () => { + const client = new Meilisearch({ host: MEILISEARCH_URL, apiKey: MASTER_KEY }); + + beforeAll(async () => { + await client.createIndex(uid).waitTask(); + }); + + it("exits 2 and deletes nothing without --yes", async () => { + const result = await runBinary(["delete", "indexes", "--id", uid, "--output", "json"], env); + expect(result.exitCode).toBe(2); + expect(result.stdout).toBe(""); + expect(JSON.parse(result.stderr).error.code).toBe("cli_usage"); + await expect(client.index(uid).getRawInfo()).resolves.toMatchObject({ uid }); + }); + + it("deletes the index", async () => { + const result = await runBinary( + ["delete", "indexes", "--id", uid, "--yes", "--wait", "--output", "json"], + env, + ); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + indexUid: uid, + type: "indexDeletion", + status: "succeeded", + }); + await expect(client.index(uid).getRawInfo()).rejects.toThrow(); + }); +}); diff --git a/test/integration/meilisearch/update-indexes.test.ts b/test/integration/meilisearch/update-indexes.test.ts new file mode 100644 index 0000000..6ddbd77 --- /dev/null +++ b/test/integration/meilisearch/update-indexes.test.ts @@ -0,0 +1,69 @@ +import { randomUUID } from "node:crypto"; +import { Meilisearch } from "meilisearch"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { runBinary } from "../../helpers/run-binary.js"; +import { MASTER_KEY, MEILISEARCH_URL, meilisearchAvailable } from "./setup.js"; + +const meilisearchUp = await meilisearchAvailable(); + +const env = { MEILI_PROJECT_URL: MEILISEARCH_URL, MEILI_PROJECT_API_KEY: MASTER_KEY }; +const uid = `meili-cli-test-update-indexes-${randomUUID().slice(0, 8)}`; +const renamed = `${uid}-renamed`; + +describe.skipIf(!meilisearchUp)("meili update indexes against a real Meilisearch", () => { + const client = new Meilisearch({ host: MEILISEARCH_URL, apiKey: MASTER_KEY }); + + beforeAll(async () => { + await client.createIndex(uid).waitTask(); + }); + + afterAll(async () => { + await client.deleteIndex(renamed).waitTask(); + }); + + it("sets the primary key of the index", async () => { + const result = await runBinary( + ["update", "indexes", "--id", uid, "--primary-key", "id", "--wait", "--output", "json"], + env, + ); + expect(result.exitCode).toBe(0); + expect(JSON.parse(result.stdout)).toMatchObject({ + indexUid: uid, + type: "indexUpdate", + status: "succeeded", + }); + expect(await client.index(uid).getRawInfo()).toMatchObject({ primaryKey: "id" }); + }); + + it("renames the index with --new-id", async () => { + const result = await runBinary( + ["update", "indexes", "--id", uid, "--new-id", renamed, "--wait", "--output", "json"], + env, + ); + expect(result.exitCode).toBe(0); + expect(await client.index(renamed).getRawInfo()).toMatchObject({ uid: renamed }); + }); + + it("prints the failed task and exits 7 when the index does not exist", async () => { + const result = await runBinary( + [ + "update", + "indexes", + "--id", + `${uid}-missing`, + "--primary-key", + "id", + "--wait", + "--output", + "json", + ], + env, + ); + expect(result.exitCode).toBe(7); + expect(JSON.parse(result.stdout)).toMatchObject({ + status: "failed", + error: { code: "index_not_found" }, + }); + expect(JSON.parse(result.stderr).error.code).toBe("cli_task_failed"); + }); +}); diff --git a/test/lib/input.test.ts b/test/lib/input.test.ts index bde1783..98b37f8 100644 --- a/test/lib/input.test.ts +++ b/test/lib/input.test.ts @@ -7,7 +7,7 @@ import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it, vi } from "vitest"; import type { MeiliCliError } from "../../src/lib/errors.js"; -import { isJsonObject, readJsonBody } from "../../src/lib/input.js"; +import { isJsonObject, readJsonBody, readJsonObjectBody } from "../../src/lib/input.js"; vi.mock("node:fs", async (importOriginal) => { const actual = await importOriginal(); @@ -57,6 +57,25 @@ describe("readJsonBody", () => { }); }); +describe("readJsonObjectBody", () => { + it("returns an empty object when neither --file nor --data is given", () => { + expect(readJsonObjectBody({})).toEqual({}); + }); + + it("returns the object of --data and of --file", () => { + expect(readJsonObjectBody({ data: '{"primaryKey":"id"}' })).toEqual({ primaryKey: "id" }); + expect(readJsonObjectBody({ file })).toEqual({ from: "file" }); + }); + + it("rejects a body that is not an object with cli_input_invalid", () => { + for (const flags of [{ data: "42" }, { data: '["movies"]' }, { data: "null" }]) { + expect(() => readJsonObjectBody(flags), JSON.stringify(flags)).toThrow( + expect.objectContaining({ code: "cli_input_invalid" }) as MeiliCliError, + ); + } + }); +}); + describe("isJsonObject", () => { it("accepts an object and nothing else", () => { expect(isJsonObject({ filter: "a = b" })).toBe(true); From 00a8b4c88559f5167d9f3743b64a071a9646421c Mon Sep 17 00:00:00 2001 From: curquiza Date: Sun, 25 Oct 2026 22:47:20 +0100 Subject: [PATCH 2/2] Regenerate the manifest from a clean dist, without the phantom create:documents `oclif manifest` reads `dist/`, and `tsc` never removes an output whose source is gone, so `dist/commands/create/documents.js` had survived the rename of the verb `create` into `add` on my machine. Regenerating on that dirty tree put a `create:documents` command back into the committed manifest and into the `meili schema` snapshot, for a file that does not exist: the CI step "The committed manifest must match the code" caught it, and CodeRabbit read the same entry. `rm -rf dist` then `pnpm gen:manifest`. The manifest now lists seventeen commands, every one of them a file in `src/commands/`, and the schema snapshot loses the five phantom examples it carried. Co-Authored-By: Claude Opus 5 (1M context) --- oclif.manifest.json | 234 ------------------ .../__snapshots__/commands.test.ts.snap | 99 -------- 2 files changed, 333 deletions(-) diff --git a/oclif.manifest.json b/oclif.manifest.json index fc6147b..b4d405f 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -1475,240 +1475,6 @@ "unset.js" ] }, - "create:documents": { - "aliases": [], - "args": {}, - "description": "Add documents to an index, replacing the ones that already have the same primary key.", - "examples": [ - "<%= config.bin %> <%= command.id %> --index movies --file movies.json", - "<%= config.bin %> <%= command.id %> --index movies --file movies.json --output json", - "cat movies.json | <%= config.bin %> <%= command.id %> --index movies --file -", - "<%= config.bin %> <%= command.id %> --index movies --data '[{\"id\":1,\"title\":\"Nosferatu\"}]' --primary-key id", - "<%= config.bin %> <%= command.id %> --index movies --file movies.json --wait" - ], - "flags": { - "output": { - "char": "o", - "description": "json (default): one document, the API body as it is, indented in a terminal, compact when piped. ndjson: one item per line, list commands only, streams with --all.", - "name": "output", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "options": [ - "json", - "ndjson" - ], - "type": "option" - }, - "debug": { - "description": "Print every HTTP request and response on stderr, secrets redacted.", - "name": "debug", - "allowNo": false, - "type": "boolean" - }, - "project-url": { - "description": "Project URL, the Meilisearch instance to talk to.", - "name": "project-url", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "project-api-key": { - "description": "Project API key. `-` reads it from stdin.", - "name": "project-api-key", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "project": { - "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", - "name": "project", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "index": { - "description": "The index to add the documents to.", - "name": "index", - "required": true, - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "primary-key": { - "description": "Primary key of the index. Only taken into account on its first documents.", - "name": "primary-key", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "custom-metadata": { - "description": "String attached to the task, returned in the task object.", - "name": "custom-metadata", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "skip-creation": { - "description": "Only update documents that already exist, ignore the new ones.", - "name": "skip-creation", - "allowNo": false, - "type": "boolean" - }, - "file": { - "description": "Read the body from a file. `-` reads from stdin.", - "exclusive": [ - "data" - ], - "name": "file", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "data": { - "description": "Inline JSON body.", - "name": "data", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "wait": { - "description": "Poll the task until it reaches a final status, then print the final task.", - "name": "wait", - "allowNo": false, - "type": "boolean" - }, - "timeout": { - "dependsOn": [ - "wait" - ], - "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", - "name": "timeout", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - } - }, - "hasDynamicHelp": false, - "hiddenAliases": [], - "id": "create:documents", - "pluginAlias": "@meilisearch/cli", - "pluginName": "@meilisearch/cli", - "pluginType": "core", - "strict": true, - "enableJsonFlag": false, - "api": "meilisearch", - "routes": [ - "POST /indexes/{index_uid}/documents" - ], - "keyActions": [ - "documents.add" - ], - "connectionFlags": { - "project-url": { - "description": "Project URL, the Meilisearch instance to talk to.", - "helpValue": "", - "input": [], - "multiple": false, - "type": "option" - }, - "project-api-key": { - "description": "Project API key. `-` reads it from stdin.", - "helpValue": "", - "input": [], - "multiple": false, - "type": "option" - }, - "project": { - "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", - "helpValue": "", - "input": [], - "multiple": false, - "type": "option" - } - }, - "waitFlags": { - "wait": { - "description": "Poll the task until it reaches a final status, then print the final task.", - "allowNo": false, - "type": "boolean" - }, - "timeout": { - "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", - "helpValue": "", - "dependsOn": [ - "wait" - ], - "input": [], - "multiple": false, - "type": "option" - } - }, - "paginationFlags": { - "limit": { - "description": "Passed to the API.", - "helpValue": "", - "input": [], - "multiple": false, - "type": "option" - }, - "offset": { - "description": "Passed to the API.", - "helpValue": "", - "input": [], - "multiple": false, - "type": "option" - }, - "all": { - "description": "Fetch every page. Streams with ndjson, buffers into one array with json.", - "allowNo": false, - "type": "boolean" - } - }, - "bodyInputFlags": { - "file": { - "description": "Read the body from a file. `-` reads from stdin.", - "helpValue": "", - "exclusive": [ - "data" - ], - "input": [], - "multiple": false, - "type": "option" - }, - "data": { - "description": "Inline JSON body.", - "helpValue": "", - "input": [], - "multiple": false, - "type": "option" - } - }, - "confirmFlags": { - "yes": { - "required": true, - "description": "Confirm the deletion. Nothing is sent to the API without it.", - "allowNo": false, - "type": "boolean" - } - }, - "isESM": true, - "relativePath": [ - "dist", - "commands", - "create", - "documents.js" - ] - }, "delete:documents": { "aliases": [], "args": {}, diff --git a/test/contract/__snapshots__/commands.test.ts.snap b/test/contract/__snapshots__/commands.test.ts.snap index 529c08a..b205df4 100644 --- a/test/contract/__snapshots__/commands.test.ts.snap +++ b/test/contract/__snapshots__/commands.test.ts.snap @@ -304,98 +304,6 @@ The file holds API keys and decides where every later command goes, for every pr ], "id": "config unset", }, - { - "api": "meilisearch", - "async": true, - "description": "Add documents to an index, replacing the ones that already have the same primary key.", - "examples": [ - "meili create documents --index movies --file movies.json", - "meili create documents --index movies --file movies.json --output json", - "cat movies.json | meili create documents --index movies --file -", - "meili create documents --index movies --data '[{"id":1,"title":"Nosferatu"}]' --primary-key id", - "meili create documents --index movies --file movies.json --wait", - ], - "flags": [ - { - "description": "Project URL, the Meilisearch instance to talk to.", - "helpValue": "", - "name": "project-url", - "type": "option", - }, - { - "description": "Project API key. \`-\` reads it from stdin.", - "helpValue": "", - "name": "project-api-key", - "type": "option", - }, - { - "description": "A project of the config file. Cannot be combined with --project-url. Required when the file has several projects.", - "helpValue": "", - "name": "project", - "type": "option", - }, - { - "description": "The index to add the documents to.", - "helpValue": "", - "name": "index", - "required": true, - "type": "option", - }, - { - "description": "Primary key of the index. Only taken into account on its first documents.", - "helpValue": "", - "name": "primary-key", - "type": "option", - }, - { - "description": "String attached to the task, returned in the task object.", - "helpValue": "", - "name": "custom-metadata", - "type": "option", - }, - { - "description": "Only update documents that already exist, ignore the new ones.", - "name": "skip-creation", - "type": "boolean", - }, - { - "description": "Read the body from a file. \`-\` reads from stdin.", - "exclusive": [ - "data", - ], - "helpValue": "", - "name": "file", - "type": "option", - }, - { - "description": "Inline JSON body.", - "helpValue": "", - "name": "data", - "type": "option", - }, - { - "description": "Poll the task until it reaches a final status, then print the final task.", - "name": "wait", - "type": "boolean", - }, - { - "dependsOn": [ - "wait", - ], - "description": "Maximum wait time, for example 30s, 2m. Default 60s. Only with --wait.", - "helpValue": "", - "name": "timeout", - "type": "option", - }, - ], - "id": "create documents", - "keyActions": [ - "documents.add", - ], - "routes": [ - "POST /indexes/{index_uid}/documents", - ], - }, { "api": "meilisearch", "async": true, @@ -1023,13 +931,6 @@ The file holds API keys and decides where every later command goes, for every pr "description": "Remove a project from the config file.", "id": "config unset", }, - { - "description": "Add documents to an index, replacing the ones that already have the same primary key.", - "id": "create documents", - "routes": [ - "POST /indexes/{index_uid}/documents", - ], - }, { "description": "Delete the documents of an index named by --ids, matched by --filter, or every one of them with --all.", "id": "delete documents",