diff --git a/canvas b/canvas index e76cda8..a4f0216 160000 --- a/canvas +++ b/canvas @@ -1 +1 @@ -Subproject commit e76cda8e0c0ef2ef3e59525ebfbe70a56ba16b71 +Subproject commit a4f0216878c2b1a2fc662b2d93f38d2ffe0907ca diff --git a/server b/server index 93ceaf4..99bc97f 160000 --- a/server +++ b/server @@ -1 +1 @@ -Subproject commit 93ceaf42677e7c42be22c077fa9e22457c38c9a6 +Subproject commit 99bc97f771827f608491d0e18db3575a0c1c40dd diff --git a/validation-core/docs/architecture/validation.md b/validation-core/docs/architecture/validation.md deleted file mode 100644 index 88ba62f..0000000 --- a/validation-core/docs/architecture/validation.md +++ /dev/null @@ -1,82 +0,0 @@ -# Validation Architecture - -Validation is split into three ownership layers. - -```mermaid -flowchart LR - Server[server
stores simple settings
binds table fields
enforces writes] - Core[validation-core
owns meaning
resolves sets
runs pure validation] - Canvas[canvas
editor integration
masking while typing
UI feedback] - Common[common/proto
wire format] - - Server --> Core - Canvas --> Core - Server --> Common - Canvas --> Common -``` - -## Rule - -`server` stores dumb, serializable settings. `validation-core` owns what those -settings mean. `canvas` uses the resolved result for editing behavior. - -```mermaid -flowchart TD - Settings[ValidationSettings
serializable data] - Rule[ValidationRule
named reusable fragment] - Set[ValidationSet
ordered rules] - Config[ValidationConfig
resolved runtime config] - Result[ValidationResult] - - Rule --> Settings - Set --> Rule - Set --> Settings - Settings --> Config - Config --> Result -``` - -## Current Data Flow - -```mermaid -sequenceDiagram - participant DB as server DB - participant Server as server - participant Core as validation-core - participant Client as client/canvas - - DB->>Server: stored field validation settings - Server->>Core: interpret shared validation primitives - Server->>Client: gRPC validation config - Client->>Core: resolve/use shared primitives - Client->>Client: canvas editing, masks, errors -``` - -## Set Flow - -```mermaid -flowchart LR - RuleA[digits-only rule] - RuleB[phone-length rule] - RuleC[phone-mask rule] - Set[phone set] - Assignment[column assignment] - Stored[server stored settings
set name + resolved config] - Runtime[server/canvas runtime] - - RuleA --> Set - RuleB --> Set - RuleC --> Set - Set --> Assignment - Assignment --> Stored - Stored --> Runtime -``` - -The server stores reusable rules and sets, and field application stores a -resolved snapshot: - -```text -field customer_phone uses set phone -resolved settings = {...} -``` - -That keeps backend enforcement stable even if the reusable set changes later. diff --git a/validation-core/docs/validation_architecture.md b/validation-core/docs/validation_architecture.md new file mode 100644 index 0000000..f064b03 --- /dev/null +++ b/validation-core/docs/validation_architecture.md @@ -0,0 +1,493 @@ +# 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 + +```mermaid +flowchart LR + Core[validation-core
validation meaning
rule/set merge rules] + Server[server
stores rules/sets
applies sets
enforces writes] + Common[common/proto
gRPC contract] + Client[client/frontend
rule/set UI
calls gRPC] + Canvas[canvas
field editing
mask display
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 + +```mermaid +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: + +```text +UpsertValidationRule(UpsertValidationRuleRequest) +``` + +Request shape: + +```text +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: + +```text +ListValidationRules({ profileName }) +``` + +Delete rule: + +```text +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: + +```text +UpsertValidationSet(UpsertValidationSetRequest) +``` + +Request shape: + +```text +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: + +```text +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: + +```text +ListValidationSets({ profileName }) +``` + +Response includes each set plus `resolvedValidation`, so the frontend can show +what the set expands to. + +Delete set: + +```text +DeleteValidationSet({ profileName, name }) +``` + +Deleting a set does not change already applied fields. + +### Apply Set To Field + +Apply a reusable set to one field: + +```text +ApplyValidationSet(ApplyValidationSetRequest) +``` + +Request shape: + +```text +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 + +```text +required: true +``` + +Backend rejects missing or empty values. + +### Limits + +```text +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: + +```text +pattern: + rules: + - position: + kind: PATTERN_POSITION_FROM + start: 0 + constraint: + kind: CHARACTER_CONSTRAINT_NUMERIC +``` + +Useful constraints: + +```text +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 + +```text +allowed_values: + values: ["open", "closed"] + allow_empty: false + case_insensitive: true +``` + +Backend rejects values not in the list. + +### Mask + +```text +mask: + pattern: "(###) ###-####" + input_char: "#" + template_char: "_" +``` + +Canvas uses this for display/editing. Backend expects raw values without mask +literals. + +### Formatter + +```text +formatter: + type: "PhoneFormatter" + options: [] + description: optional +``` + +Formatter is resolved client-side. Backend stores it but does not execute it. + +### External Validation + +```text +external_validation_enabled: true +``` + +This is a frontend/UI hint. Backend stores it but does not perform external +validation. + +## Recommended Frontend Screens + +### Rule List + +Show all rules for a profile. + +Actions: + +```text +create rule +edit rule +delete rule +preview rule config +``` + +### Rule Editor + +Build a `ValidationRuleDefinition`. + +Recommended UI: + +```text +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: + +```text +create set +edit set +delete set +preview resolved validation +``` + +### Set Editor + +Build a `ValidationSetDefinition`. + +Recommended UI: + +```text +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: + +```text +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`: + +```text +validation: + required: true +``` + +Create rule `phone-length`: + +```text +validation: + limits: + min: 10 + max: 10 + countMode: CHARS +``` + +Create rule `digits-only`: + +```text +validation: + pattern: + rules: + - position: + kind: PATTERN_POSITION_FROM + start: 0 + constraint: + kind: CHARACTER_CONSTRAINT_NUMERIC +``` + +Create rule `phone-mask`: + +```text +validation: + mask: + pattern: "(###) ###-####" + input_char: "#" +``` + +Create set `phone`: + +```text +ruleNames: + - required + - phone-length + - digits-only + - phone-mask +``` + +Apply set: + +```text +profileName: "default" +tableName: "customers" +dataKey: "customer_phone" +setName: "phone" +``` + +Then refresh: + +```text +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: + +```text +validation-core/src/set.rs +validation-core/src/config.rs +``` + +Wire contract: + +```text +common/proto/table_validation.proto +``` + +Server implementation: + +```text +server/src/table_validation/get/service.rs +server/src/table_validation/post/repo.rs +server/src/table_validation/config.rs +``` + +Storage: + +```text +server/migrations/20260506170000_create_validation_rules_and_sets.sql +```