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>
4.5 KiB
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 sliceNotEmpty()— value cannot be empty/nil (only checks content, not presence)
String
MinLen(n)— at least n charactersMaxLen(n)— at most n charactersOneOfSlice[T](allowed)— value must be in allowed list
Numeric
Min(n)— value >= nMax(n)— value <= n
Format
URL()— valid HTTP/HTTPS URL with hostHTTPSUrl()— HTTPS-only URLDomain()— valid RFC-compliant domain nameGID(entityTypes...)— valid GID, optionally restricted to specific entity types
Security
NoHTML()— rejects HTML tagsPrintableText()— rejects invisible/harmful Unicode (control chars, bidi overrides, zero-width)NoNewLine()— rejects\nand\rSafeText(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 referenceBefore(refTime)— time must be before referenceRangeDuration(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:
- Request struct's
Validate()returnsValidationErrorsornil - Service method checks error before processing
- GraphQL/HTTP handlers convert
ValidationErrorsto 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
}