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>
145 lines
4.5 KiB
Markdown
145 lines
4.5 KiB
Markdown
# 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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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:
|
|
|
|
```go
|
|
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
|
|
|
|
```go
|
|
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
|
|
}
|
|
```
|