Skip to content

Edits to odc-cli and subject services for production usage#1374

Open
david-roper wants to merge 7 commits into
DouglasNeuroInformatics:mainfrom
david-roper:cli-fixes
Open

Edits to odc-cli and subject services for production usage#1374
david-roper wants to merge 7 commits into
DouglasNeuroInformatics:mainfrom
david-roper:cli-fixes

Conversation

@david-roper
Copy link
Copy Markdown
Collaborator

@david-roper david-roper commented Jun 5, 2026

Works on issue #1373
code implemented with usage of claude-sonnet-4-6 on claude code

Bug fixes for cascading subject deletion and the odc-cli HTTP client.

Changes

Fix: Cascading subject deletion order (apps/api/src/subjects/subjects.service.ts)

Reordered the Prisma $transaction in SubjectsService.deleteById so that
instrument records are deleted before sessions, then the subject itself.

The previous order (sessions → instrument records → subject) caused a
foreign-key violation in production because MongoDB enforced referential
integrity at the session level before instrument records had been cleared.
Local development was unaffected since standalone MongoDB does not enforce
FK constraints without a replica set.

New order:

  1. Delete instrument records
  2. Delete sessions
  3. Delete subject

Fix: odc-cli sending Content-Type on bodyless requests (cli/odc-cli)

DELETE requests with no body were including a Content-Type: application/json
header, causing the API to return 400 Body cannot be empty. The header is now
only added when a request body is present.

The request timeout was also increased from 2 s to 30 s to accommodate the
longer cascade-delete operation.


fix(cli): correct instrument-records minDate serialization and subject find filters

  • Serialize minDate as ISO 8601 string before URL encoding to ensure
    correct parsing by the backend's z.coerce.date() schema
  • Use GET /v1/subjects/:id (findById) when --subject-id is provided
    instead of passing it as an ignored list query param
  • Document that --subject-id and --min-date are mutually exclusive
    in the subjects find help text
  • Fix typo in setup init --dummy-subject-count help text

Co-Authored-By: Claude Sonnet 4.6 noreply@anthropic.com

Test Plan

  • Force-delete a subject with existing instrument records and sessions via
    the API — confirm 200 and no FK error in logs
  • Run python3 odc-cli subjects delete --id '<subject-id>' against a remote
    instance — confirm 200 and no 400 error
  • Run python3 odc-cli subjects find — confirm requests without a body
    still succeed
  • Verify delete completes within the 30 s window for subjects with many
    records

Summary by CodeRabbit

  • Bug Fixes

    • Increased HTTP request timeout from 2s to 30s for more reliable CLI operations.
    • Ensured subject deletion removes related records in proper order.
  • Improvements

    • CLI only sends JSON body and Content-Type when a body is present.
    • Subject lookup uses a direct endpoint when an ID is supplied; listing supports client-side date filtering.
  • Tests

    • Added test to verify transactional deletion order.

… timeout

DELETE requests were incorrectly including Content-Type: application/json
with no body, causing production servers to reject them with 400. The
request timeout was also too short (2s) for force-delete operations that
cascade through instrument records and sessions.
@david-roper david-roper requested a review from joshunrau as a code owner June 5, 2026 18:05
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Jun 5, 2026

Wondering what really moved? Review this PR in Change Stack to inspect semantic changes, definitions, and references.

Review Change Stack

Walkthrough

Adds instrument-record deletion into the subject deletion transaction (before session deletion). Updates CLI HttpClient to only set Content-Type when a body exists, construct request bodies conditionally, increase request timeout to 30s, serialize minDate for instrument records, and use path-style subject lookup with client-side min_date filtering when subject_id is absent.

Changes

Subject deletion transaction

Layer / File(s) Summary
Subject deletion transaction ordering
apps/api/src/subjects/subjects.service.ts, apps/api/src/subjects/__tests__/subjects.service.spec.ts
deleteById transaction now includes instrumentRecord.deleteMany scoped by subject id, ordered before session.deleteMany, and still deletes the subject at the end. Tests assert $transaction is called once and that the operations are ordered instrumentRecord → session → subject.

HTTP client headers and timeout

Layer / File(s) Summary
HTTP request construction and headers
cli/odc-cli
HttpClient._request builds and JSON-encodes a body only when data is not None, computes has_body for headers, and calls _send (which has a 30s timeout). _get_default_headers(has_body: bool = False) sets Content-Type: application/json only when has_body is true.
SubjectCommands.find URL and client-side filtering
cli/odc-cli
SubjectCommands.find requests /v1/subjects/{subject_id} when subject_id is provided; otherwise it requests /v1/subjects and applies min_date filtering client-side by parsing each item's createdAt and normalizing to UTC (adds timezone import). Help text for subjects find updated to forbid using --min-date with --subject-id, and a help typo was fixed.
InstrumentRecordsCommands.find minDate serialization
cli/odc-cli
InstrumentRecordsCommands.find now serializes minDate to an ISO-formatted string when provided.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Possibly related PRs

Suggested reviewers

  • joshunrau
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Title check ❓ Inconclusive The title 'Edits to odc-cli and subject services for production usage' is too vague and generic—it doesn't convey the specific nature of the fixes (transaction reordering, HTTP header/timeout fixes, serialization changes). Consider a more specific title like 'Fix cascading delete transaction order and HTTP client issues for production' to clearly communicate the main issues being addressed.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses production issues around subject cascade deletion in the API and improves odc-cli HTTP request behavior to avoid server-side 400 errors on bodyless requests.

Changes:

  • Reordered the subject force-delete $transaction so instrument records are deleted before sessions (then the subject).
  • Updated odc-cli to only send Content-Type: application/json when a request body is present.
  • Increased odc-cli request timeout from 2s to 30s to better accommodate long-running delete operations.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
cli/odc-cli Adjusts request header construction (avoid Content-Type on bodyless requests) and increases timeout for long-running operations.
apps/api/src/subjects/subjects.service.ts Fixes cascade delete order inside a Prisma transaction to avoid relation/FK violations in production.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread apps/api/src/subjects/subjects.service.ts
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (2)
cli/odc-cli (2)

416-416: 💤 Low value

Simplify: empty params dict is unnecessary.

Passing {} to build_url_with_params adds no value. Pass the URL string directly to HttpClient.get().

♻️ Proposed simplification
-        url = build_url_with_params(
-            f'{config.base_url}/v1/subjects',
-            {}
-        )
-        response = HttpClient.get(url)
+        response = HttpClient.get(f'{config.base_url}/v1/subjects')
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/odc-cli` at line 416, The call currently passes an empty params dict into
build_url_with_params and then into HttpClient.get; remove the unnecessary {} by
passing the plain URL string directly to HttpClient.get (i.e., stop calling
build_url_with_params when params is empty) so replace the
build_url_with_params(..., {}) usage with the URL string and call
HttpClient.get(url) instead, updating any surrounding code that assumes a
params-augmented URL; focus on the sites where build_url_with_params and
HttpClient.get are used together.

419-419: 💤 Low value

Clarify behavior when min_date is provided but response is not a list.

When min_date is provided but response.data is not a list, filtering is silently skipped. This could confuse users who expect filtering to occur. Consider logging a warning or validating the response type.

📝 Proposed clarification
-            if min_date is not None and isinstance(response.data, list):
+            if min_date is not None:
+                if not isinstance(response.data, list):
+                    print("Warning: Cannot apply min_date filter; API response is not a list", file=sys.stderr)
+                else:
                 min_date_utc = min_date.replace(tzinfo=timezone.utc) if min_date.tzinfo is None else min_date
                 response.data = [
                     s for s in response.data
                     if datetime.fromisoformat(s['createdAt'].replace('Z', '+00:00')) >= min_date_utc
                 ]
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/odc-cli` at line 419, When min_date is provided but response.data isn't a
list, we should surface that instead of silently skipping filtering: update the
branch around the "if min_date is not None and isinstance(response.data, list):"
check to handle the non-list case by logging a warning (via the module logger or
existing logger used nearby) or by raising a clear ValueError indicating that
filtering by min_date requires response.data to be a list; reference and use the
variables min_date and response.data in the message so callers can diagnose the
issue. Ensure the change is applied where response is processed (the code that
evaluates response.data) and keep the current list-filtering path intact.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@cli/odc-cli`:
- Around line 419-424: Wrap the date-parsing/filtering logic that uses
datetime.fromisoformat on s['createdAt'] in a try-except so malformed or missing
createdAt values do not crash the CLI: in the block that checks min_date and
isinstance(response.data, list) (referencing min_date, response.data, and the
list comprehension that accesses s['createdAt']), validate each item has a
truthy 'createdAt' string, attempt to parse it inside a try/except (catch
ValueError/KeyError/TypeError), skip items with invalid or missing dates
(optionally log or collect them), and only compare parsed datetimes to
min_date_utc for inclusion. Ensure min_date_utc computation
(min_date.replace/tzinfo) remains unchanged and used for the comparison.

---

Nitpick comments:
In `@cli/odc-cli`:
- Line 416: The call currently passes an empty params dict into
build_url_with_params and then into HttpClient.get; remove the unnecessary {} by
passing the plain URL string directly to HttpClient.get (i.e., stop calling
build_url_with_params when params is empty) so replace the
build_url_with_params(..., {}) usage with the URL string and call
HttpClient.get(url) instead, updating any surrounding code that assumes a
params-augmented URL; focus on the sites where build_url_with_params and
HttpClient.get are used together.
- Line 419: When min_date is provided but response.data isn't a list, we should
surface that instead of silently skipping filtering: update the branch around
the "if min_date is not None and isinstance(response.data, list):" check to
handle the non-list case by logging a warning (via the module logger or existing
logger used nearby) or by raising a clear ValueError indicating that filtering
by min_date requires response.data to be a list; reference and use the variables
min_date and response.data in the message so callers can diagnose the issue.
Ensure the change is applied where response is processed (the code that
evaluates response.data) and keep the current list-filtering path intact.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: 071169b5-b050-4ce2-a16a-1ad05552cee8

📥 Commits

Reviewing files that changed from the base of the PR and between e665a8a and 71d5b84.

📒 Files selected for processing (1)
  • cli/odc-cli

Comment thread cli/odc-cli
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

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

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
cli/odc-cli (1)

409-412: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Mutual exclusivity between --min-date and --subject-id is documented but not enforced.

If both arguments are provided, min_date is silently ignored. Consider using add_mutually_exclusive_group() in argparse or adding an explicit check with an error message.

Option 1: Enforce in argparse (preferred)

In _create_subjects_parser, use a mutually exclusive group:

mutex_group = find_parser.add_mutually_exclusive_group()
mutex_group.add_argument('--min-date', type=ArgumentTypes.valid_datetime, help='...')
mutex_group.add_argument('--subject-id', help='...')
Option 2: Runtime check
 def find(min_date: datetime | None, subject_id: str | None) -> None:
+    if min_date is not None and subject_id is not None:
+        print('Error: --min-date and --subject-id cannot be used together', file=sys.stderr)
+        sys.exit(1)
     if subject_id is not None:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@cli/odc-cli` around lines 409 - 412, The find function currently allows both
min_date and subject_id to be provided and silently ignores min_date; update the
CLI argument parsing in _create_subjects_parser to enforce mutual exclusivity by
creating a mutually exclusive group via add_mutually_exclusive_group() and
registering the two options there (use the existing ArgumentTypes.valid_datetime
for --min-date and the existing --subject-id argument), or if you prefer a
runtime guard, add an explicit check at the start of find(subject_id, min_date)
that raises a clear error when both are present (e.g., print error and exit) to
ensure the documented exclusivity is enforced.
🧹 Nitpick comments (1)
apps/api/src/subjects/__tests__/subjects.service.spec.ts (1)

143-147: ⚡ Quick win

Also assert deleteMany filters, not just operation order.

This test proves sequencing, but it can still pass if the delete predicates are wrong. Add assertions that each deleteMany is called with the expected subject.id/id filters for '123' to lock down cascade scope and prevent silent over/under-deletes in future edits.

Suggested assertion additions
       await subjectsService.deleteById('123', { force: true });
+      expect(prismaClient.instrumentRecord.deleteMany).toHaveBeenCalledWith(
+        expect.objectContaining({
+          where: expect.objectContaining({ subject: { id: '123' } })
+        })
+      );
+      expect(prismaClient.session.deleteMany).toHaveBeenCalledWith(
+        expect.objectContaining({
+          where: expect.objectContaining({ subject: { id: '123' } })
+        })
+      );
+      expect(prismaClient.subject.deleteMany).toHaveBeenCalledWith(
+        expect.objectContaining({
+          where: expect.objectContaining({ id: '123' })
+        })
+      );
       expect(prismaClient.$transaction).toHaveBeenCalledWith([instrumentRecordOp, sessionOp, subjectOp]);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@apps/api/src/subjects/__tests__/subjects.service.spec.ts` around lines 143 -
147, Add assertions that the three deleteMany calls use the correct where
filters for subject '123': assert prismaClient.instrumentRecord.deleteMany was
called with a filter like { where: { subjectId: '123' } }, assert
prismaClient.session.deleteMany was called with { where: { subjectId: '123' } },
and assert prismaClient.subject.deleteMany was called with { where: { id: '123'
} }; keep the existing assertion that prismaClient.$transaction was called with
[instrumentRecordOp, sessionOp, subjectOp] to preserve sequencing.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@cli/odc-cli`:
- Around line 409-412: The find function currently allows both min_date and
subject_id to be provided and silently ignores min_date; update the CLI argument
parsing in _create_subjects_parser to enforce mutual exclusivity by creating a
mutually exclusive group via add_mutually_exclusive_group() and registering the
two options there (use the existing ArgumentTypes.valid_datetime for --min-date
and the existing --subject-id argument), or if you prefer a runtime guard, add
an explicit check at the start of find(subject_id, min_date) that raises a clear
error when both are present (e.g., print error and exit) to ensure the
documented exclusivity is enforced.

---

Nitpick comments:
In `@apps/api/src/subjects/__tests__/subjects.service.spec.ts`:
- Around line 143-147: Add assertions that the three deleteMany calls use the
correct where filters for subject '123': assert
prismaClient.instrumentRecord.deleteMany was called with a filter like { where:
{ subjectId: '123' } }, assert prismaClient.session.deleteMany was called with {
where: { subjectId: '123' } }, and assert prismaClient.subject.deleteMany was
called with { where: { id: '123' } }; keep the existing assertion that
prismaClient.$transaction was called with [instrumentRecordOp, sessionOp,
subjectOp] to preserve sequencing.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: cdead55c-652f-4b26-a051-bc780bdce324

📥 Commits

Reviewing files that changed from the base of the PR and between 71d5b84 and 5858b03.

📒 Files selected for processing (2)
  • apps/api/src/subjects/__tests__/subjects.service.spec.ts
  • cli/odc-cli

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.

Comment thread cli/odc-cli
Comment on lines 571 to 578
find_parser = subparsers.add_parser('find', help='search subject records by optional filters')
find_parser.add_argument(
'--min-date',
type=ArgumentTypes.valid_datetime,
help='filter records created after this date (format: yyyy-mm-dd or yyyy-mm-dd hh:mm:ss)',
help='filter subjects created after this date (format: yyyy-mm-dd or yyyy-mm-dd hh:mm:ss). Cannot be used with --subject-id',
)
find_parser.add_argument('--subject-id', help='filter by subject id')
find_parser.add_argument('--subject-id', help='find a single subject by exact id. Cannot be used with --min-date')
find_parser.set_defaults(fn=SubjectCommands.find)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants