From c43bdacc1eb6e11d2014200863b3dc20ca0381dc Mon Sep 17 00:00:00 2001 From: Debdut Goswami Date: Wed, 26 Aug 2026 00:35:24 +0530 Subject: [PATCH] Support --fields on get and document its summary default MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit get returns each model's summary serialization — a handful of fields with no indication the record is partial and no way to request more (issue #16). An agent reading get output can wrongly conclude a field is not exposed through the API. get now accepts --fields, routed through the same find endpoint as list with an exact-id filter, so any field is reachable: fulfil sales_channel get 42 --fields settings_json,configuration Without --fields the summary representation is returned unchanged, and the help text now states that it is partial. On the --fields path, missing IDs fail with exit code 5 and results keep input ID order. --- src/fulfil_cli/cli/commands/model.py | 41 +++++++++++++++++++++++++-- tests/test_cli/test_model_commands.py | 39 +++++++++++++++++++++++++ 2 files changed, 77 insertions(+), 3 deletions(-) diff --git a/src/fulfil_cli/cli/commands/model.py b/src/fulfil_cli/cli/commands/model.py index 829de26..578b88b 100644 --- a/src/fulfil_cli/cli/commands/model.py +++ b/src/fulfil_cli/cli/commands/model.py @@ -164,19 +164,54 @@ def list_cmd( @model_group.command("get") @click.argument("ids", type=str) + @click.option( + "--fields", + "fields_str", + default=None, + help="Comma-separated field names, e.g. name,state,sale_date", + ) @format_option @click.pass_context - def get_cmd(ctx: click.Context, ids: str, output_format: str | None) -> None: - """Get records by ID(s). IDS is one or more comma-separated integers (e.g. 123 or 1,2,3).""" + def get_cmd( + ctx: click.Context, ids: str, fields_str: str | None, output_format: str | None + ) -> None: + """Get records by ID(s). IDS is one or more comma-separated integers (e.g. 123 or 1,2,3). + + Without --fields, returns a summary representation of each record — + not all fields. Use --fields to request specific fields ('describe' + lists all available fields). + """ app_ctx: AppContext = ctx.obj parsed_ids = _parse_ids(ids) + fields = _parse_fields(fields_str) try: client = app_ctx.get_client() - result = client.call(f"model.{model_name}.serialize", parsed_ids) + if fields: + result = client.call( + f"model.{model_name}.find", + where={"id": {"in": parsed_ids}}, + fields=fields, + page_size=len(parsed_ids), + ) + else: + result = client.call(f"model.{model_name}.serialize", parsed_ids) except FulfilError as exc: handle_error(exc, context=model_name) + if fields: + records = result["data"] if isinstance(result, dict) and "data" in result else result + by_id = {record["id"]: record for record in records} + missing = [i for i in parsed_ids if i not in by_id] + if missing: + missing_str = ", ".join(str(i) for i in missing) + console.print( + f"[red]No {model_name} record(s) found for ID(s): {missing_str}[/red]" + ) + raise typer.Exit(code=EXIT_NOT_FOUND) + + result = [by_id[i] for i in parsed_ids] + if len(parsed_ids) == 1 and isinstance(result, list) and len(result) == 1: result = result[0] diff --git a/tests/test_cli/test_model_commands.py b/tests/test_cli/test_model_commands.py index c03f2df..736c437 100644 --- a/tests/test_cli/test_model_commands.py +++ b/tests/test_cli/test_model_commands.py @@ -168,6 +168,45 @@ def test_multiple_ids(self, httpx_mock, cli_env, jsonrpc_success): assert isinstance(data, list) assert len(data) == 2 + def test_with_fields_routes_via_find( + self, httpx_mock, cli_env, jsonrpc_success, pagination_response + ): + httpx_mock.add_response( + json=jsonrpc_success(pagination_response([{"id": 42, "settings_json": {}}])) + ) + + result = runner.invoke(app, ["sale_order", "get", "42", "--fields", "settings_json"]) + assert result.exit_code == 0 + + data = json.loads(result.stdout) + assert data == {"id": 42, "settings_json": {}} + + body = json.loads(httpx_mock.get_request().content) + assert body["method"] == "model.sale_order.find" + assert body["params"]["where"] == {"id": {"in": [42]}} + assert body["params"]["fields"] == ["settings_json"] + assert body["params"]["page_size"] == 1 + + def test_with_fields_preserves_input_id_order( + self, httpx_mock, cli_env, jsonrpc_success, pagination_response + ): + httpx_mock.add_response(json=jsonrpc_success(pagination_response([{"id": 1}, {"id": 2}]))) + + result = runner.invoke(app, ["sale_order", "get", "2,1", "--fields", "id"]) + assert result.exit_code == 0 + + data = json.loads(result.stdout) + assert [r["id"] for r in data] == [2, 1] + + def test_with_fields_missing_id_exits_not_found( + self, httpx_mock, cli_env, jsonrpc_success, pagination_response + ): + httpx_mock.add_response(json=jsonrpc_success(pagination_response([{"id": 1}]))) + + result = runner.invoke(app, ["sale_order", "get", "1,999", "--fields", "id"]) + assert result.exit_code == 5 + assert "999" in result.output + def test_invalid_ids(self, cli_env): result = runner.invoke(app, ["sale_order", "get", "abc"]) assert result.exit_code == 2