Skip to content

Answers piping and conditional questions - #53

Merged
plutov merged 3 commits into
mainfrom
answer-piping
Jul 26, 2026
Merged

plutov merged 3 commits into
mainfrom
answer-piping

Conversation

@plutov

@plutov plutov commented Jul 19, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • Add answer piping ({{ question_id }}) across survey text and option labels.
  • Add validated visible_if conditions with matching API and client evaluation.
  • Enforce visibility server-side, clear hidden answers transitively, and omit them from sessions and webhook payloads.
  • Update the sample survey with branching and piping examples.

Validation

  • cd api && go test -race ./...
  • cd ui && npm run type-check && npm run build && npm run lint

Summary by CodeRabbit

  • New Features
    • Conditional question visibility using visible_if, including nested all/any/not logic and answered-state checks.
    • Answer piping to interpolate {{ question_id }} placeholders in survey intro/outro, question text, and option labels.
    • Hidden questions are removed from API/webhook/export-style responses; their stored answers are cleared, and navigation updates to consider only visible questions.
  • Documentation
    • Updated README with conditional visibility and answer piping behavior, including supported operators and interpolation details.
  • Tests
    • Added coverage for condition parsing, validation, interpolation, and visibility behavior.

@plutov plutov self-assigned this Jul 19, 2026
@plutov
plutov marked this pull request as ready for review July 21, 2026 18:55
@coderabbitai

coderabbitai Bot commented Jul 21, 2026 •

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 27c9fbda-6162-4d7e-843b-e3be58454d37

📥 Commits

Reviewing files that changed from the base of the PR and between b13176b and 9cd36d8.

📒 Files selected for processing (1)
  • api/surveys/simple/metadata.yaml
🚧 Files skipped from review as they are similar to previous changes (1)
  • api/surveys/simple/metadata.yaml

Walkthrough

Conditional question visibility and answer piping were implemented across survey configuration, server answer handling, storage, webhooks, and the Vue client. The sample survey and documentation now demonstrate visible_if predicates and {{ question_id }} interpolation.

Changes

Conditional survey flow

Layer / File(s) Summary
Condition contracts and evaluation
api/pkg/types/questions.go, api/pkg/types/conditions.go, api/pkg/types/answers.go, api/pkg/types/conditions_test.go
Adds recursive visibility predicates, YAML validation, typed comparisons, boolean composition, and condition evaluation tests.
Interpolation and configuration validation
api/pkg/types/survey.go, api/pkg/parser/parser_test.go, api/surveys/simple/*, README.md, docs/ANSWER_PIPING_PLAN.md
Adds answer interpolation, reference validation, hidden-question omission, sample conditional survey content, and documentation.
Server answer lifecycle and storage
api/pkg/surveys/*, api/pkg/storage/*, api/pkg/db/*, api/pkg/services/services.go
Enforces visibility during submission, deletes newly hidden answers, recalculates completion, filters sessions, interpolates webhooks, and adds deletion storage support.
Client visibility and rendering
ui/src/lib/*, ui/src/components/app/survey/*
Adds client-side condition evaluation, visible-question navigation, hidden-answer cleanup, interpolation, answer restoration, and session-aware rendering.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Respondent
  participant SurveyQuestions
  participant SubmitAnswer
  participant QuestionCondition
  participant Postgres
  Respondent->>SurveyQuestions: answer visible question
  SurveyQuestions->>SubmitAnswer: submit answer
  SubmitAnswer->>QuestionCondition: evaluate visibility
  QuestionCondition-->>SubmitAnswer: visibility result
  SubmitAnswer->>Postgres: store answer and delete newly hidden answers
  Postgres-->>SurveyQuestions: updated session
  SurveyQuestions->>SurveyQuestions: recompute visible navigation and interpolation
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 10.26% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title is concise and accurately summarizes the main changes: answer piping and conditional question visibility.
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.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch answer-piping

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🤖 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 `@api/pkg/surveys/answers.go`:
- Around line 136-149: The isSessionCompleted function must recompute completion
even when session.Status is Completed, since edited answers may expose an
unanswered question. Remove or adjust the early completed-status return, and
when recomputation finds the session incomplete, transition it to in-progress
and clear its completion timestamp in the SubmitAnswer flow.
- Around line 86-98: Make the full submission lifecycle atomic in the
answer-submission method containing the upsert, visibility cleanup loop, and
completion update: serialize submissions per session or execute all writes
within one transaction. Ensure validation, answer upserts,
DeleteSurveyQuestionAnswer cleanup, and the completion update cannot interleave
with another request for the same session, while preserving existing error
behavior.

In `@api/pkg/surveys/sessions.go`:
- Around line 204-218: Before calling sessionAnswerValues and
survey.Config.Interpolated in the webhook payload flow, clone the session and
apply filterVisibleAnswers to the clone. Use this filtered session for deriving
answers and as SurveySession when constructing the payload, preserving the
original session while ensuring hidden or stale question_answers are not
serialized.

In `@api/surveys/simple/metadata.yaml`:
- Around line 3-5: Remove the unavailable {{ name }} interpolation from the
intro text in the survey metadata, while preserving the existing outro piping of
name.

In `@docs/ANSWER_PIPING_PLAN.md`:
- Line 157: Update the array-separator wording in ANSWER_PIPING_PLAN so the
inline code span has no trailing space, describing the separator as
“comma-space” instead of showing `, `.

In `@ui/src/components/app/survey/SurveyFooter.vue`:
- Around line 22-24: Update formatOutro and its SurveyFooter rendering so
respondent-controlled interpolated answers are never passed to v-html. Preserve
line breaks using Vue text interpolation with white-space: pre-line, or escape
the interpolated text before converting newlines to breaks, while retaining the
existing outro formatting behavior.
🪄 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: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 46234294-1174-49bc-8e06-74e1e5c8f6ce

📥 Commits

Reviewing files that changed from the base of the PR and between ae676c1 and b13176b.

📒 Files selected for processing (23)
  • README.md
  • api/pkg/db/queries/surveys.sql
  • api/pkg/db/surveys.sql.go
  • api/pkg/parser/parser_test.go
  • api/pkg/services/services.go
  • api/pkg/storage/interface.go
  • api/pkg/storage/mocks_test.go
  • api/pkg/storage/postgres.go
  • api/pkg/surveys/answers.go
  • api/pkg/surveys/sessions.go
  • api/pkg/types/answers.go
  • api/pkg/types/conditions.go
  • api/pkg/types/conditions_test.go
  • api/pkg/types/questions.go
  • api/pkg/types/survey.go
  • api/surveys/simple/metadata.yaml
  • api/surveys/simple/questions.yaml
  • docs/ANSWER_PIPING_PLAN.md
  • ui/src/components/app/survey/SurveyFooter.vue
  • ui/src/components/app/survey/SurveyIntro.vue
  • ui/src/components/app/survey/SurveyQuestions.vue
  • ui/src/lib/questions.ts
  • ui/src/lib/types.ts

Comment on lines +86 to +98
// Re-evaluate in order and remove answers made stale by this submission.
for _, q := range survey.Config.Questions.Questions {
isVisible, evalErr := survey.Config.IsQuestionVisible(q, answers)
if evalErr != nil {
return errors.New("invalid visibility condition"), evalErr
}
if !isVisible {
if err := svc.Storage.DeleteSurveyQuestionAnswer(session.UUID, q.UUID); err != nil {
return errors.New("unable to clear hidden answer"), nil
}
delete(answers, q.ID)
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Make visibility cleanup atomic with answer submission.

The upsert, hidden-answer deletes, and completion update are separate writes. Concurrent requests can interleave: one request hides and deletes an answer while another, having validated against older state, upserts that now-hidden answer afterward. Serialize per-session submissions or run the full lifecycle in one transaction.

🤖 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 `@api/pkg/surveys/answers.go` around lines 86 - 98, Make the full submission
lifecycle atomic in the answer-submission method containing the upsert,
visibility cleanup loop, and completion update: serialize submissions per
session or execute all writes within one transaction. Ensure validation, answer
upserts, DeleteSurveyQuestionAnswer cleanup, and the completion update cannot
interleave with another request for the same session, while preserving existing
error behavior.

Comment on lines +136 to 149
func isSessionCompleted(survey *types.Survey, answers map[string]any, session *types.SurveySession) bool {
if session.Status == types.SurveySessionStatus_Completed {
return true
}
for _, q := range survey.Config.Questions.Questions {
visible, err := survey.Config.IsQuestionVisible(q, answers)
if err != nil || !visible {
continue
}
if _, ok := answers[q.ID]; !ok {
return false
}
}

return true

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Reopen a session when an edit exposes a required question.

SubmitAnswer permits answer updates, but this early return leaves a completed session completed even when the edit makes a previously hidden question visible and unanswered. Recompute completion regardless, transition back to in-progress when needed, and clear the completion timestamp.

🤖 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 `@api/pkg/surveys/answers.go` around lines 136 - 149, The isSessionCompleted
function must recompute completion even when session.Status is Completed, since
edited answers may expose an unanswered question. Remove or adjust the early
completed-status return, and when recomputation finds the session incomplete,
transition it to in-progress and clear its completion timestamp in the
SubmitAnswer flow.

Comment on lines +204 to +218
answers, err := sessionAnswerValues(survey, session)
if err != nil {
return fmt.Errorf("invalid session answers: %w", err)
}
config, err := survey.Config.Interpolated(answers)
if err != nil {
return fmt.Errorf("invalid interpolated survey: %w", err)
}
// Preserve the established session payload while adding a presentation-safe
// survey copy. It contains only visible questions and resolved text.
payload := struct {
*types.SurveySession
Survey *types.SurveyConfig `json:"survey"`
}{SurveySession: session, Survey: config}
data, err := json.Marshal(payload)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Filter the session before constructing the webhook payload.

Interpolated filters only payload.survey; payload.SurveySession remains the original session and can still serialize hidden or stale question_answers. Clone and run filterVisibleAnswers on the session first, then derive interpolation values and marshal that filtered copy.

🤖 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 `@api/pkg/surveys/sessions.go` around lines 204 - 218, Before calling
sessionAnswerValues and survey.Config.Interpolated in the webhook payload flow,
clone the session and apply filterVisibleAnswers to the clone. Use this filtered
session for deriving answers and as SurveySession when constructing the payload,
preserving the original session while ensuring hidden or stale question_answers
are not serialized.

Comment thread api/surveys/simple/metadata.yaml
- Match both `{{ question_id }}` and `{{question_id}}`.
- Trim whitespace around IDs.
- Missing answer becomes an empty string.
- Arrays join with `, `.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Remove the trailing space in the inline code span.

This triggers markdownlint MD038. Describe the separator as “comma-space” instead.

🧰 Tools
🪛 markdownlint-cli2 (0.23.0)

[warning] 157-157: Spaces inside code span elements

(MD038, no-space-in-code)

🤖 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 `@docs/ANSWER_PIPING_PLAN.md` at line 157, Update the array-separator wording
in ANSWER_PIPING_PLAN so the inline code span has no trailing space, describing
the separator as “comma-space” instead of showing `, `.

Source: Linters/SAST tools

Comment on lines 22 to +24
function formatOutro(outro: string): string {
return outro.replace(/(?:\r\n|\r|\n)/g, '<br>')
const answers = props.session ? answersFor(props.survey, props.session) : {}
return interpolateText(outro, answers).replace(/(?:\r\n|\r|\n)/g, '<br>')

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | ⚡ Quick win

Do not pass piped answers through v-html.

interpolateText inserts respondent-controlled values, and the template renders the result as HTML. A text answer containing markup can execute on the completion page. Render with Vue text interpolation and white-space: pre-line, or escape HTML before adding line breaks.

🤖 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 `@ui/src/components/app/survey/SurveyFooter.vue` around lines 22 - 24, Update
formatOutro and its SurveyFooter rendering so respondent-controlled interpolated
answers are never passed to v-html. Preserve line breaks using Vue text
interpolation with white-space: pre-line, or escape the interpolated text before
converting newlines to breaks, while retaining the existing outro formatting
behavior.

@plutov plutov left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

func stringsNumericOperator(op string) bool {
return op == "greater_than" || op == "greater_than_or_equal" || op == "less_than" || op == "less_than_or_equal"
}
func number(v any) (float64, bool) {

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Find a way to improve it, too verbose.

@plutov
plutov merged commit 6f3a83b into main Jul 26, 2026
3 checks passed
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.

1 participant