Skip to content

feat(cli): table field create, update, delete#358

Open
adriannoes wants to merge 5 commits into
devfrom
feat/cli-table-field-commands
Open

feat(cli): table field create, update, delete#358
adriannoes wants to merge 5 commits into
devfrom
feat/cli-table-field-commands

Conversation

@adriannoes

Copy link
Copy Markdown
Collaborator

Summary

  • Add pipefy table field subcommand group (create / update / delete) over existing SDK facade methods
  • Flip three docs/parity.md rows to shipped: create_table_field, update_table_field, delete_table_field
  • Update database-tables skill CLI references and CLI help golden fixture
  • Add CHANGELOG.md Unreleased entry

Closes #355

Part of milestone CLI parity closeout (PR 1 of 3).

Test plan

  • uv run pytest packages/cli/tests/test_table_field_commands.py -v (4 passed)
  • uv run pytest packages/cli/tests/test_cli_help_golden.py -v (2 passed)
  • uv run ruff check . && uv run ruff format --check .
  • uv run pipefy table field --help lists create, update, delete

Close CLI parity for create_table_field, update_table_field, and
delete_table_field via a new `pipefy table field` subcommand group.
@adriannoes adriannoes self-assigned this Jul 3, 2026
@adriannoes adriannoes added this to the CLI parity closeout milestone Jul 3, 2026
Comment on lines +212 to +223
extra = parse_json_object(extra_json, "--extra") or {}
attrs: dict[str, Any] = {}
if label is not None:
attrs["label"] = label
attrs.update(extra)
if not attrs:
raise typer.BadParameter(
"Provide at least one of: --label, --extra (non-empty)."
)

async def factory(client: PipefyClient):
return await client.update_table_field(field_id, table_id=table, **attrs)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Silent wrong-write when --extra carries a reserved key.

--extra is merged straight into attrs (line 216) and splatted into update_table_field(field_id, table_id=table, **attrs). Two failure modes:

  • --extra '{"id":"999"}' lands id in attrs. The SDK sets payload["id"] = field_id then overwrites it in its merge loop, so the update silently retargets a different field than the positional field_id. No error, wrong write. This is the realistic trap: copy a field object into --extra.
  • --extra '{"table_id":"42"}' collides with the explicit table_id=table kwarg and raises TypeError: got multiple values for keyword argument 'table_id', which run_cli_command does not catch, so it escapes as a raw traceback (exit 1).

The MCP path guards exactly these via _UPDATE_TABLE_FIELD_EXTRA_RESERVED = {"id"} and by popping table_id out of extra_input (packages/mcp/src/pipefy_mcp/tools/table_tools.py:59). This CLI re-implements the merge without the guard.

Suggest a shared helper in _common.py (e.g. build_update_attrs(explicit, extra, *, reserved)) that strips reserved keys before splatting, used by both this and create below, rather than two hand-rolled merges.

@adriannoes adriannoes Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8885813. merge_extra_attrs in _common.py strips id from --extra before the SDK call.

When --table is set, table_id is reserved in extra too. Typed flags override extra.

Tests:

  • test_table_field_update_strips_id_from_extra,
  • test_table_field_update_prefers_table_flag_over_extra_table_id.

Smoke tested on PipeClaw org.

Comment on lines +180 to +188
extra = parse_json_object(extra_json, "--extra") or {}
lab = label.strip()
ft = field_type.strip()
if not lab or not ft:
typer.echo("--label and --type must be non-empty.", err=True)
raise typer.Exit(2)

async def factory(client: PipefyClient):
return await client.create_table_field(table_id, lab, ft, **extra)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--extra reserved keys crash create.

create_table_field(table_id, lab, ft, **extra) splats --extra alongside the positional args, so:

  • --extra '{"label":"X"}'TypeError: got multiple values for argument 'label'
  • --extra '{"table_id":"99"}'TypeError: got multiple values for argument 'table_id'

run_cli_command catches PipefyError/TransportError/ValueError but not TypeError, so the user sees an unhandled traceback. ({"type": ...} in extra does not crash but silently overrides --type.)

The MCP tool filters these with _CREATE_TABLE_FIELD_EXTRA_RESERVED = {"table_id", "label", "type"} (table_tools.py:58). Routing this call through the same reserved-key helper suggested on the update command fixes both spots at once.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 8885813. create passes --extra through merge_extra_attrs with reserved {table_id, label, type} before splatting. Test: test_table_field_create_strips_reserved_keys_from_extra. Verified live on PipeClaw.

Comment on lines +197 to +201
table: str | None = typer.Option(
None,
"--table",
help="Table id containing this field (recommended; required by API).",
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

--table is optional here but required by the API.

delete (below) makes --table required, and the API requires table_id for updates too. As written, update f1 --label X (no --table) sends a payload without table_id and defers to a raw server-side rejection. The MCP tool returns a clean "table_id is required by the API" message before calling. Consider making --table required for parity with delete, or validating it here.

Note this is independent of the reserved-key collision above: requiring --table does not prevent a user from also passing table_id inside --extra, so both need addressing.

@adriannoes adriannoes Jul 7, 2026

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kept --table optional to match MCP: table_id can come from --extra when the flag is omitted (test_table_field_update_accepts_table_id_from_extra). merge_extra_attrs prevents table_id in extra from colliding when --table is set. No local preflight for missing table_id; API error path unchanged.

Happy to require --table in a follow-up if you want parity with delete.

Comment on lines +181 to +185
lab = label.strip()
ft = field_type.strip()
if not lab or not ft:
typer.echo("--label and --type must be non-empty.", err=True)
raise typer.Exit(2)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reuse + consistency (non-blocking).

Two things here:

  1. This block is near-identical to field_create in field.py (same --label/--type options, same .strip() + empty-check). A shared helper (e.g. require_nonempty_label_type) would collapse the duplication across table fields and phase fields.
  2. Error idiom is inconsistent within this PR: create uses typer.echo(...) + raise typer.Exit(2) while update uses typer.BadParameter. BadParameter is the idiomatic Typer path (exit 2 + usage context). Standardizing on it reads better. Relatedly, update never validates --label "" non-empty, so an empty label passes through there while create rejects it. Pick one behavior.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferred a shared require_nonempty_label_type helper; duplication with field_create predates this PR and is out of scope. Kept typer.Exit(2) on create to match field_create. Empty --label on update still passes through, same as MCP update_table_field.

Comment on lines +202 to +207
label: str | None = typer.Option(None, "--label", "-l", help="New field label."),
extra_json: str | None = typer.Option(
None,
"--extra",
help="JSON object merged into UpdateTableFieldInput.",
),

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Design question (non-blocking): typed flags vs --extra on update.

The MCP update_table_field exposes label, description, required, options as first-class args; here only --label is typed and everything else goes through --extra JSON. The minimalism matches table update, so it is defensible, but it is worth a conscious decision: --description/--required are common enough that pushing them into hand-written JSON is a rough edge, and it is exactly that --extra path that triggers the collision issues above. If you keep --extra-only, the reserved-key helper carries the safety; if you add typed flags, you shrink the JSON surface people actually touch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added --description, --required/--not-required, and --options on update (3ecea62). --extra remains for uncommon UpdateTableFieldInput keys.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Coverage gap that maps to the --extra findings.

These tests exercise only happy paths and delete-deny; no test passes --extra at all. That is why the reserved-key collision and silent id-override on update would ship unnoticed. Whatever you decide on those, add cases with a reserved key in --extra (e.g. update ... --extra '{"id":"999"}' and create ... --extra '{"label":"X"}') plus the update "no attrs provided" BadParameter path.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added six tests: reserved keys on create/update, table_id from extra, --table priority over extra table_id, typed flags, and BadParameter when no attrs are provided.

Comment thread CHANGELOG.md Outdated

- **MCP**: opt-in OAuth 2.0 resource-server profile for the `--remote` HTTP transport. Setting `PIPEFY_MCP_RS_RESOURCE_SERVER_URL` activates it (no separate enable flag): the server validates an inbound `Authorization: Bearer` on every request (RS256 against the issuer's JWKS, issuer and expiry, optional audience), serves RFC 9728 protected-resource metadata, and returns `401` with a `WWW-Authenticate` challenge on a missing or invalid token. Token-validation knobs live in `pipefy_auth.JwtValidationSettings` (`PIPEFY_JWT_ISSUER_URL`, which defaults to the login issuer, plus `PIPEFY_JWT_AUDIENCE` / `PIPEFY_JWT_VERIFY_AUDIENCE` / `PIPEFY_JWT_JWKS_URI`); resource identity (`PIPEFY_MCP_RS_RESOURCE_SERVER_URL`, `PIPEFY_MCP_RS_REQUIRED_SCOPES`) stays in `pipefy_mcp.ResourceServerSettings`; `PIPEFY_ALLOW_INSECURE_URLS` covers both. The stdio profile is unchanged. Per-request on-behalf-of identity is not yet wired (#302), so even with a validated bearer every call runs as the single startup identity. Closes #301.
- **SDK / MCP / CLI / auth**: outbound Pipefy requests now carry client-identifying telemetry headers so the API can attribute traffic by surface and version. The GraphQL path (public, Interfaces, and Internal API endpoints) sends `User-Agent: pipefy-sdk/<version> (<surface>)`, `X-Client-Name: <surface>`, and `X-Client-Version: <version>`, where `<surface>` is `mcp`, `cli`, or `sdk`. The surface is stamped programmatically at each entry point's composition root: the MCP server passes `mcp`, the CLI `cli`, and direct SDK use keeps the `sdk` default. It is a `PipefyClient` constructor argument rather than configuration, so it is never read from env or TOML and a caller cannot forge it. OAuth requests to the sign-in host carry `User-Agent: pipefy-auth/<version>`, `X-Client-Name: auth`, and `X-Client-Version`, identifying the auth component rather than an API surface. The header builder lives in `pipefy_infra.telemetry` so the SDK and auth packages share one `User-Agent` format. Closes #336.
- **CLI**: `pipefy table field create|update|delete` manage database table columns, closing the three deferred table-field rows in the MCP↔CLI parity matrix (`create_table_field`, `update_table_field`, `delete_table_field`). `create` takes positional table id plus `--label`/`--type` (pass-through to `CreateTableFieldInput`); `update` sends only provided keys (`--table` recommended, required by the API); `delete` is destructive (`--yes` or interactive confirm; `--table` required).

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit (optional): dense prose. This entry stacks several parentheticals into one long sentence. Splitting the three subcommands into short clauses (or a sub-list) would scan more easily. Purely cosmetic.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in ed4de16. Split the Unreleased entry into short clauses per subcommand.

Add merge_extra_attrs so create and update strip MCP-parity reserved keys
before splatting into the SDK, preventing silent wrong-writes and TypeError
tracebacks when --extra copies a full field object.
Expose --description, --required, and --options on pipefy table field update
for MCP parity, shrinking the JSON surface agents need via --extra.
Document the new update flags and clearer Unreleased changelog prose for
the table field CLI commands shipped in the parity closeout.
@adriannoes adriannoes requested a review from Danielmoraisg July 7, 2026 19:08
@adriannoes adriannoes added the enhancement New feature or request label Jul 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants