Skip to content

Make webhook serialization extensible by moving it onto the page controller #1408

Description

@micolgiannelli2

Context

WebhookModel currently builds its payload via a top-level pagesToQuestions(page, state) function in server/plugins/engine/models/submission/WebhookModel.ts. Each page type that needs different serialization (e.g. repeating sections (which I am currenlty working on and can link the downstream PR for)) forces a branch, either inside pagesToQuestions itself, or one level up via a type-check on the page object:

isRepeatingFieldPageController(page)
  ? repeatingPageToQuestion(page, state)
  : pagesToQuestions(page, state);

This pattern doesn't scale: every new page-type variant adds another branch to shared code.

Proposal

Add a toWebhookQuestions(state) method to PageControllerBase. The default implementation is the current pagesToQuestions body. Subclasses with different state shapes override it.

// PageControllerBase.ts
toWebhookQuestions(state: FormSubmissionState): WebhookQuestion[] {
// port current existing logic
}

// WebhookModel.ts
questions = relevantPages.flatMap((page) => page.toWebhookQuestions(state));

Returning an array (rather than a single question) future-proofs the signature for controllers that legitimately emit N blocks per page (e.g. per-entry serialization).

Why this is worth doing

  • WebhookModel stops needing to know page types. It iterates pages and asks each to serialize itself. Adding a new variant means one new override on the variant's controller — no diff against shared code.
  • Serialization lives next to state shape. Today, how a controller writes state is in one file and how that state is read for the webhook is in another. Co-locating them removes a known source of drift (e.g. sectionKey not being known to the serializer).
  • Future serializers benefit. Any CSV / JSON export / audit feature that needs "ask each page how to represent its state" can use the same hook.

Scope

(small only three files require changing)

  • PageControllerBase: add the method with the existing pagesToQuestions` body.
  • WebhookModel: swithch map to call page.toWebhookQuestions(state), this will require no added switch logic
  • Any controller that needs custom serialization: add an override (initially zero or one).

Backwards compatibility

For non-repeating pages, the default implementation produces a single-element array containing exactly the object pagesToQuestions returns today, so the emitted payload is byte-identical. A snapshot test against an existing form should confirm.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions