Skip to content

Message save location#2731

Open
burnettk wants to merge 8 commits intomainfrom
message-save-location
Open

Message save location#2731
burnettk wants to merge 8 commits intomainfrom
message-save-location

Conversation

@burnettk
Copy link
Copy Markdown
Contributor

No description provided.

jasquat and others added 4 commits February 24, 2026 11:00
# Conflicts:
#	spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Backend: message model responses now include primary id; incoming message payloads are sanitized to remove id/location; persistence merges message models preserving IDs and updates locations. Frontend: message editor fetches shared message models, finds nearest ancestor locations, lets users select existing shared messages, and propagates id/location when updating.

Changes

Cohort / File(s) Summary
Backend — message responses
spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py
Include primary id in message model response objects returned by _get_message_models().
Backend — process group handling
spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py
Add _strip_message_model_metadata() to remove id/location from incoming messages payloads; sanitize request body before persisting; pass unsanitized messages to message-model collection where appropriate.
Backend — message persistence
spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py
Support configurable location and optional numeric id resolution; look up existing MessageModel by (identifier, location); merge models via db.session.merge, flush to obtain IDs, and recreate association rows for correlation properties.
Backend — tests
spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py
Assert returned message id is int; add integration test that moves a message to a new location using an existing message id and verifies persistent id is preserved while location changes.
Frontend — types & interfaces
spiffworkflow-frontend/src/interfaces.ts
Extend MessageDefinition with optional id?: number and location?: string.
Frontend — message editor
spiffworkflow-frontend/src/components/messages/MessageEditor.tsx
Major refactor: strong typing for fetched message models/form state; fetch process group + message models; compute nearest ancestor shared message; expose useExistingSharedMessageId select; merge correlation properties; set/remove id and location on update; delete old message entry when identifier/id changes.
Frontend — utilities & tests
spiffworkflow-frontend/src/components/messages/MessageHelper.tsx, MessageHelper.test.tsx, MessageEditor.test.tsx
Add findNearestAncestorLocation() utility and unit tests; add MessageEditor test mocking backend responses and verifying shared-message selection behavior.
Frontend — i18n
spiffworkflow-frontend/src/locales/en_us/translation.json
Add three localization keys for UI text around using existing shared messages.
Frontend — build runtime
spiffworkflow-frontend/Dockerfile
Bump nginx base image from 1.29.5-alpine to 1.29.7-alpine and remove two explicit apk upgrade steps.
Backend — dependency
spiffworkflow-backend/pyproject.toml
Tighten authlib minimum version from >=1.6.0 to >=1.6.9.

Sequence Diagram

sequenceDiagram
    participant User
    participant Frontend as MessageEditor
    participant API as Backend API
    participant DB as Database

    User->>Frontend: Open message editor
    activate Frontend
    Frontend->>API: GET /process-groups/{id}
    Frontend->>API: GET /message-models/{id}
    activate API
    API->>DB: Query process group
    API->>DB: Query message models (include id, location)
    DB-->>API: Process group + message models
    API-->>Frontend: Return process group + message models
    deactivate API

    Frontend->>Frontend: Build shared message options\nfindNearestAncestorLocation\nprefill form state
    User->>Frontend: Select shared message option / edit form
    Frontend->>Frontend: Merge correlation properties\nset schema / id / location as needed
    User->>Frontend: Submit update
    Frontend->>API: PATCH /process-groups/{id} (sanitized messages payload)
    activate API
    API->>API: _strip_message_model_metadata(payload)
    API->>DB: Lookup MessageModel by (identifier, location)
    DB-->>API: existing message (optional)
    API->>DB: db.session.merge(message_model) -> flush -> obtain id
    API->>DB: delete & recreate correlation properties for message_id
    DB-->>API: commit/flush results
    API-->>Frontend: Updated process group
    deactivate API
    deactivate Frontend
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

Suggested reviewers

  • jasquat
🚥 Pre-merge checks | ✅ 1 | ❌ 2

❌ Failed checks (1 warning, 1 inconclusive)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
Description check ❓ Inconclusive No pull request description was provided by the author, making it impossible to assess whether it relates to the changeset. Add a description explaining the purpose of the changes, such as how message models now support configurable locations and ID management across the application.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title 'Message save location' directly relates to the main change: implementing configurable location handling for message models throughout the backend and frontend.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch message-save-location

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

@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: 2

🧹 Nitpick comments (2)
spiffworkflow-frontend/src/interfaces.ts (1)

245-247: Sync the fetched message-model type too.

These new fields update the saved message definition shape, but MessageModel later in this file still omits id. That leaves the /message-models response under-typed even though this PR now depends on that field.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spiffworkflow-frontend/src/interfaces.ts` around lines 245 - 247, The
MessageModel type is missing the new fields added to MessageDefinition, causing
the /message-models response to be under-typed; update the MessageModel
interface to include the same optional properties (e.g., add id?: number and
location?: string to the MessageModel declaration) so its shape matches
MessageDefinition and the fetched response is correctly typed.
spiffworkflow-frontend/src/components/messages/MessageEditor.tsx (1)

310-344: Consider adding cleanup to prevent state updates on unmounted components.

The coordination pattern using closure variables works, but if the component unmounts before both API calls complete, the callbacks will attempt to update state on an unmounted component. While React handles this gracefully, adding an abort signal or mounted flag improves robustness.

♻️ Optional improvement with mounted flag
   useEffect(() => {
+    let isMounted = true;
     const currentGroupLocation = unModifyProcessIdentifierForPathParam(
       modifiedProcessGroupIdentifier,
     );
     // ... existing code ...

     const maybeFinalize = () => {
-      if (processGroupLoaded && messageModelsLoaded && processGroupResult) {
+      if (isMounted && processGroupLoaded && messageModelsLoaded && processGroupResult) {
         processResult(processGroupResult, messageModelsResult);
       }
     };

     // ... API calls ...
+
+    return () => {
+      isMounted = false;
+    };
   }, [modifiedProcessGroupIdentifier, correlationProperties, messageId]);
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spiffworkflow-frontend/src/components/messages/MessageEditor.tsx` around
lines 310 - 344, The component makes two HttpService.makeCallToBackend calls and
uses closure vars (processGroupResult, messageModelsResult, processGroupLoaded,
messageModelsLoaded, maybeFinalize) to update state when both return, but
there’s no cleanup to prevent updates after unmount; modify the surrounding
useEffect to create a mounted flag (or an AbortController) and in each
successCallback/failureCallback check the flag (or abort signal) before
assigning to processGroupResult/messageModelsResult, setting the loaded flags,
and calling maybeFinalize, and return a cleanup function that sets mounted=false
(or calls abort()) so callbacks won’t update state on an unmounted component.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py`:
- Around line 18-29: The code currently trusts a caller-supplied message_id and
assigns it to message_model.id which can overwrite an unrelated row; before
setting message_model.id (or before merge), query MessageModel by the numeric
message_id (e.g., MessageModel.query.get(message_id) or
filter_by(id=message_id)) and verify that if a record exists its identifier and
location match the current identifier and message_location; if they match, you
may reuse that id, if a record exists but identifier/location differ,
raise/abort (reject the payload) to avoid reusing someone else's PK, and if no
record exists allow using the supplied id; if caller did not supply an id fall
back to existing_model.id as before.

In `@spiffworkflow-frontend/src/components/messages/MessageHelper.tsx`:
- Around line 9-15: The filter for candidateLocations in MessageHelper.tsx fails
to treat the root (normalizedCandidate === '') as an ancestor; update the
predicate in the candidateLocations.filter (where normalizedCandidate is
derived) to explicitly accept an empty normalizedCandidate as matching any
descendant (e.g., add a check like normalizedCandidate === '' || ...), so that
normalizedCurrentLocation is considered a match when the candidate is root,
while keeping the existing equality and startsWith(`${normalizedCandidate}/`)
checks.

---

Nitpick comments:
In `@spiffworkflow-frontend/src/components/messages/MessageEditor.tsx`:
- Around line 310-344: The component makes two HttpService.makeCallToBackend
calls and uses closure vars (processGroupResult, messageModelsResult,
processGroupLoaded, messageModelsLoaded, maybeFinalize) to update state when
both return, but there’s no cleanup to prevent updates after unmount; modify the
surrounding useEffect to create a mounted flag (or an AbortController) and in
each successCallback/failureCallback check the flag (or abort signal) before
assigning to processGroupResult/messageModelsResult, setting the loaded flags,
and calling maybeFinalize, and return a cleanup function that sets mounted=false
(or calls abort()) so callbacks won’t update state on an unmounted component.

In `@spiffworkflow-frontend/src/interfaces.ts`:
- Around line 245-247: The MessageModel type is missing the new fields added to
MessageDefinition, causing the /message-models response to be under-typed;
update the MessageModel interface to include the same optional properties (e.g.,
add id?: number and location?: string to the MessageModel declaration) so its
shape matches MessageDefinition and the fetched response is correctly typed.
🪄 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: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c4619d60-8b95-431e-b9e2-2a2f46e2819d

📥 Commits

Reviewing files that changed from the base of the PR and between 7fd22f2 and 1d56db6.

📒 Files selected for processing (10)
  • spiffworkflow-backend/src/spiffworkflow_backend/routes/messages_controller.py
  • spiffworkflow-backend/src/spiffworkflow_backend/routes/process_groups_controller.py
  • spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py
  • spiffworkflow-backend/tests/spiffworkflow_backend/integration/test_messages.py
  • spiffworkflow-frontend/src/components/messages/MessageEditor.test.tsx
  • spiffworkflow-frontend/src/components/messages/MessageEditor.tsx
  • spiffworkflow-frontend/src/components/messages/MessageHelper.test.tsx
  • spiffworkflow-frontend/src/components/messages/MessageHelper.tsx
  • spiffworkflow-frontend/src/interfaces.ts
  • spiffworkflow-frontend/src/locales/en_us/translation.json

Comment on lines +9 to +15
const matchingLocations = candidateLocations.filter((candidateLocation) => {
const normalizedCandidate = candidateLocation.replace(/(^\/+|\/+$)/g, '');
return (
normalizedCurrentLocation === normalizedCandidate ||
normalizedCurrentLocation.startsWith(`${normalizedCandidate}/`)
);
});
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot Mar 26, 2026

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Handle the root location as an ancestor.

When candidateLocation normalizes to '', Line 13 checks startsWith('/'), so a root-scoped shared message never matches descendant paths like order/residential. That diverges from the backend's “higher in the directory tree” lookup and will miss valid shared-message candidates.

Possible fix
   const matchingLocations = candidateLocations.filter((candidateLocation) => {
     const normalizedCandidate = candidateLocation.replace(/(^\/+|\/+$)/g, '');
     return (
+      normalizedCandidate === '' ||
       normalizedCurrentLocation === normalizedCandidate ||
       normalizedCurrentLocation.startsWith(`${normalizedCandidate}/`)
     );
   });
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const matchingLocations = candidateLocations.filter((candidateLocation) => {
const normalizedCandidate = candidateLocation.replace(/(^\/+|\/+$)/g, '');
return (
normalizedCurrentLocation === normalizedCandidate ||
normalizedCurrentLocation.startsWith(`${normalizedCandidate}/`)
);
});
const matchingLocations = candidateLocations.filter((candidateLocation) => {
const normalizedCandidate = candidateLocation.replace(/(^\/+|\/+$)/g, '');
return (
normalizedCandidate === '' ||
normalizedCurrentLocation === normalizedCandidate ||
normalizedCurrentLocation.startsWith(`${normalizedCandidate}/`)
);
});
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spiffworkflow-frontend/src/components/messages/MessageHelper.tsx` around
lines 9 - 15, The filter for candidateLocations in MessageHelper.tsx fails to
treat the root (normalizedCandidate === '') as an ancestor; update the predicate
in the candidateLocations.filter (where normalizedCandidate is derived) to
explicitly accept an empty normalizedCandidate as matching any descendant (e.g.,
add a check like normalizedCandidate === '' || ...), so that
normalizedCurrentLocation is considered a match when the candidate is root,
while keeping the existing equality and startsWith(`${normalizedCandidate}/`)
checks.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

@copilot fix if you can also run ./bin/run_pyl

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!

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.

🧹 Nitpick comments (1)
spiffworkflow-frontend/Dockerfile (1)

45-45: Pin the NGINX base image by digest for reproducibility and supply-chain traceability.

Line [45] uses a mutable tag (nginx:1.29.7-alpine). Pin this image to its immutable digest for better supply-chain safety and build reproducibility:

Suggested Dockerfile adjustment
-FROM nginx:1.29.7-alpine
+FROM nginx:1.29.7-alpine@sha256:<resolved_digest>

To find the exact digest, run: docker manifest inspect nginx:1.29.7-alpine | jq '.[0].digest'

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@spiffworkflow-frontend/Dockerfile` at line 45, Replace the mutable base image
reference "FROM nginx:1.29.7-alpine" with its immutable digest form to ensure
reproducible builds and better supply-chain traceability; obtain the exact
digest by running `docker manifest inspect nginx:1.29.7-alpine | jq
'.[0].digest'` (or the registry equivalent) and update the Dockerfile's FROM
line to use "nginx@sha256:<digest>" instead of the tag.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@spiffworkflow-frontend/Dockerfile`:
- Line 45: Replace the mutable base image reference "FROM nginx:1.29.7-alpine"
with its immutable digest form to ensure reproducible builds and better
supply-chain traceability; obtain the exact digest by running `docker manifest
inspect nginx:1.29.7-alpine | jq '.[0].digest'` (or the registry equivalent) and
update the Dockerfile's FROM line to use "nginx@sha256:<digest>" instead of the
tag.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0598cbc7-bc6d-487e-a4be-b9bf0cfeae1b

📥 Commits

Reviewing files that changed from the base of the PR and between 1d56db6 and dfb2ca0.

⛔ Files ignored due to path filters (1)
  • spiffworkflow-backend/uv.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • spiffworkflow-backend/pyproject.toml
  • spiffworkflow-frontend/Dockerfile
✅ Files skipped from review due to trivial changes (1)
  • spiffworkflow-backend/pyproject.toml

@burnettk
Copy link
Copy Markdown
Contributor Author

@copilot Verify each finding against the current code and only fix it if needed.

In
@spiffworkflow-backend/src/spiffworkflow_backend/services/message_definition_service.py
around lines 18 - 29, The code currently trusts a caller-supplied message_id and
assigns it to message_model.id which can overwrite an unrelated row; before
setting message_model.id (or before merge), query MessageModel by the numeric
message_id (e.g., MessageModel.query.get(message_id) or
filter_by(id=message_id)) and verify that if a record exists its identifier and
location match the current identifier and message_location; if they match, you
may reuse that id, if a record exists but identifier/location differ,
raise/abort (reject the payload) to avoid reusing someone else's PK, and if no
record exists allow using the supplied id; if caller did not supply an id fall
back to existing_model.id as before.

Copy link
Copy Markdown
Contributor

Copilot AI commented Mar 27, 2026

@copilot Verify each finding against the current code and only fix it if needed.

In
...

Fixed in 6f8eeb3. When message_id is caller-supplied, we now query MessageModel by that id before using it. If a record exists but its identifier or location doesn't match the current message, a ValueError is raised to prevent overwriting an unrelated row. If the record matches or doesn't exist, the supplied id is reused. If no id was supplied, it falls back to existing_model.id as before.

Agent-Logs-Url: https://github.com/sartography/spiff-arena/sessions/c961d6bd-c5cb-48e5-96e0-a9508db4dac0

Co-authored-by: burnettk <18027+burnettk@users.noreply.github.com>
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.

3 participants