Files
probo/contrib/claude/validation.md
Sacha Al Himdani eecbe4c46c Rename vendors to third parties
Renames the user-facing 'vendor' concept to 'third party' across the
entire codebase. The shared common_third_parties reference table is
unchanged.

Migration. Renames the vendor_category enum, the vendors and
vendor_<entity> tables (contacts, services, compliance_reports,
business_associate_agreements, data_privacy_agreements,
risk_assessments) and their vendor_id columns, the asset_vendors /
data_vendors / processing_activity_vendors junction tables,
generated_documents.vendors_document_id, the webhook_event_type
'vendor:<verb>' values, and the snapshots_type 'VENDORS' value.

Backend. Renames coredata models and SQL queries, probo services,
GraphQL / MCP API surface, console / trust / webhook resolvers and
types, the CLI (prb vendor* -> prb third-party*; pkg/cmd/vendormgmt
-> pkg/cmd/thirdpartymgmt), the document generator, vetting agent
prompts, and the common-third-parties-import command.

Frontend, packages, n8n, e2e. Renames apps/console pages, components,
hooks, routes, dialogs, and tabs; the shared @probo/vendors package
(now @probo/third-parties); the @probo/ui Vendors atoms (now
ThirdParties, VendorLogo -> ThirdPartyLogo); the n8n community node
actions/vendor folder (now actions/thirdParty); and the e2e Go test
suite (console and MCP). Filesystem and URL paths use kebab-case
(third-parties), GraphQL fields and TypeScript identifiers use
camelCase (thirdParty / thirdParties), Go types use PascalCase
(ThirdParty), and human-facing text uses 'third party' with a space.

Co-authored-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Bryan Frimin <bryan@getprobo.com>
Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-05-13 21:21:39 +02:00

4.5 KiB

Validation Framework

Custom fluent validation API in pkg/validator/. Used in every service method to validate request structs before processing.

Basic pattern

Create a validator, chain Check() calls for each field, then call Error() to get accumulated errors:

func (req *CreateThirdPartyRequest) Validate() error {
	v := validator.New()

	v.Check(req.OrganizationID, "organization_id",
		validator.Required(),
		validator.GID(coredata.OrganizationEntityType),
	)
	v.Check(req.Name, "name",
		validator.Required(),
		validator.SafeTextNoNewLine(TitleMaxLength),
	)
	v.Check(req.Category, "category",
		validator.OneOfSlice(coredata.ThirdPartyCategories()),
	)

	return v.Error()
}

Check(value, fieldName, validators...) runs validators sequentially on a value. Multiple Check() calls accumulate all errors. Error() returns nil if clean, or ValidationErrors (which implements error).

Available validators

Common

  • Required() — value must not be nil, empty string, or empty slice
  • NotEmpty() — value cannot be empty/nil (only checks content, not presence)

String

  • MinLen(n) — at least n characters
  • MaxLen(n) — at most n characters
  • OneOfSlice[T](allowed) — value must be in allowed list

Numeric

  • Min(n) — value >= n
  • Max(n) — value <= n

Format

  • URL() — valid HTTP/HTTPS URL with host
  • HTTPSUrl() — HTTPS-only URL
  • Domain() — valid RFC-compliant domain name
  • GID(entityTypes...) — valid GID, optionally restricted to specific entity types

Security

  • NoHTML() — rejects HTML tags
  • PrintableText() — rejects invisible/harmful Unicode (control chars, bidi overrides, zero-width)
  • NoNewLine() — rejects \n and \r
  • SafeText(maxLen) — combines NotEmpty + MaxLen + NoHTML + PrintableText (allows newlines)
  • SafeTextNoNewLine(maxLen) — same as SafeText but also rejects newlines (for single-line fields)

Time

  • After(refTime) — time must be after reference
  • Before(refTime) — time must be before reference
  • RangeDuration(min, max) — duration between min and max inclusive

Pointer handling

The framework automatically dereferences pointers at any level. Nil pointers pass all non-Required validators:

v.Check(stringValue, "field", validator.Required())    // Direct value
v.Check(&stringValue, "field", validator.Required())   // Pointer — auto-dereferenced
v.Check(nilPointer, "field", validator.MinLen(5))      // Nil passes (not Required)
v.Check(nilPointer, "field", validator.Required())     // Nil fails Required

Collection validation

Use CheckEach to validate each item in a slice:

v.CheckEach(ids, "ids", func(index int, item any) {
	gidValue := item.(gid.GID)
	v.Check(gidValue, fmt.Sprintf("ids[%d]", index),
		validator.Required(),
		validator.GID(coredata.ThirdPartyEntityType),
	)
})

Nil or empty slices are silently skipped.

Error types

type ValidationError struct {
	Field   string    // e.g. "email"
	Code    ErrorCode // e.g. ErrorCodeRequired
	Message string    // human-readable
	Value   any       // the problematic value
}

Error codes:

Code Meaning
REQUIRED Field is missing or empty
INVALID_FORMAT Value does not match expected format
OUT_OF_RANGE Numeric value outside bounds
TOO_SHORT String below minimum length
TOO_LONG String above maximum length
INVALID_EMAIL Invalid email address
INVALID_URL Invalid URL
INVALID_ENUM Value not in allowed set
INVALID_GID Invalid GID or wrong entity type
UNSAFE_CONTENT HTML, control chars, or harmful Unicode
CUSTOM Custom validation error

ValidationErrors is a slice with query methods:

errs := err.(validator.ValidationErrors)
errs.HasErrors()           // bool
errs.Fields()              // unique field names
errs.ByField("name")       // filter by field
errs.ByCode(ErrorCodeRequired) // filter by code
errs.First()               // first error

Error propagation

Validation errors flow naturally through Go's error interface:

  1. Request struct's Validate() returns ValidationErrors or nil
  2. Service method checks error before processing
  3. GraphQL/HTTP handlers convert ValidationErrors to appropriate response format
func (s *Service) CreateThirdParty(ctx context.Context, req CreateThirdPartyRequest) (*coredata.ThirdParty, error) {
	if err := req.Validate(); err != nil {
		return nil, err
	}
	// proceed with business logic
}