Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ uuid = { version = "1", features = ["v4"] }
rpassword = "7"
json_dotpath = "1"
self_update = { version = "0.43", default-features = false, features = ["reqwest", "rustls", "archive-tar", "compression-flate2"] }
validator = { version = "0.19", features = ["derive"] }
regex = "1"

[dev-dependencies]
mockito = "1"
Expand Down
8 changes: 8 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,14 @@ rw clinicians assign joe@example.com 60c0e3b8-64b6-491f-a502-7346d14b3192
rw clinicians prepare joe@example.com
rw clinicians prepare 60fda0c4-eca0-434a-80d8-fd4e490aa051

# Update a clinician attribute (by UUID, email, or "me")
rw clinicians update joe@example.com --field name --value "Jane Doe"
rw clinicians update 60fda0c4-eca0-434a-80d8-fd4e490aa051 --field email --value jane@example.com
rw clinicians update me --field npi --value 1234567890
rw clinicians update me --field credentials --value "RN,MD"
rw clinicians update me --field npi # omit --value to clear (sends null)
rw clinicians update me --field credentials # omit --value to clear (sends [])

# Use a named profile
rw clinicians enable joe@example.com --profile mercy
```
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
schema: spec-driven
created: 2026-04-05
68 changes: 68 additions & 0 deletions openspec/changes/archive/2026-04-05-clinicians-update/design.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## Context

The CLI already has a `clinicians` command module (`src/commands/clinicians.rs`) with subcommands for `assign`, `enable`, `disable`, and `prepare`. These subcommands follow a consistent pattern: resolve a target clinician (UUID or email), build a JSON:API PATCH body, send it, and print the result.

Target resolution today handles UUID or email strings. The `"me"` shorthand is not yet supported anywhere but the API likely exposes a `/clinicians/me` endpoint or the current user can be inferred from auth context.

The four fields to support — `name`, `email`, `npi`, `credentials` — are all `attributes` on the clinician resource. The API accepts PATCH in JSON:API format. `credentials` is an array of strings in the schema, while the others are scalars.

## Goals / Non-Goals

**Goals:**
- Add `rw clinicians update <target> --field <name> --value <val>`
- Support `target` = `"me"`, email address, or UUID
- Validate each field client-side before sending to the API
- Send a JSON:API PATCH to update a single attribute at a time
- Print the updated clinician on success

**Non-Goals:**
- Updating multiple fields in one command invocation (one `--field`/`--value` pair only)
- Adding to or removing individual items from the `credentials` array (the value replaces the entire array)
- Any changes to how other subcommands resolve targets

## Decisions

### 1. Single field per invocation

**Decision:** Accept exactly one `--field`/`--value` pair per call.

**Rationale:** Keeps the CLI surface minimal and consistent with the existing `--field`/`--value` pattern in other commands (e.g., `fields update`). Users wanting multiple field updates can chain commands. A multi-field flag design would require validating pairs and is significantly more complex.

**Alternative considered:** `--name`, `--email`, etc. as individual flags. Rejected because it couples the CLI interface to the attribute names and requires new flags for any future fields.

### 2. `"me"` resolution via `/clinicians` list filtered by auth identity

**Decision:** Resolve `"me"` by calling `GET /clinicians?filter[me]=true` or by fetching `/auth/me` (or equivalent) to get the current user's UUID, then fetching the clinician by that UUID. If the API provides a `/clinicians/me` shorthand, use it directly.

**Rationale:** The existing `require_auth` + `resolve_uuid_by_email` pattern doesn't cover the authenticated-user case. The cleanest solution is to add a `resolve_me()` helper that calls the appropriate endpoint.

**Alternative considered:** Caching the current user's UUID in auth config. Rejected because it would go stale and adds write-back complexity.

### 3. `credentials` value is comma-separated → array

**Decision:** When `--field credentials` is used, `--value` is treated as a comma-separated list of credential strings (e.g., `"RN,MD"`), which is split into an array before sending.

**Rationale:** The API stores credentials as `string[]`. The CLI value must be a single string argument. Comma-separation is a simple and widely-used convention for array inputs in CLI tools.

**Alternative considered:** Repeated `--value` flags for arrays. Rejected because the current `--field`/`--value` signature uses a single value pair; extending to multiple values would require a flag redesign.

### 4. Validation strategy

**Decision:** Use the [`validator`](https://crates.io/crates/validator) crate for client-side validation. Define a struct per field (or a unified `ClinicianUpdateInput` struct) with `#[validate(...)]` annotations, call `validate()` before sending to the API, and fail fast with a clear error.

| Field | Validation |
|--------------|-----------------------------------------|
| `name` | `#[validate(length(min = 1))]` after trimming |
| `email` | `#[validate(email)]` |
| `npi` | Empty or omitted `--value` sends `null` (clears NPI); non-empty value must pass `#[validate(length(equal = 10), regex(path = *NPI_RE))]` where `NPI_RE` matches `[0-9]{10}` |
| `credentials`| Empty or omitted `--value` sends `[]` (clears all); non-empty value is split on commas — no `validator` annotation needed |

**Rationale:** The `validator` crate provides declarative, well-tested validation (including RFC-compliant email checking) and standardised error types, avoiding hand-rolled regex for email. It is the conventional Rust choice for struct-level input validation.

**Alternative considered:** Hand-rolled regex / manual checks. Rejected in favour of `validator` to reduce bespoke validation code and leverage its email format support.

## Risks / Trade-offs

- **`"me"` resolution depends on an undiscovered endpoint** → Mitigation: check the OpenAPI spec for a `/clinicians/me` path or an auth profile endpoint; if absent, fetch the full clinician list and match against the authenticated identity.
- **`credentials` comma-split is lossy if a credential itself contains a comma** → Low risk in practice (credential strings like `"RN"`, `"MD"` never contain commas), but worth noting.
- **Single field per call means multiple roundtrips for bulk updates** → Acceptable trade-off given the scope of this feature.
31 changes: 31 additions & 0 deletions openspec/changes/archive/2026-04-05-clinicians-update/proposal.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
## Why

Clinician data sometimes needs to be corrected or updated after initial creation, but the CLI currently has no way to do this, requiring users to fall back to the web UI or direct API calls. Adding an `update` subcommand to `clinicians` closes this gap for common fields.

## What Changes

- New subcommand: `rw clinicians update <target> --field <name> --value <val>`
- `<target>` accepts `"me"` (authenticated user), an email address, or a UUID
- Updatable fields: `name`, `email`, `npi`, `credentials`
- Field-specific validation before sending to the API:
- `name`: non-empty string
- `email`: valid email format
- `npi`: string or empty/null (nullable)
- `credentials`: one or more credential strings (e.g. `"RN"`)

## Capabilities

### New Capabilities

- `clinicians-update`: Update a clinician's attributes via the CLI, with target resolution and per-field validation

### Modified Capabilities

<!-- none -->

## Impact

- Adds a new `update` subcommand under the existing `clinicians` command module
- Makes a `PATCH` request to the clinicians API endpoint in JSON:API format
- Requires target resolution logic (me / email / UUID) shared with existing clinician lookup patterns
- No new dependencies expected
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
## ADDED Requirements

### Requirement: Update a clinician attribute
The `clinicians update` subcommand SHALL update a single attribute of a clinician resource via a PATCH request to the API. The command SHALL accept a `--field` flag naming the attribute and a `--value` flag providing the new value.

#### Scenario: Successful update by UUID
- **WHEN** the user runs `rw clinicians update <uuid> --field name --value "Jane Doe"`
- **THEN** the CLI SHALL PATCH the clinician resource with the new name and print the updated clinician

#### Scenario: Successful update by email
- **WHEN** the user runs `rw clinicians update jane@example.com --field email --value "jane2@example.com"`
- **THEN** the CLI SHALL resolve the email to a UUID, PATCH the resource, and print the updated clinician

#### Scenario: Successful update as authenticated user
- **WHEN** the user runs `rw clinicians update me --field name --value "Jane Doe"`
- **THEN** the CLI SHALL resolve `"me"` to the currently authenticated clinician's UUID, PATCH the resource, and print the updated clinician

#### Scenario: Unsupported field rejected
- **WHEN** the user specifies `--field` with a value not in `{name, email, npi, credentials}`
- **THEN** the CLI SHALL exit with a non-zero status and print an error listing the allowed fields

### Requirement: Target resolution
The `<target>` argument SHALL be resolved to a clinician UUID before making any API call. Three forms are accepted: `"me"` (authenticated user), a valid email address, or a UUID string.

#### Scenario: UUID target passed through
- **WHEN** `<target>` is a valid UUID string
- **THEN** the CLI SHALL use it directly as the clinician ID without any lookup

#### Scenario: Email target resolved
- **WHEN** `<target>` is an email address (contains `@`)
- **THEN** the CLI SHALL fetch the clinician list and resolve the matching clinician's UUID

#### Scenario: "me" target resolved
- **WHEN** `<target>` is the literal string `"me"`
- **THEN** the CLI SHALL resolve to the UUID of the currently authenticated user

#### Scenario: Unknown target
- **WHEN** `<target>` does not match a UUID, email, or `"me"`, and no clinician is found
- **THEN** the CLI SHALL exit with a non-zero status and print a descriptive error

### Requirement: Field validation before API call
Each field SHALL be validated client-side before sending the PATCH request. Invalid values SHALL produce a clear error without making a network call.

#### Scenario: name must be non-empty
- **WHEN** `--field name` is given with an empty or whitespace-only `--value`
- **THEN** the CLI SHALL exit with a non-zero status and report that name must be non-empty

#### Scenario: email must have valid format
- **WHEN** `--field email` is given and `--value` does not contain `@` and a domain
- **THEN** the CLI SHALL exit with a non-zero status and report that the value is not a valid email address

#### Scenario: npi must be exactly 10 digits when provided
- **WHEN** `--field npi` is given with a non-empty `--value` that is not exactly 10 decimal digits
- **THEN** the CLI SHALL exit with a non-zero status and report that NPI must be a 10-digit number

#### Scenario: valid npi accepted
- **WHEN** `--field npi` is given with a string of exactly 10 decimal digits (e.g. `"1234567890"`)
- **THEN** the CLI SHALL send the value to the API

#### Scenario: npi cleared when value is empty or omitted
- **WHEN** `--field npi` is given with an empty `--value` or with `--value` omitted
- **THEN** the CLI SHALL send `null` as the npi value in the PATCH body

#### Scenario: credentials cleared when value is empty or omitted
- **WHEN** `--field credentials` is given with an empty `--value` or with `--value` omitted
- **THEN** the CLI SHALL send `[]` as the credentials array in the PATCH body

#### Scenario: credentials split on comma
- **WHEN** `--field credentials --value "RN,MD"` is provided
- **THEN** the CLI SHALL send `["RN", "MD"]` as the credentials array in the PATCH body

### Requirement: API PATCH in JSON:API format
The PATCH request SHALL conform to JSON:API format, including only the field being updated under `attributes`.

#### Scenario: PATCH body structure
- **WHEN** the update command sends a request
- **THEN** the request body SHALL be `{"data": {"type": "clinicians", "id": "<uuid>", "attributes": {"<field>": <value>}}}`

#### Scenario: PATCH response contains updated clinician
- **WHEN** the API returns a 2xx response to the PATCH request
- **THEN** the CLI SHALL parse the full clinician resource from the response body (no additional GET required)

#### Scenario: API error surfaced to user
- **WHEN** the API returns a non-2xx response
- **THEN** the CLI SHALL exit with a non-zero status and print the HTTP status code and response body
Loading