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>
7.3 KiB
GraphQL (Go Backend — gqlgen)
Schema-first GraphQL using gqlgen. The schema is hand-written and split into per-entity files under graphql/; Go types and resolvers are generated.
Schema file organization
Each API's schema lives in pkg/server/api/{api}/v1/graphql/ as multiple .graphql files, one per coredata model:
base.graphql— directives, scalars, Node interface, PageInfo, OrderDirection, root Query/Mutation/Organization types- Entity files (e.g.,
thirdParty.graphql,control.graphql) — useextend type Mutationto add their mutations.
gqlgen's follow-schema layout generates one resolver file per schema file (e.g., thirdParty.resolvers.go). Types that get extended across files (Organization, Mutation, Viewer, TrustCenter) must be defined in base.graphql.
extend type restrictions
The only permitted use of extend type is extend type Mutation. Never use extend type on any other type — not on entity types, not on Organization, not on Query. If CookieBanner needs a consentRecords connection, add the field directly to the CookieBanner type definition in cookie_banner.graphql — do not write extend type CookieBanner in another file. This keeps each entity's full field set visible in one place and avoids resolver mis-routing across generated files.
Connection types and @goModel
Always define a custom Go type for connection types using the @goModel directive. The model path points to the types package for the relevant API. The totalCount field must use @goField(forceResolver: true). Edge types do not need @goModel.
type ThirdPartyConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.ThirdPartyConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [ThirdPartyEdge!]!
pageInfo: PageInfo!
}
type ThirdPartyEdge {
cursor: CursorKey!
node: ThirdParty!
}
Without @goModel, gqlgen generates a default struct that lacks the custom fields (ParentID, Resolver, Filter) needed by the pagination resolvers.
Enums and @goModel / @goEnum
Map GraphQL enums to existing Go types using @goModel on the enum and @goEnum on each value:
enum ThirdPartyOrderField
@goModel(model: "go.probo.inc/probo/pkg/coredata.ThirdPartyOrderField") {
CREATED_AT
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ThirdPartyOrderFieldCreatedAt")
NAME
@goEnum(value: "go.probo.inc/probo/pkg/coredata.ThirdPartyOrderFieldName")
}
Schema directives
| Directive | Target | Purpose |
|---|---|---|
@goModel(model: "...") |
OBJECT, ENUM, INPUT_OBJECT, SCALAR, INTERFACE, UNION |
Map GraphQL type to existing Go type |
@goEnum(value: "...") |
ENUM_VALUE |
Map enum value to Go constant |
@goField(forceResolver: true) |
FIELD_DEFINITION |
Force a resolver function instead of struct field |
@goField(name: "...") |
FIELD_DEFINITION, INPUT_FIELD_DEFINITION |
Override Go field name |
@goField(omittable: true) |
INPUT_FIELD_DEFINITION |
Use graphql.Omittable[T] for distinguishing null vs absent |
Cursor pagination schema types
Every paginated field uses shared base types plus entity-specific types:
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: CursorKey
endCursor: CursorKey
}
enum OrderDirection
@goModel(model: "go.probo.inc/probo/pkg/page.OrderDirection") {
ASC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionAsc")
DESC @goEnum(value: "go.probo.inc/probo/pkg/page.OrderDirectionDesc")
}
Each entity defines: enum XxxOrderField, input XxxOrder, type XxxConnection (with @goModel), type XxxEdge.
Connection fields on parent types use standard Relay arguments:
type Organization {
thirdParties(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: ThirdPartyOrder
filter: ThirdPartyFilter
): ThirdPartyConnection!
}
Go connection type pattern
Each connection type lives in types/*_connection.go and follows this structure:
type (
ThirdPartyOrderBy OrderBy[coredata.ThirdPartyOrderField]
ThirdPartyConnection struct {
TotalCount int
Edges []*ThirdPartyEdge
PageInfo PageInfo
Resolver any
ParentID gid.GID
}
)
func NewThirdPartyConnection(
p *page.Page[*coredata.ThirdParty, coredata.ThirdPartyOrderField],
parentType any,
parentID gid.GID,
) *ThirdPartyConnection {
edges := make([]*ThirdPartyEdge, len(p.Data))
for i, v := range p.Data {
edges[i] = NewThirdPartyEdge(v, p.Cursor.OrderBy.Field)
}
return &ThirdPartyConnection{
Edges: edges,
PageInfo: *NewPageInfo(p),
Resolver: parentType,
ParentID: parentID,
}
}
func NewThirdPartyEdge(
v *coredata.ThirdParty,
orderBy coredata.ThirdPartyOrderField,
) *ThirdPartyEdge {
return &ThirdPartyEdge{
Cursor: v.CursorKey(orderBy),
Node: NewThirdParty(v),
}
}
Cursor format
Cursors are opaque CursorKey scalars. Internally they encode as base64url(JSON):
["<entity_global_id>", <sort_field_value>]
This enables keyset pagination — the database seeks directly to the right position instead of using OFFSET.
Keyset pagination
The database query uses the cursor to build a WHERE clause:
DESC: rows where(field <= cursor_value) AND NOT (field = cursor_value AND id > cursor_id)ASC: rows where(field >= cursor_value) AND NOT (field = cursor_value AND id < cursor_id)
The query fetches size + 1 (or size + 2 with a cursor) rows to detect whether more pages exist. NewPage trims extra rows and sets hasNextPage / hasPreviousPage.
For backward pagination (last / before), SQL sort direction is reversed, then the result slice is reversed back.
Default page size is 25 when neither first nor last is provided.
Adding a new paginated field — checklist
- Schema — add
enum XxxOrderField(with@goModel/@goEnum),input XxxOrder,type XxxConnection(with@goModelandtotalCountusing@goField(forceResolver: true)),type XxxEdge, and the connection field with Relay arguments on the parent type - Coredata — add
*_order_field.go(withColumn(),IsValid(), marshaling),CursorKey(field)method on the entity, and theLoadAllBy* query using cursor SQL fragments +page.NewPage() - API types — add
*_connection.gowithOrderByalias, connection struct,NewXxxConnection,NewXxxEdge - Resolver — implement the resolver (authorize, build order, build cursor, call service, build connection)
- Codegen — run
go generatefor the relevant API package