From 6d8f47cf61bd3fb709f0589ecdbeaa09cc38742e Mon Sep 17 00:00:00 2001 From: curquiza Date: Fri, 23 Oct 2026 17:44:42 +0200 Subject: [PATCH] The verb of POST is `add`, not `create` `meili create documents` reads wrong: the API itself calls that route `documents.add`. `add` reads as well as `create` on every other resource, it is three letters, and it is the word an agent reaches for first after `get`. The rename goes through the derivation rather than around it: `METHOD_VERBS.POST` in `src/lib/api-naming.ts` is the single source, so every POST route to come is named `add indexes`, `add keys`, `add teams` without touching anything again. The ADRs are rewritten as if the decision had been taken on day one, rather than superseded: nothing is released yet and the history of a word nobody ever typed is not worth keeping. ADR 0003 carries the why. Co-Authored-By: Claude Opus 5 (1M context) --- .changeset/first-version.md | 2 +- .claude/skills/add-command/SKILL.md | 6 +- CLAUDE.md | 2 +- README.md | 2 +- docs/adr/0003-commands-derive-from-openapi.md | 5 +- ...two-apis-one-credential-set-per-command.md | 2 +- docs/adr/0007-delete-commands-require-yes.md | 2 +- docs/contract/commands.md | 14 +- oclif.manifest.json | 394 +++++++++--------- package.json | 4 +- src/commands/{create => add}/documents.ts | 4 +- src/lib/api-naming.ts | 2 +- .../__snapshots__/documents.test.ts.snap | 18 +- .../{create => add}/documents.test.ts | 24 +- .../__snapshots__/commands.test.ts.snap | 198 ++++----- test/contract/api.test.ts | 2 +- ...ocuments.test.ts => add-documents.test.ts} | 12 +- test/lib/api-naming.test.ts | 12 +- 18 files changed, 353 insertions(+), 352 deletions(-) rename src/commands/{create => add}/documents.ts (95%) rename test/commands/{create => add}/__snapshots__/documents.test.ts.snap (78%) rename test/commands/{create => add}/documents.test.ts (89%) rename test/integration/meilisearch/{create-documents.test.ts => add-documents.test.ts} (88%) diff --git a/.changeset/first-version.md b/.changeset/first-version.md index 94bfe06..e06144f 100644 --- a/.changeset/first-version.md +++ b/.changeset/first-version.md @@ -5,7 +5,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 create 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`. +- 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`. - `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 add` and `config remove` manage the file. diff --git a/.claude/skills/add-command/SKILL.md b/.claude/skills/add-command/SKILL.md index 2df8dfd..67691cb 100644 --- a/.claude/skills/add-command/SKILL.md +++ b/.claude/skills/add-command/SKILL.md @@ -8,7 +8,7 @@ argument-hint: You ship exactly one command in this change: `meili $ARGUMENTS`, with every route of its verb and resource. One file for the command, one test file, one integration test, the regenerated manifest, one changeset line. Nothing else, unless a shared piece of `src/lib/` is missing for this kind of command (say so in the report). -When `$ARGUMENTS` names several commands of the same resource, `get create update delete documents`, rule 10 of `CLAUDE.md` allows them in one change: run every step below once per command, and ship them complete or not at all. Commands of different resources stay in different changes. +When `$ARGUMENTS` names several commands of the same resource, `get add update delete documents`, rule 10 of `CLAUDE.md` allows them in one change: run every step below once per command, and ship them complete or not at all. Commands of different resources stay in different changes. Work through the steps in order. Every check below is enforced by `pnpm test` through `test/contract/`, so a shortcut only moves the failure later. @@ -50,7 +50,7 @@ The command id is the file path. `meili get stats` gives: | integration test | `test/integration/meilisearch/get-stats.test.ts` | | snapshot (generated) | `test/commands/get/__snapshots__/stats.test.ts.snap` | -The class name is the command id in PascalCase (`GetStats`). The first command of a new verb (`create`, `update`, `delete`, `cancel`, ...) also needs the topic description in `package.json`, under `oclif.topics..description`, otherwise `meili --help` does not list the verb. A verb-only command (`search`, `export`) is a file at the root of `src/commands/`. +The class name is the command id in PascalCase (`GetStats`). The first command of a new verb (`add`, `update`, `delete`, `cancel`, ...) also needs the topic description in `package.json`, under `oclif.topics..description`, otherwise `meili --help` does not list the verb. A verb-only command (`search`, `export`) is a file at the root of `src/commands/`. Find the SDK method in `node_modules/meilisearch/dist/meilisearch.d.ts` or `node_modules/meilisearch/dist/indexes.d.ts`. Prefer the variant that returns the API body unchanged: `getRawIndexes` over `getIndexes`, `getRawIndex` over `getIndex`. If the SDK has no method for a route, stop and ask: ADR 0003 forbids hand-written HTTP. @@ -60,7 +60,7 @@ Find the SDK method in `node_modules/meilisearch/dist/meilisearch.d.ts` or `node |---|---|---| | read, one route, one result | `src/commands/get/health.ts` | `await this.output(result)` | | read with a list route and a unit route | `src/commands/get/indexes.ts` | `outputList({...})` or `output(...)` by route | -| write returning a task (the response is a task) | `src/commands/create/documents.ts` | `await this.outputTask(task, flags)` | +| write returning a task (the response is a task) | `src/commands/add/documents.ts` | `await this.outputTask(task, flags)` | | write with several routes, one per selector | `src/commands/delete/documents.ts` | `await this.outputTask(task, flags)` | | offline (`schema`) | `src/commands/schema.ts` | `await this.output(result)` | diff --git a/CLAUDE.md b/CLAUDE.md index ea0387a..5bf85f6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,7 +24,7 @@ Package: `@meilisearch/cli` on npm. Binary: `meili`. 7. Errors are structured. Use `MeiliCliError` from `src/lib/errors.ts` with a code and an exit code from the table in `docs/contract/output.md`. Never `throw new Error("...")`. 8. Secrets never appear in output, logs or debug traces. Use the redaction helpers in `src/lib/config.ts`. 9. No new dependency without a one line justification in the pull request. Prefer the standard library. -10. One command per pull request, or the commands of one resource when they ship together and complete: the `get`, `create`, `update` and `delete` of `documents` are one change. One file per command, covering every route of its verb and resource. Every command has a snapshot test. +10. One command per pull request, or the commands of one resource when they ship together and complete: the `get`, `add`, `update` and `delete` of `documents` are one change. One file per command, covering every route of its verb and resource. Every command has a snapshot test. 11. Every command declares the API it uses with `static api = "meilisearch" | "management" | "none"`. `BaseCommand` resolves credentials and builds the client. Never resolve credentials or build a client in a command. 12. Code and docs change together. If your change touches a command name, a flag, an environment variable, an error code, an exit code or a file path that appears in `docs/`, update that doc in the same pull request. The contract tests in `test/contract/` fail on most of these drifts, so a red `pnpm test` after a docs-free change is expected, not a flaky test. 13. Never edit an accepted ADR to match the code. If your change contradicts an ADR, stop and ask, or write a new ADR that supersedes the old one and mark the old one `superseded by ADR NNNN`. The history of decisions stays readable. diff --git a/README.md b/README.md index 3db8831..7901ba1 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ export MEILI_PROJECT_API_KEY=masterKey # only if the instance has a master key meili get health ``` -**Management API**, for the commands that manage Meilisearch Cloud, `meili get projects` or `meili create teams` (not shipped yet): +**Management API**, for the commands that manage Meilisearch Cloud, `meili get projects` or `meili add teams` (not shipped yet): ```sh export MEILI_MANAGEMENT_TOKEN=... # a personal access token from the Meilisearch Cloud dashboard diff --git a/docs/adr/0003-commands-derive-from-openapi.md b/docs/adr/0003-commands-derive-from-openapi.md index 9b28aae..27cf4f8 100644 --- a/docs/adr/0003-commands-derive-from-openapi.md +++ b/docs/adr/0003-commands-derive-from-openapi.md @@ -13,7 +13,8 @@ The grammar matters as much as the derivation. Coding agents have a strong prior ## Decision - The OpenAPI documents are the reference for what a command is named and what it takes. `docs/contract/commands.md` states the naming rules, not the commands. `src/lib/api-naming.ts` implements the rules and `test/contract/api.test.ts` enforces them. -- The grammar is `meili [flags]`. The verb comes from the HTTP method, `get`, `create`, `update`, `delete`, or from a trailing literal of the route taken from a closed list, `cancel`, `compact`, `swap`. The resource is the last literal segment of the path, plural. The most typed commands, `search`, `similar`, `facet-search`, `multi-search`, `export`, carry no resource word, as recorded exceptions. Every value is a named flag: no positional argument, so an agent names what it passes and never has to know an order. +- The grammar is `meili [flags]`. The verb comes from the HTTP method, `get`, `add`, `update`, `delete`, or from a trailing literal of the route taken from a closed list, `cancel`, `compact`, `swap`. The resource is the last literal segment of the path, plural. The most typed commands, `search`, `similar`, `facet-search`, `multi-search`, `export`, carry no resource word, as recorded exceptions. Every value is a named flag: no positional argument, so an agent names what it passes and never has to know an order. +- `POST` is `add`, not `create`. Both words read well on `indexes` and on `keys`; only one of them reads well on `documents`, the most typed write command, which the API itself calls `documents.add`. `add` is also three letters and the word an agent reaches for first after `get`. The price is that the CLI's verb no longer echoes the `indexes.create` and `keys.create` key actions of the other resources: a command declares its key actions in `static keyActions`, read from the document, so no name is derived from the other. - One command covers every route that shares its verb and resource, and the flags select the route: `get indexes` lists or, with `--id`, gets one; `delete documents` deletes by `--ids`, by `--filter` or everything with `--all`. Flags that call for two routes at once are a usage error. The command declares its routes, `static routes`, and the contract test checks that each is reachable by a distinct set of flags. - Identifiers: the resource's own identifier is `--id`, or `--ids` when the list route filters by identifiers, in which case the unit route is not implemented and `--id` is an alias. A parent in the path is a flag named after its collection, `--index`. A query or body parameter that carries identifiers is named after the resource it identifies, `--ids` for the command's own resource, `--batch-ids` or `--task-ids` for another, whatever the API spelling, which stays accepted as an alias. Every other body field keeps its API name. - The documents are downloaded, never committed. `pnpm openapi` fetches the Meilisearch API document from the latest release, or from the release in `MEILISEARCH_VERSION`, and the management document with a GitHub token that reads the private repository (`gh auth login` on a workstation, `MEILI_BOT_GH_PAT` in CI). A missing document fails the contract tests: nobody writes a command without seeing the route. @@ -28,7 +29,7 @@ The grammar matters as much as the derivation. Coding agents have a strong prior - Adding a command is one pull request: the command with all its routes, its tests, the manifest, a changeset, and an exception only if the rules give a wrong name. Fifty-odd commands cover the ninety-odd stable routes of the two APIs. - `meili get --help` lists every resource on one screen. The agent learns one verb set and one flag convention, not a tree. - The command tree follows the APIs. `pnpm api:coverage` prints every route with its command name and whether it exists. -- Some derived names read oddly to a human: `create indexes --id movies` creates one index, `create dumps` creates one dump. One spelling, always plural, beats two spellings an agent has to choose between. On a typo, the `cli_usage` error carries a hint with the closest existing command, "Did you mean `meili delete indexes`?", computed from the manifest; the CLI suggests, it never asks. +- Some derived names read oddly to a human: `add indexes --id movies` adds one index, `add dumps` takes one dump. One spelling, always plural, beats two spellings an agent has to choose between. On a typo, the `cli_usage` error carries a hint with the closest existing command, "Did you mean `meili delete indexes`?", computed from the manifest; the CLI suggests, it never asks. - A command that filters a list instead of calling a unit route, `get tasks --ids 42`, returns the list envelope and the agent reads `results[0]`. In exchange the agent never has to know whether a route exists for one item. - `BaseCommand` owns a small dispatcher: which route the given flags select, which flags conflict, which are ignored. Commands stay declarative: routes, flags, one call. - Types come from the SDK and the generator, so `--help` descriptions and output shapes cannot drift from the API without a compile error. An undocumented parameter is a documentation bug to fix upstream before the flag exists in the CLI. diff --git a/docs/adr/0004-two-apis-one-credential-set-per-command.md b/docs/adr/0004-two-apis-one-credential-set-per-command.md index 3966dd3..02d68a6 100644 --- a/docs/adr/0004-two-apis-one-credential-set-per-command.md +++ b/docs/adr/0004-two-apis-one-credential-set-per-command.md @@ -17,7 +17,7 @@ A command needs exactly one of the two. If the CLI resolved both on every call, - `BaseCommand` owns credential resolution and client construction. It resolves only the declared API, builds only that client, and produces errors that name only that API's sources. - The resolution order is the same for both APIs and has two levels: the flag, then the environment variable. A flag and its variable share their name, `MEILI_` plus the flag name in upper snake case (CLAUDE.md, rule 14). The sources are documented in `docs/contract/config.md`. - Connections come from flags and environment variables. A config file is not decided; if one comes, it comes with its own ADR. -- Management commands carry no prefix. The resource says which API a command talks to, `get projects`, `create teams`, and `static api` says which credentials it needs. No resource name collides between the two documents today; a collision would be an exception in `commands.md`. +- Management commands carry no prefix. The resource says which API a command talks to, `get projects`, `add teams`, and `static api` says which credentials it needs. No resource name collides between the two documents today; a collision would be an exception in `commands.md`. ## Consequences diff --git a/docs/adr/0007-delete-commands-require-yes.md b/docs/adr/0007-delete-commands-require-yes.md index 7b26ba8..3afacfa 100644 --- a/docs/adr/0007-delete-commands-require-yes.md +++ b/docs/adr/0007-delete-commands-require-yes.md @@ -33,7 +33,7 @@ Destructive, in `output.md`, names the verb `delete` and nothing else: what the ## Consequences - A `delete` command without `--yes` fails like any command missing a required flag: `cli_usage`, exit 2, before the credentials are read and before any network call. No new error code, nothing new for an agent to parse, and `--help` and `meili schema` show `--yes` as required. -- The rule about verbs lives in the contract test, not in the binary: `test/contract/commands.test.ts` checks that every command under `src/commands/delete/` declares the flag and that no other command does. A `delete` command that forgets the group, or a `create` command that invents a confirmation of its own, turns the suite red before review. What the binary knows is narrower and enough: a required flag that oclif refuses to do without. Keying the refusal on the command's verb at runtime was considered and dropped: the verb comes from `this.id`, which oclif fills from the manifest, so it is empty when a test runs a command class directly, and the rule would then hold in the binary but not in the tests. +- The rule about verbs lives in the contract test, not in the binary: `test/contract/commands.test.ts` checks that every command under `src/commands/delete/` declares the flag and that no other command does. A `delete` command that forgets the group, or an `add` command that invents a confirmation of its own, turns the suite red before review. What the binary knows is narrower and enough: a required flag that oclif refuses to do without. Keying the refusal on the command's verb at runtime was considered and dropped: the verb comes from `this.id`, which oclif fills from the manifest, so it is empty when a test runs a command class directly, and the rule would then hold in the binary but not in the tests. - The selector rule is unchanged: `delete documents` still refuses to run without `--ids`, `--filter` or `--all`. - Every command line that deletes, in the documentation, in the examples and in the tests, carries `--yes`. - This is a breaking change for the `delete` commands: a major version once the CLI is released, a minor one before 1.0. diff --git a/docs/contract/commands.md b/docs/contract/commands.md index 35e729e..75ae4f8 100644 --- a/docs/contract/commands.md +++ b/docs/contract/commands.md @@ -19,14 +19,14 @@ When the document does not describe what a command needs (the route, a parameter Every API command declares its routes, `static routes = ["GET /indexes", "GET /indexes/{index_uid}"]`, written as in the document, and the key actions those routes require, `static keyActions = ["indexes.get"]`, the specific action of each `security` entry of the document, empty when the routes need none. `meili schema --command` prints both, and `test/contract/api.test.ts` checks both against the document. The rules below turn routes into a command name and its flags. `src/lib/api-naming.ts` implements them, `test/contract/api.test.ts` enforces them. -1. **Verb.** The HTTP method gives it: `GET` is `get`, `POST` is `create`, `PUT` and `PATCH` are `update`, `DELETE` is `delete`. A trailing literal segment that is an action replaces it: `POST /tasks/cancel` gives `cancel`, `POST /indexes/{index_uid}/compact` gives `compact`, `POST /indexes/{index_uid}/documents/delete` gives `delete`. The literal verbs are a closed list: `delete`, `cancel`, `compact`, `swap`, `search`, `similar`, `facet-search`, `multi-search`, `export`. Adding one is a contract change. +1. **Verb.** The HTTP method gives it: `GET` is `get`, `POST` is `add`, `PUT` and `PATCH` are `update`, `DELETE` is `delete`. A trailing literal segment that is an action replaces it: `POST /tasks/cancel` gives `cancel`, `POST /indexes/{index_uid}/compact` gives `compact`, `POST /indexes/{index_uid}/documents/delete` gives `delete`. The literal verbs are a closed list: `delete`, `cancel`, `compact`, `swap`, `search`, `similar`, `facet-search`, `multi-search`, `export`. Adding one is a contract change. 2. **Resource.** The last literal segment of the path that names a resource, plural, in kebab-case of the API name, without the `/api/v2` prefix of the Management API: `indexes`, `documents`, `team-members`, `stats`, `health`. A singleton keeps its name: `get health`, `get version`, `get stats`. Five commands have no resource word, because they are the most typed: `search`, `similar`, `facet-search`, `multi-search`, `export`. They are listed in the exceptions. 3. **Several routes, one command.** Every route with the same verb and resource belongs to one command: `GET /indexes` and `GET /indexes/{index_uid}` are `get indexes`, `GET /stats` and `GET /indexes/{index_uid}/stats` are `get stats`, the three deletion routes of documents are `delete documents`. The flags choose the route: first the route whose path parameters are exactly the identifier flags given, then the route whose parameters cover the other flags given. Two flags that call for different routes, `--ids` and `--filter` on `delete documents` for example, are a usage error, `cli_usage`, exit 2, before any network call. A flag that belongs to the list route only, `--limit` or `--offset`, is ignored and not sent when `--id` selects the unit route. 4. **Identifiers.** The path parameter of the resource itself is `--id` when the unit route is implemented: `get indexes --id movies`, `delete keys --id `, `get projects --id 343`. When the list route can filter by identifiers, the unit route is not implemented and the list takes `--ids`, with `--id` accepted as an alias: `get documents --index movies --ids 42` calls the fetch route, `get tasks --ids 42` filters `GET /tasks`, `get batches --ids 7` filters `GET /batches`. The path parameter of a parent resource is a flag named after that collection, singular: `--index movies`. The verb-only commands take the index the same way: `search --index movies`. -5. **Identifiers are named by what they identify.** A query or body parameter that carries identifiers, `uid`, `uids`, `batchUids`, `indexUids`, is named after the resource it identifies: the command's own resource gives `--id` or `--ids`, another resource gives `---ids`. So `create indexes --id movies` and `create keys --id ` carry `uid`; `get tasks --ids 1,2 --batch-ids 7 --index-ids movies` carries `uids`, `batchUids` and `indexUids`; `get batches --ids 7 --task-ids 1,2` carries `batchUids` and `uids`, because bare `uids` are task identifiers everywhere. The API spellings, `--uid`, `--uids`, `--batch-uids`, `--index-uids`, are accepted as aliases, absent from `--help` and `meili schema`. One exception: on `update indexes`, `--id` is the index itself, so the new `uid` is `--new-id`, alias `--uid`. Every other body field keeps its API name, `--primary-key`, `--name`. +5. **Identifiers are named by what they identify.** A query or body parameter that carries identifiers, `uid`, `uids`, `batchUids`, `indexUids`, is named after the resource it identifies: the command's own resource gives `--id` or `--ids`, another resource gives `---ids`. So `add indexes --id movies` and `add keys --id ` carry `uid`; `get tasks --ids 1,2 --batch-ids 7 --index-ids movies` carries `uids`, `batchUids` and `indexUids`; `get batches --ids 7 --task-ids 1,2` carries `batchUids` and `uids`, because bare `uids` are task identifiers everywhere. The API spellings, `--uid`, `--uids`, `--batch-uids`, `--index-uids`, are accepted as aliases, absent from `--help` and `meili schema`. One exception: on `update indexes`, `--id` is the index itself, so the new `uid` is `--new-id`, alias `--uid`. Every other body field keeps its API name, `--primary-key`, `--name`. 6. **Other flags.** The query parameters and the first-level fields of the JSON body, in kebab-case of the API name: `retrieveVectors` is `--retrieve-vectors`, `teamId` is `--team-id`, `primaryKey` is `--primary-key`. When a body has a single object property named after the resource, `{"project": {"name": …}}` in the Management API, the fields of that object are the flags. The only flags that do not come from a route are the global flags and the CLI-owned flags: `--wait`, `--timeout`, `--all`, `--file`, `--data`, `--yes`. 7. **Values.** A scalar is passed as it is. A boolean is a bare flag, `--retrieve-vectors`. An array of scalars is comma-separated, `--fields id,title`, as the API's own GET syntax. An object or a nested array is JSON text, `--typo-tolerance '{"enabled":false}'`. The value `null` sends a JSON null, `--expires-at null`, `--stop-words null`. -8. **Body input.** On every route with a body, `--file ` or `--data ''` replaces the body fields; `--file -` reads stdin. Path and query flags stay valid next to them. A body-field flag given together with `--file` or `--data` overrides that field, as a flag overrides its environment variable. When a route's body is not an object of named fields but the value itself, the array of identifiers of `delete documents --ids` or the documents of `create documents`, the body is that value, and its shape selects the route among the ones the command covers: `delete documents --data '[1,2]'` deletes by identifiers, `--data '{"filter":…}'` deletes by filter. Documents are sent as JSON, the only content type the document declares. +8. **Body input.** On every route with a body, `--file ` or `--data ''` replaces the body fields; `--file -` reads stdin. Path and query flags stay valid next to them. A body-field flag given together with `--file` or `--data` overrides that field, as a flag overrides its environment variable. When a route's body is not an object of named fields but the value itself, the array of identifiers of `delete documents --ids` or the documents of `add documents`, the body is that value, and its shape selects the route among the ones the command covers: `delete documents --data '[1,2]'` deletes by identifiers, `--data '{"filter":…}'` deletes by filter. Documents are sent as JSON, the only content type the document declares. 9. **Routes that are not implemented.** The `GET` twin of a `POST` route that takes the same parameters in a body (search, similar, the list of documents); the unit route of a resource whose list filters by identifiers (documents, tasks, batches); the settings sub-routes, since `update settings` sends any subset and `null` resets one setting; `GET /api/v2/meilisearch-versions`. Each is recorded without command in the exceptions. 10. **Experimental routes** are out of the contract until the feature is stable. Implementing one is a contract change: an exception row with the reason. `compact` is the one exception today. 11. **Destructive commands.** A command of the verb `delete` takes `--yes` and refuses to run without it: `cli_usage`, exit 2, before any network call. `--yes` is a flag to write, never a question the CLI asks, see `output.md`. @@ -36,13 +36,13 @@ Every API command declares its routes, `static routes = ["GET /indexes", "GET /i | `GET /health` | `meili get health` | | `GET /stats`, `GET /indexes/{index_uid}/stats` | `meili get stats [--index ]` | | `GET /indexes`, `GET /indexes/{index_uid}` | `meili get indexes [--id ]` | -| `POST /indexes` | `meili create indexes --id [--primary-key ]` | +| `POST /indexes` | `meili add indexes --id [--primary-key ]` | | `PATCH /indexes/{index_uid}` | `meili update indexes --id [--primary-key ] [--new-id ]` | | `DELETE /indexes/{index_uid}` | `meili delete indexes --id --yes` | | `POST /indexes/{index_uid}/compact` | `meili compact indexes --id ` | | `POST /swap-indexes` | `meili swap indexes --indexes ,` | | `POST /indexes/{index_uid}/documents/fetch` | `meili get documents --index [--ids ] [--filter ]` | -| `POST /indexes/{index_uid}/documents` | `meili create documents --index --file ` | +| `POST /indexes/{index_uid}/documents` | `meili add documents --index --file ` | | `PUT /indexes/{index_uid}/documents` | `meili update documents --index --file ` | | `POST …/documents/delete-batch`, `POST …/documents/delete`, `DELETE …/documents` | `meili delete documents --index (--ids \| --filter \| --all) --yes` | | `POST /indexes/{index_uid}/search` | `meili search --index --q ` | @@ -50,9 +50,9 @@ Every API command declares its routes, `static routes = ["GET /indexes", "GET /i | `GET /tasks` | `meili get tasks [--ids ] [--statuses ]` | | `POST /tasks/cancel` | `meili cancel tasks --ids ` | | `GET /keys`, `GET /keys/{key}` | `meili get keys [--id ]` | -| `POST /dumps` | `meili create dumps` | +| `POST /dumps` | `meili add dumps` | | `GET /api/v2/projects`, `GET /api/v2/projects/{id}` | `meili get projects [--id ] [--team-id ]` | -| `POST /api/v2/projects` | `meili create projects --name --team-id --resource-id ` | +| `POST /api/v2/projects` | `meili add projects --name --team-id --resource-id ` | | `GET /api/v2/team-members` | `meili get team-members --team-id ` | ## Exceptions diff --git a/oclif.manifest.json b/oclif.manifest.json index 817a7c8..9f6fa59 100644 --- a/oclif.manifest.json +++ b/oclif.manifest.json @@ -1,13 +1,15 @@ { "commands": { - "auth:login": { + "add:documents": { "aliases": [], "args": {}, - "description": "Log in to Meilisearch Cloud with a browser, and store the token for the management commands.", + "description": "Add documents to an index, replacing the ones that already have the same primary key.", "examples": [ - "<%= config.bin %> <%= command.id %>", - "<%= config.bin %> <%= command.id %> --output json", - "<%= config.bin %> <%= command.id %> --no-browser" + "<%= 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": { @@ -29,48 +31,113 @@ "allowNo": false, "type": "boolean" }, - "management-client-id": { - "description": "The OAuth application to log in with, on a Cloud that is not Meilisearch Cloud. Also MEILI_MANAGEMENT_CLIENT_ID.", - "name": "management-client-id", + "project-url": { + "description": "Project URL, the Meilisearch instance to talk to.", + "name": "project-url", "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, - "management-url": { - "description": "Management API base URL, which holds the token endpoint.", - "name": "management-url", + "project-api-key": { + "description": "Project API key. `-` reads it from stdin.", + "name": "project-api-key", "hasDynamicHelp": false, - "helpValue": "", + "helpValue": "", "multiple": false, "type": "option" }, - "management-dashboard-url": { - "description": "Dashboard URL, which hosts the consent page.", - "name": "management-dashboard-url", + "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" }, - "browser": { - "description": "Open the consent page in a browser. --no-browser only prints the URL.", - "name": "browser", - "allowNo": true, + "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": "auth:login", + "id": "add:documents", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", "strict": true, "enableJsonFlag": false, - "api": "none", - "routes": [], - "keyActions": [], + "api": "meilisearch", + "routes": [ + "POST /indexes/{index_uid}/documents" + ], + "keyActions": [ + "documents.add" + ], "connectionFlags": { "project-url": { "description": "Project URL, the Meilisearch instance to talk to.", @@ -163,17 +230,18 @@ "relativePath": [ "dist", "commands", - "auth", - "login.js" + "add", + "documents.js" ] }, - "auth:logout": { + "auth:login": { "aliases": [], "args": {}, - "description": "Revoke the token of `auth login` and forget it. Does nothing when nobody is logged in.", + "description": "Log in to Meilisearch Cloud with a browser, and store the token for the management commands.", "examples": [ "<%= config.bin %> <%= command.id %>", - "<%= config.bin %> <%= command.id %> --output json" + "<%= config.bin %> <%= command.id %> --output json", + "<%= config.bin %> <%= command.id %> --no-browser" ], "flags": { "output": { @@ -194,11 +262,41 @@ "name": "debug", "allowNo": false, "type": "boolean" + }, + "management-client-id": { + "description": "The OAuth application to log in with, on a Cloud that is not Meilisearch Cloud. Also MEILI_MANAGEMENT_CLIENT_ID.", + "name": "management-client-id", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "management-url": { + "description": "Management API base URL, which holds the token endpoint.", + "name": "management-url", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "management-dashboard-url": { + "description": "Dashboard URL, which hosts the consent page.", + "name": "management-dashboard-url", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "browser": { + "description": "Open the consent page in a browser. --no-browser only prints the URL.", + "name": "browser", + "allowNo": true, + "type": "boolean" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "auth:logout", + "id": "auth:login", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -300,13 +398,13 @@ "dist", "commands", "auth", - "logout.js" + "login.js" ] }, - "auth:refresh": { + "auth:logout": { "aliases": [], "args": {}, - "description": "Trade the refresh token of the stored session for a new access token, before it expires.", + "description": "Revoke the token of `auth login` and forget it. Does nothing when nobody is logged in.", "examples": [ "<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> --output json" @@ -334,7 +432,7 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "auth:refresh", + "id": "auth:logout", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -436,13 +534,13 @@ "dist", "commands", "auth", - "refresh.js" + "logout.js" ] }, - "auth:status": { + "auth:refresh": { "aliases": [], "args": {}, - "description": "Say whether a session of `auth login` exists and whether the authorization server still accepts it.", + "description": "Trade the refresh token of the stored session for a new access token, before it expires.", "examples": [ "<%= config.bin %> <%= command.id %>", "<%= config.bin %> <%= command.id %> --output json" @@ -470,7 +568,7 @@ }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "auth:status", + "id": "auth:refresh", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -572,17 +670,16 @@ "dist", "commands", "auth", - "status.js" + "refresh.js" ] }, - "config:add": { + "auth:status": { "aliases": [], "args": {}, - "description": "Add a project to the config file, creating it owner-only; refuses a name that exists.\n\nThe file holds API keys and decides where every later command goes, for every process that reads it: an agent runs this only when the human asked for it explicitly.", + "description": "Say whether a session of `auth login` exists and whether the authorization server still accepts it.", "examples": [ - "<%= config.bin %> <%= command.id %> --project local --project-url http://localhost:7700", - "<%= config.bin %> <%= command.id %> --project prod --project-url https://ms-abc.sfo.meilisearch.io --project-api-key - < key.txt", - "<%= config.bin %> <%= command.id %> --project local --project-url http://localhost:7700 --output json" + "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> --output json" ], "flags": { "output": { @@ -603,37 +700,11 @@ "name": "debug", "allowNo": false, "type": "boolean" - }, - "project": { - "description": "Name of the project to add.", - "name": "project", - "required": true, - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "project-url": { - "description": "URL of the project.", - "name": "project-url", - "required": true, - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" - }, - "project-api-key": { - "description": "API key of the project. `-` reads it from stdin, out of the shell history.", - "name": "project-api-key", - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "config:add", + "id": "auth:status", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -734,18 +805,18 @@ "relativePath": [ "dist", "commands", - "config", - "add.js" + "auth", + "status.js" ] }, - "config:list": { + "config:add": { "aliases": [], "args": {}, - "description": "Print the config file path and its projects as JSON, keys masked.", + "description": "Add a project to the config file, creating it owner-only; refuses a name that exists.\n\nThe file holds API keys and decides where every later command goes, for every process that reads it: an agent runs this only when the human asked for it explicitly.", "examples": [ - "<%= config.bin %> <%= command.id %>", - "<%= config.bin %> <%= command.id %> --output json", - "MEILI_CONFIG=./meili.json <%= config.bin %> <%= command.id %>" + "<%= config.bin %> <%= command.id %> --project local --project-url http://localhost:7700", + "<%= config.bin %> <%= command.id %> --project prod --project-url https://ms-abc.sfo.meilisearch.io --project-api-key - < key.txt", + "<%= config.bin %> <%= command.id %> --project local --project-url http://localhost:7700 --output json" ], "flags": { "output": { @@ -766,11 +837,37 @@ "name": "debug", "allowNo": false, "type": "boolean" + }, + "project": { + "description": "Name of the project to add.", + "name": "project", + "required": true, + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project-url": { + "description": "URL of the project.", + "name": "project-url", + "required": true, + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" + }, + "project-api-key": { + "description": "API key of the project. `-` reads it from stdin, out of the shell history.", + "name": "project-api-key", + "hasDynamicHelp": false, + "helpValue": "", + "multiple": false, + "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "config:list", + "id": "config:add", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -872,16 +969,17 @@ "dist", "commands", "config", - "list.js" + "add.js" ] }, - "config:remove": { + "config:list": { "aliases": [], "args": {}, - "description": "Remove a project from the config file.\n\nThe file holds API keys and decides where every later command goes, for every process that reads it: an agent runs this only when the human asked for it explicitly.", + "description": "Print the config file path and its projects as JSON, keys masked.", "examples": [ - "<%= config.bin %> <%= command.id %> --project prod", - "<%= config.bin %> <%= command.id %> --project prod --output json" + "<%= config.bin %> <%= command.id %>", + "<%= config.bin %> <%= command.id %> --output json", + "MEILI_CONFIG=./meili.json <%= config.bin %> <%= command.id %>" ], "flags": { "output": { @@ -902,20 +1000,11 @@ "name": "debug", "allowNo": false, "type": "boolean" - }, - "project": { - "description": "Name of the project to remove.", - "name": "project", - "required": true, - "hasDynamicHelp": false, - "helpValue": "", - "multiple": false, - "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "config:remove", + "id": "config:list", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", @@ -1017,19 +1106,16 @@ "dist", "commands", "config", - "remove.js" + "list.js" ] }, - "create:documents": { + "config:remove": { "aliases": [], "args": {}, - "description": "Add documents to an index, replacing the ones that already have the same primary key.", + "description": "Remove a project from the config file.\n\nThe file holds API keys and decides where every later command goes, for every process that reads it: an agent runs this only when the human asked for it explicitly.", "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" + "<%= config.bin %> <%= command.id %> --project prod", + "<%= config.bin %> <%= command.id %> --project prod --output json" ], "flags": { "output": { @@ -1051,113 +1137,27 @@ "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.", + "description": "Name of the project to remove.", "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": "", + "helpValue": "", "multiple": false, "type": "option" } }, "hasDynamicHelp": false, "hiddenAliases": [], - "id": "create:documents", + "id": "config:remove", "pluginAlias": "@meilisearch/cli", "pluginName": "@meilisearch/cli", "pluginType": "core", "strict": true, "enableJsonFlag": false, - "api": "meilisearch", - "routes": [ - "POST /indexes/{index_uid}/documents" - ], - "keyActions": [ - "documents.add" - ], + "api": "none", + "routes": [], + "keyActions": [], "connectionFlags": { "project-url": { "description": "Project URL, the Meilisearch instance to talk to.", @@ -1250,8 +1250,8 @@ "relativePath": [ "dist", "commands", - "create", - "documents.js" + "config", + "remove.js" ] }, "delete:documents": { diff --git a/package.json b/package.json index 74a2c13..4d062e1 100644 --- a/package.json +++ b/package.json @@ -46,8 +46,8 @@ "get": { "description": "Read a resource: the list, or one item with --id." }, - "create": { - "description": "Create a resource, or add to it. The Meilisearch API answers with a task: the command prints it and returns at once, and --wait polls it to a final status instead." + "add": { + "description": "Add a resource, or content to one that exists. The Meilisearch API answers with a task: the command prints it and returns at once, and --wait polls it to a final status instead." }, "update": { "description": "Change a resource, merging what is given into what is there. The Meilisearch API answers with a task: the command prints it and returns at once, and --wait polls it to a final status instead." diff --git a/src/commands/create/documents.ts b/src/commands/add/documents.ts similarity index 95% rename from src/commands/create/documents.ts rename to src/commands/add/documents.ts index 34a636f..2e22d96 100644 --- a/src/commands/create/documents.ts +++ b/src/commands/add/documents.ts @@ -4,7 +4,7 @@ import { type Api, BaseCommand } from "../../lib/base-command.js"; import { MeiliCliError } from "../../lib/errors.js"; import { readJsonBody } from "../../lib/input.js"; -export default class CreateDocuments extends BaseCommand { +export default class AddDocuments extends BaseCommand { static api: Api = "meilisearch"; static routes = ["POST /indexes/{index_uid}/documents"]; @@ -45,7 +45,7 @@ export default class CreateDocuments extends BaseCommand { }; async run(): Promise { - const { flags } = await this.parse(CreateDocuments); + const { flags } = await this.parse(AddDocuments); const documents = readJsonBody(flags); if (documents === undefined) { throw new MeiliCliError("cli_usage", "The documents to add are missing.", { diff --git a/src/lib/api-naming.ts b/src/lib/api-naming.ts index d483c97..7933f7e 100644 --- a/src/lib/api-naming.ts +++ b/src/lib/api-naming.ts @@ -32,7 +32,7 @@ export const LITERAL_VERBS: readonly string[] = [ const METHOD_VERBS: Record = { GET: "get", - POST: "create", + POST: "add", PUT: "update", PATCH: "update", DELETE: "delete", diff --git a/test/commands/create/__snapshots__/documents.test.ts.snap b/test/commands/add/__snapshots__/documents.test.ts.snap similarity index 78% rename from test/commands/create/__snapshots__/documents.test.ts.snap rename to test/commands/add/__snapshots__/documents.test.ts.snap index 08e7e64..349eee4 100644 --- a/test/commands/create/__snapshots__/documents.test.ts.snap +++ b/test/commands/add/__snapshots__/documents.test.ts.snap @@ -1,6 +1,6 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html -exports[`meili create documents > passes the 404 of an unknown index through and exits 4 1`] = ` +exports[`meili add documents > passes the 404 of an unknown index through and exits 4 1`] = ` { "exitCode": 4, "stderr": "{"error":{"code":"index_not_found","type":"invalid_request","message":"Index \`nope\` not found.","link":"https://docs.meilisearch.com/errors#index_not_found"}} @@ -9,11 +9,11 @@ exports[`meili create documents > passes the 404 of an unknown index through and } `; -exports[`meili create documents > prints its help without any network call 1`] = ` +exports[`meili add documents > prints its help without any network call 1`] = ` "Add documents to an index, replacing the ones that already have the same primary key. USAGE - $ meili create documents --index [-o ] [--debug] + $ meili add documents --index [-o ] [--debug] [--project-url ] [--project-api-key ] [--project ] [--primary-key ] [--custom-metadata ] [--skip-creation] [--file | --data ] [--timeout --wait] @@ -51,20 +51,20 @@ DESCRIPTION primary key. EXAMPLES - $ meili create documents --index movies --file movies.json + $ meili add documents --index movies --file movies.json - $ meili create documents --index movies --file movies.json --output json + $ meili add documents --index movies --file movies.json --output json - cat movies.json | meili create documents --index movies --file - + cat movies.json | meili add documents --index movies --file - - $ meili create documents --index movies --data '[{"id":1,"title":"Nosferatu"}]' --primary-key id + $ meili add documents --index movies --data '[{"id":1,"title":"Nosferatu"}]' --primary-key id - $ meili create documents --index movies --file movies.json --wait + $ meili add documents --index movies --file movies.json --wait " `; -exports[`meili create documents > prints the enqueued task as the API returned it, without waiting 1`] = ` +exports[`meili add documents > prints the enqueued task as the API returned it, without waiting 1`] = ` { "exitCode": 0, "stderr": "", diff --git a/test/commands/create/documents.test.ts b/test/commands/add/documents.test.ts similarity index 89% rename from test/commands/create/documents.test.ts rename to test/commands/add/documents.test.ts index 828ba5e..ace5c8e 100644 --- a/test/commands/create/documents.test.ts +++ b/test/commands/add/documents.test.ts @@ -2,7 +2,7 @@ import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { describe, expect, it } from "vitest"; -import CreateDocuments from "../../../src/commands/create/documents.js"; +import AddDocuments from "../../../src/commands/add/documents.js"; import { MEILISEARCH_URL, makeEnqueuedTask, @@ -16,13 +16,13 @@ useMockServer(); const env = { MEILI_PROJECT_URL: MEILISEARCH_URL }; const documents = '[{"id":1,"title":"Nosferatu"},{"id":2,"title":"Alien"}]'; -const directory = mkdtempSync(join(tmpdir(), "meili-create-documents-")); +const directory = mkdtempSync(join(tmpdir(), "meili-add-documents-")); const file = join(directory, "movies.json"); writeFileSync(file, documents); -describe("meili create documents", () => { +describe("meili add documents", () => { it("prints its help without any network call", async () => { - const result = await runCli(["create", "documents", "--help"]); + const result = await runCli(["add", "documents", "--help"]); expect(result.exitCode).toBe(0); expect(result.stderr).toBe(""); expect(result.stdout).toMatchSnapshot(); @@ -31,7 +31,7 @@ describe("meili create documents", () => { it("prints the enqueued task as the API returned it, without waiting", async () => { server.use(meilisearchHandlers.documentsAdd(makeEnqueuedTask())); const result = await runCommand( - CreateDocuments, + AddDocuments, ["--index", "movies", "--data", documents, "--output", "json"], { env }, ); @@ -42,7 +42,7 @@ describe("meili create documents", () => { server.use(meilisearchHandlers.documentsAdd(makeEnqueuedTask())); const requests = await recordRequests(async () => { await runCommand( - CreateDocuments, + AddDocuments, [ "--index", "movies", @@ -71,7 +71,7 @@ describe("meili create documents", () => { it("sends no query parameter when no option is given", async () => { server.use(meilisearchHandlers.documentsAdd(makeEnqueuedTask())); const requests = await recordRequests(async () => { - await runCommand(CreateDocuments, ["--index", "movies", "--data", documents], { env }); + await runCommand(AddDocuments, ["--index", "movies", "--data", documents], { env }); }); expect(requests[0]?.url.search).toBe(""); }); @@ -79,7 +79,7 @@ describe("meili create documents", () => { it("refuses to run without documents, before any network call", async () => { let result: Awaited> | undefined; const requests = await countRequests(async () => { - result = await runCommand(CreateDocuments, ["--index", "movies"], { env }); + result = await runCommand(AddDocuments, ["--index", "movies"], { env }); }); expect(requests).toBe(0); expect(result?.exitCode).toBe(2); @@ -90,7 +90,7 @@ describe("meili create documents", () => { let result: Awaited> | undefined; const requests = await countRequests(async () => { result = await runCommand( - CreateDocuments, + AddDocuments, ["--index", "movies", "--file", join(directory, "absent.json")], { env }, ); @@ -109,7 +109,7 @@ describe("meili create documents", () => { ]), ); const result = await runCommand( - CreateDocuments, + AddDocuments, ["--index", "movies", "--data", documents, "--wait", "--output", "json"], { env }, ); @@ -123,7 +123,7 @@ describe("meili create documents", () => { meilisearchHandlers.taskSequence(42, [makeTask({ uid: 42, status: "failed" })]), ); const result = await runCommand( - CreateDocuments, + AddDocuments, ["--index", "movies", "--data", documents, "--wait", "--output", "json"], { env }, ); @@ -142,7 +142,7 @@ describe("meili create documents", () => { }), ); const result = await runCommand( - CreateDocuments, + AddDocuments, ["--index", "nope", "--data", documents, "--output", "json"], { env }, ); diff --git a/test/contract/__snapshots__/commands.test.ts.snap b/test/contract/__snapshots__/commands.test.ts.snap index fd8540a..cb73563 100644 --- a/test/contract/__snapshots__/commands.test.ts.snap +++ b/test/contract/__snapshots__/commands.test.ts.snap @@ -3,6 +3,98 @@ exports[`oclif.manifest.json > \`meili schema\` is a snapshot, the index and every command in full, so a renamed command or flag is visible in review 1`] = ` { "commands": [ + { + "api": "meilisearch", + "async": true, + "description": "Add documents to an index, replacing the ones that already have the same primary key.", + "examples": [ + "meili add documents --index movies --file movies.json", + "meili add documents --index movies --file movies.json --output json", + "cat movies.json | meili add documents --index movies --file -", + "meili add documents --index movies --data '[{"id":1,"title":"Nosferatu"}]' --primary-key id", + "meili add 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": "add documents", + "keyActions": [ + "documents.add", + ], + "routes": [ + "POST /indexes/{index_uid}/documents", + ], + }, { "api": "none", "description": "Log in to Meilisearch Cloud with a browser, and store the token for the management commands.", @@ -133,98 +225,6 @@ The file holds API keys and decides where every later command goes, for every pr ], "id": "config remove", }, - { - "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, @@ -660,6 +660,13 @@ The file holds API keys and decides where every later command goes, for every pr ], "index": { "commands": [ + { + "description": "Add documents to an index, replacing the ones that already have the same primary key.", + "id": "add documents", + "routes": [ + "POST /indexes/{index_uid}/documents", + ], + }, { "description": "Log in to Meilisearch Cloud with a browser, and store the token for the management commands.", "id": "auth login", @@ -688,13 +695,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 remove", }, - { - "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", diff --git a/test/contract/api.test.ts b/test/contract/api.test.ts index ae6c624..405cfac 100644 --- a/test/contract/api.test.ts +++ b/test/contract/api.test.ts @@ -71,7 +71,7 @@ const globalFlags = [ ...Object.keys(BaseCommand.baseFlags), ...Object.keys(BaseCommand.connectionFlags), ]; -const VERBS = ["get", "create", "update", "delete", ...LITERAL_VERBS]; +const VERBS = ["get", "add", "update", "delete", ...LITERAL_VERBS]; describe("API commands derive from their routes", () => { for (const { id, file, command } of commands) { diff --git a/test/integration/meilisearch/create-documents.test.ts b/test/integration/meilisearch/add-documents.test.ts similarity index 88% rename from test/integration/meilisearch/create-documents.test.ts rename to test/integration/meilisearch/add-documents.test.ts index 7df9898..de2a04a 100644 --- a/test/integration/meilisearch/create-documents.test.ts +++ b/test/integration/meilisearch/add-documents.test.ts @@ -10,11 +10,11 @@ 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-create-documents-${randomUUID().slice(0, 8)}`; +const uid = `meili-cli-test-add-documents-${randomUUID().slice(0, 8)}`; const file = join(mkdtempSync(join(tmpdir(), "meili-cli-test-")), "movies.json"); writeFileSync(file, JSON.stringify([{ id: 1, title: "Nosferatu" }])); -describe.skipIf(!meilisearchUp)("meili create documents against a real Meilisearch", () => { +describe.skipIf(!meilisearchUp)("meili add documents against a real Meilisearch", () => { const client = new Meilisearch({ host: MEILISEARCH_URL, apiKey: MASTER_KEY }); beforeAll(async () => { @@ -27,7 +27,7 @@ describe.skipIf(!meilisearchUp)("meili create documents against a real Meilisear it("enqueues a task and returns at once, without --wait", async () => { const result = await runBinary( - ["create", "documents", "--index", uid, "--file", file, "--output", "json"], + ["add", "documents", "--index", uid, "--file", file, "--output", "json"], env, ); expect(result.exitCode).toBe(0); @@ -39,7 +39,7 @@ describe.skipIf(!meilisearchUp)("meili create documents against a real Meilisear it("reads the documents from stdin with --file -, and --wait prints the final task", async () => { const result = await runBinary( - ["create", "documents", "--index", uid, "--file", "-", "--wait", "--output", "json"], + ["add", "documents", "--index", uid, "--file", "-", "--wait", "--output", "json"], env, JSON.stringify([{ id: 2, title: "Alien" }]), ); @@ -51,7 +51,7 @@ describe.skipIf(!meilisearchUp)("meili create documents against a real Meilisear it("replaces a document that has the same primary key", async () => { const result = await runBinary( [ - "create", + "add", "documents", "--index", uid, @@ -70,7 +70,7 @@ describe.skipIf(!meilisearchUp)("meili create documents against a real Meilisear it("prints the failed task and exits 7 when --wait sees the task fail", async () => { const result = await runBinary( [ - "create", + "add", "documents", "--index", uid, diff --git a/test/lib/api-naming.test.ts b/test/lib/api-naming.test.ts index c6c16d6..84c9483 100644 --- a/test/lib/api-naming.test.ts +++ b/test/lib/api-naming.test.ts @@ -115,15 +115,15 @@ describe("deriveCommandId", () => { expect(derive("GET /health")).toBe("get health"); expect(derive("GET /indexes")).toBe("get indexes"); expect(derive("GET /indexes/{index_uid}")).toBe("get indexes"); - expect(derive("POST /indexes")).toBe("create indexes"); + expect(derive("POST /indexes")).toBe("add indexes"); expect(derive("PATCH /indexes/{index_uid}")).toBe("update indexes"); expect(derive("DELETE /indexes/{index_uid}")).toBe("delete indexes"); expect(derive("GET /indexes/{index_uid}/stats")).toBe("get stats"); - expect(derive("POST /indexes/{index_uid}/documents")).toBe("create documents"); + expect(derive("POST /indexes/{index_uid}/documents")).toBe("add documents"); expect(derive("PUT /indexes/{index_uid}/documents")).toBe("update documents"); expect(derive("DELETE /indexes/{index_uid}/documents")).toBe("delete documents"); expect(derive("GET /tasks/{task_id}/documents")).toBe("get documents"); - expect(derive("POST /dumps")).toBe("create dumps"); + expect(derive("POST /dumps")).toBe("add dumps"); expect(derive("GET /api/v2/me")).toBe("get me"); expect(derive("GET /api/v2/projects/{id}")).toBe("get projects"); expect(derive("DELETE /api/v2/team-members")).toBe("delete team-members"); @@ -136,7 +136,7 @@ describe("deriveCommandId", () => { expect(derive("POST /indexes/{index_uid}/search")).toBe("search indexes"); expect(derive("POST /multi-search")).toBe("multi-search"); expect(derive("POST /export")).toBe("export"); - expect(derive("POST /swap-indexes")).toBe("create swap-indexes"); + expect(derive("POST /swap-indexes")).toBe("add swap-indexes"); }); }); @@ -219,7 +219,7 @@ describe("flagsForRoute", () => { }); it("unwraps a body made of one object named after the resource", () => { - expect(flags("POST /api/v2/projects", "create projects")).toEqual([ + expect(flags("POST /api/v2/projects", "add projects")).toEqual([ "name", "resource-id", "team-id", @@ -227,7 +227,7 @@ describe("flagsForRoute", () => { }); it("names identifiers by what they identify, with the API spelling as an alias", () => { - expect(flags("POST /indexes", "create indexes")).toEqual(["id", "primary-key", "uid"]); + expect(flags("POST /indexes", "add indexes")).toEqual(["id", "primary-key", "uid"]); expect(flags("GET /tasks", "get tasks")).toEqual([ "batch-ids", "batch-uids",