Files
komp_ac/validation-core/docs/validation_architecture.md
2026-05-06 20:29:05 +02:00

10 KiB

Validation

This document is the frontend guide for the validation system.

The important idea: reusable validation is built from rules and sets. The frontend creates and manages those. When a set is applied to a table field, the server resolves it into the existing FieldValidation shape, and the form runtime continues to work through the normal table-validation flow.

Ownership

flowchart LR
    Core[validation-core<br/>validation meaning<br/>rule/set merge rules]
    Server[server<br/>stores rules/sets<br/>applies sets<br/>enforces writes]
    Common[common/proto<br/>gRPC contract]
    Client[client/frontend<br/>rule/set UI<br/>calls gRPC]
    Canvas[canvas<br/>field editing<br/>mask display<br/>local feedback]

    Server --> Core
    Canvas --> Core
    Client --> Common
    Server --> Common
    Client --> Canvas

server stores simple serializable settings. validation-core owns how those settings combine. canvas uses resolved field validation to guide editing.

Terms

Term Meaning
FieldValidation Existing per-column validation config from common/proto/table_validation.proto. This is what forms/canvas already consume.
ValidationRule One named reusable fragment, for example digits-only, phone-length, or required. Stored by the server as a FieldValidation fragment with no meaningful dataKey.
ValidationSet Ordered collection of rule names, for example phone = [required, phone-length, digits-only, phone-mask].
Applied validation A resolved snapshot of a set written to table_validation_rules for a concrete (table, dataKey).
Snapshot Applying a set copies the resolved config to a field. Later edits to the set do not automatically update fields that were already applied.

What Backend Enforces

Backend write validation enforces only server-relevant parts:

FieldValidation part Backend Canvas/frontend
required Yes Yes
limits Yes Yes
pattern Yes Yes
allowed_values Yes Yes
mask Partly: raw value length/literals Yes: display/editing mask
formatter No Yes
external_validation_enabled No Yes/UI hint

mask is visual metadata, but the backend still uses it to reject incorrectly submitted raw values. Example: if the mask is (###) ###-####, the backend expects the stored value to be raw digits, not (123) 456-7890.

Main User Flow

sequenceDiagram
    participant UI as Frontend UI
    participant API as TableValidationService
    participant DB as Server DB
    participant Form as Existing Form Runtime

    UI->>API: UpsertValidationRule(required)
    UI->>API: UpsertValidationRule(digits-only)
    UI->>API: UpsertValidationRule(phone-length)
    UI->>API: UpsertValidationSet(phone: [required, phone-length, digits-only])
    UI->>API: ApplyValidationSet(profile, table, dataKey, phone)
    API->>DB: write resolved FieldValidation snapshot
    Form->>API: GetTableValidation(profile, table)
    API->>Form: resolved FieldValidation for dataKey

After ApplyValidationSet, the existing form code does not need to know that a set was used. It receives normal FieldValidation.

API

All APIs live on TableValidationService.

Rules

Create or update one reusable rule:

UpsertValidationRule(UpsertValidationRuleRequest)

Request shape:

profileName: string
rule:
  name: string
  description: optional string
  validation: FieldValidation

Frontend rules:

  • rule.name is required and unique inside a profile.
  • rule.validation.dataKey is ignored by the server.
  • A rule should usually configure one logical fragment.
  • Examples: required, phone-length, digits-only, phone-mask.

List rules:

ListValidationRules({ profileName })

Delete rule:

DeleteValidationRule({ profileName, name })

Deleting a rule removes it from future reusable composition. Already applied field snapshots are not changed.

Sets

Create or update one reusable set:

UpsertValidationSet(UpsertValidationSetRequest)

Request shape:

profileName: string
set:
  name: string
  description: optional string
  ruleNames: repeated string

Frontend rules:

  • set.name is required and unique inside a profile.
  • ruleNames must contain at least one rule.
  • ruleNames are ordered.
  • Every rule name must already exist.
  • Duplicate rule names in the same set are rejected.
  • Conflicting singleton fragments are rejected.

Singleton fragments are:

limits
allowed_values
mask
formatter

That means a set cannot currently contain two rules that both define limits. Pattern rules are additive: multiple rules with pattern are merged into one combined pattern.

List sets:

ListValidationSets({ profileName })

Response includes each set plus resolvedValidation, so the frontend can show what the set expands to.

Delete set:

DeleteValidationSet({ profileName, name })

Deleting a set does not change already applied fields.

Apply Set To Field

Apply a reusable set to one field:

ApplyValidationSet(ApplyValidationSetRequest)

Request shape:

profileName: string
tableName: string
dataKey: string
setName: string

Server behavior:

  1. Loads the set.
  2. Loads its ordered rules.
  3. Resolves/merges them through validation-core.
  4. Validates that dataKey exists in the table definition.
  5. Writes the resolved config into existing table_validation_rules.

This is a snapshot. If the user later edits the phone set, fields that already used phone keep their old resolved config until the set is applied again.

FieldValidation Guide

Rules and direct field validation both use FieldValidation.

Required

required: true

Backend rejects missing or empty values.

Limits

limits:
  min: 10
  max: 10
  warnAt: optional
  countMode: CHARS | BYTES | DISPLAY_WIDTH

Backend enforces min and max. warnAt is mainly UI feedback.

Pattern

Pattern rules validate characters at positions.

Example digits-only:

pattern:
  rules:
    - position:
        kind: PATTERN_POSITION_FROM
        start: 0
      constraint:
        kind: CHARACTER_CONSTRAINT_NUMERIC

Useful constraints:

CHARACTER_CONSTRAINT_ALPHABETIC
CHARACTER_CONSTRAINT_NUMERIC
CHARACTER_CONSTRAINT_ALPHANUMERIC
CHARACTER_CONSTRAINT_EXACT
CHARACTER_CONSTRAINT_ONE_OF
CHARACTER_CONSTRAINT_REGEX

Pattern fragments from multiple rules are merged.

Allowed Values

allowed_values:
  values: ["open", "closed"]
  allow_empty: false
  case_insensitive: true

Backend rejects values not in the list.

Mask

mask:
  pattern: "(###) ###-####"
  input_char: "#"
  template_char: "_"

Canvas uses this for display/editing. Backend expects raw values without mask literals.

Formatter

formatter:
  type: "PhoneFormatter"
  options: []
  description: optional

Formatter is resolved client-side. Backend stores it but does not execute it.

External Validation

external_validation_enabled: true

This is a frontend/UI hint. Backend stores it but does not perform external validation.

Rule List

Show all rules for a profile.

Actions:

create rule
edit rule
delete rule
preview rule config

Rule Editor

Build a ValidationRuleDefinition.

Recommended UI:

name
description
required toggle
limits section
pattern section
allowed values section
mask section
formatter section
external validation toggle

For v1, encourage one fragment per rule. Example: create phone-length and digits-only separately, instead of one huge rule.

Set List

Show all sets for a profile.

Use ListValidationSets, because it returns resolvedValidation.

Actions:

create set
edit set
delete set
preview resolved validation

Set Editor

Build a ValidationSetDefinition.

Recommended UI:

name
description
ordered rule picker
resolved preview

When rule ordering changes, call UpsertValidationSet and then refresh ListValidationSets.

Apply Set

On the table/field validation screen, add:

Apply validation set

Flow:

  1. Load sets with ListValidationSets.
  2. User selects a set.
  3. Call ApplyValidationSet(profileName, tableName, dataKey, setName).
  4. Refresh GetTableValidation(profileName, tableName).

The field should now behave exactly like a directly configured field validation.

Example: Phone

Create rule required:

validation:
  required: true

Create rule phone-length:

validation:
  limits:
    min: 10
    max: 10
    countMode: CHARS

Create rule digits-only:

validation:
  pattern:
    rules:
      - position:
          kind: PATTERN_POSITION_FROM
          start: 0
        constraint:
          kind: CHARACTER_CONSTRAINT_NUMERIC

Create rule phone-mask:

validation:
  mask:
    pattern: "(###) ###-####"
    input_char: "#"

Create set phone:

ruleNames:
  - required
  - phone-length
  - digits-only
  - phone-mask

Apply set:

profileName: "default"
tableName: "customers"
dataKey: "customer_phone"
setName: "phone"

Then refresh:

GetTableValidation(default, customers)

The response contains a normal FieldValidation for customer_phone.

Important UX Notes

  • Applying a set is not a live link.
  • Editing a rule or set does not mutate fields where it was already applied.
  • To update a field after set changes, apply the set again.
  • If a set has conflicting singleton rules, the server rejects it.
  • For now, the system does not store field metadata like sourceSetName on applied fields. The field only stores the resolved validation snapshot.

Files

Core model:

validation-core/src/set.rs
validation-core/src/config.rs

Wire contract:

common/proto/table_validation.proto

Server implementation:

server/src/table_validation/get/service.rs
server/src/table_validation/post/repo.rs
server/src/table_validation/config.rs

Storage:

server/migrations/20260506170000_create_validation_rules_and_sets.sql