Files
probo/pkg/server/api/console/v1/graphql/cookie_banner.graphql
Émile Ré 66ffcf3411 Filter banner trackers by linked third party
The trackers list page on the cookie-banner configuration screen needs
to filter rows by the third party that the pattern resolves to. The
tricky part is that "third party" comes from two unrelated tables:
ThirdParty (org-scoped, linked through tracker_patterns.third_party_id)
and CommonThirdParty (global catalog, reached indirectly through
common_tracker_patterns.common_third_party_id). The filter must accept
either flavour of GID and resolve transparently.

Add a single thirdPartyId field to TrackerPatternFilter and dispatch
on the GID's entity-type prefix at the resolver:

  ThirdPartyEntityType       -> WithThirdPartyID
  CommonThirdPartyEntityType -> resolve common_tracker_pattern_id list
                                via the cookiebanner service, then
                                WithCommonTrackerPatternIDs

Any other entity type returns an Invalid error rather than silently
matching everything; an unknown caller-supplied GID is a contract bug.
The empty-but-non-nil ID slice produced when a CommonThirdParty has no
patterns yet correctly yields zero rows because the SQL fragment uses
ANY(...).

To populate the filter combobox, add a new CookieBanner.linkedThirdParties
field returning [TrackerPatternThirdPartyLink!]!, a union of ThirdParty
and CommonThirdParty. The resolver collects DISTINCT third_party_id and
common_tracker_pattern_id from the banner's tracker patterns (no joins,
per coredata convention), then chains the catalog lookup through
CommonTrackerPatterns -> CommonThirdParties. Authorization scopes
follow the existing trackerPattern resolvers: ActionTrackerPatternList
gates the aggregation, ActionThirdPartyGet and ActionCommonThirdPartyGet
each gate their respective fan-out only when that branch has work.

The org-scoped fan-out uses dataloadgen.LoadAll so it batches in a
single round-trip; per-key NotFound errors are dropped (a deleted
third party doesn't fail the whole list), other errors bubble up.

Signed-off-by: Émile Ré <emile@probo.com>
2026-05-29 10:07:29 +02:00

839 lines
21 KiB
GraphQL

enum CookieBannerState
@goModel(model: "go.probo.inc/probo/pkg/coredata.CookieBannerState") {
ACTIVE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieBannerStateActive"
)
INACTIVE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieBannerStateInactive"
)
}
enum CookieConsentMode
@goModel(model: "go.probo.inc/probo/pkg/coredata.CookieConsentMode") {
OPT_IN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieConsentModeOptIn"
)
OPT_OUT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieConsentModeOptOut"
)
}
enum CookieSource
@goModel(model: "go.probo.inc/probo/pkg/coredata.CookieSource") {
SCRIPT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieSourceScript"
)
PRE_EXISTING
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieSourcePreExisting"
)
EXTENSION
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieSourceExtension"
)
}
enum TrackerType
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrackerType") {
COOKIE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerTypeCookie"
)
LOCAL_STORAGE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerTypeLocalStorage"
)
SESSION_STORAGE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerTypeSessionStorage"
)
INDEXED_DB
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerTypeIndexedDB"
)
CACHE_STORAGE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerTypeCacheStorage"
)
}
enum CookieBannerOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CookieBannerOrderField"
) {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieBannerOrderFieldCreatedAt"
)
}
enum CookieCategoryOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CookieCategoryOrderField"
) {
RANK
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieCategoryOrderFieldRank"
)
}
enum CookieCategoryKind
@goModel(
model: "go.probo.inc/probo/pkg/coredata.CookieCategoryKind"
) {
NORMAL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieCategoryKindNormal"
)
NECESSARY
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieCategoryKindNecessary"
)
UNCATEGORISED
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.CookieCategoryKindUncategorised"
)
}
input CookieBannerOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieBannerOrderBy"
) {
direction: OrderDirection!
field: CookieBannerOrderField!
}
input CookieCategoryOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieCategoryOrderBy"
) {
direction: OrderDirection!
field: CookieCategoryOrderField!
}
input CookieCategoryFilter
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieCategoryFilter"
) {
excludeKind: CookieCategoryKind
}
type CookieBanner implements Node {
id: ID!
name: String!
origin: String!
state: CookieBannerState!
privacyPolicyUrl: String
cookiePolicyUrl: String!
consentExpiryDays: Int!
showBranding: Boolean!
defaultLanguage: String!
organization: Organization @goField(forceResolver: true)
categories(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: CookieCategoryOrder
filter: CookieCategoryFilter
): CookieCategoryConnection @goField(forceResolver: true)
translations: [CookieBannerTranslation!]! @goField(forceResolver: true)
latestVersion: CookieBannerVersion @goField(forceResolver: true)
consentRecords(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: CookieConsentRecordOrder
filter: CookieConsentRecordFilter
): CookieConsentRecordConnection @goField(forceResolver: true)
trackerPatterns(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: TrackerPatternOrder
filter: TrackerPatternFilter
): TrackerPatternConnection @goField(forceResolver: true)
"""
The deduped union of third parties referenced by the banner's
tracker patterns, both org-scoped (ThirdParty) and global
(CommonThirdParty). Powers the third-party filter combobox on the
trackers list page.
"""
linkedThirdParties: [TrackerPatternThirdPartyLink!]!
@goField(forceResolver: true)
uncategorisedTrackerResources(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: TrackerResourceOrder
filter: TrackerResourceFilter
): TrackerResourceConnection @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type CookieBannerTranslation implements Node {
id: ID!
language: String!
translations: String!
createdAt: Datetime!
updatedAt: Datetime!
}
type CookieCategory implements Node {
id: ID!
cookieBanner: CookieBanner @goField(forceResolver: true)
name: String!
slug: String!
description: String!
kind: CookieCategoryKind!
rank: Int!
gcmConsentTypes: [String!]!
posthogConsent: Boolean!
trackerPatterns(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: TrackerPatternOrder
): TrackerPatternConnection @goField(forceResolver: true)
trackerResources(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: TrackerResourceOrder
): TrackerResourceConnection @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
enum TrackerPatternMatchType
@goModel(
model: "go.probo.inc/probo/pkg/coredata.TrackerPatternMatchType"
) {
EXACT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternMatchTypeExact"
)
GLOB
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternMatchTypeGlob"
)
}
enum TrackerPatternOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.TrackerPatternOrderField"
) {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternOrderFieldCreatedAt"
)
NAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternOrderFieldName"
)
LAST_MATCHED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternOrderFieldLastMatchedAt"
)
UPDATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternOrderFieldUpdatedAt"
)
SOURCE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerPatternOrderFieldSource"
)
}
type TrackerPattern implements Node
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPattern"
) {
id: ID!
cookieCategory: CookieCategory @goField(forceResolver: true)
trackerType: TrackerType!
pattern: String!
matchType: TrackerPatternMatchType!
displayName: String!
maxAgeSeconds: Int
description: String!
source: CookieSource
excluded: Boolean!
detectedCount: Int! @goField(forceResolver: true)
lastMatchedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
"""
The org-scoped third party this pattern is mapped to, if any. Set
when the mapping worker promoted the pattern to a tenant-managed
ThirdParty record. When this field is non-null, commonThirdParty is
always null.
"""
thirdParty: ThirdParty @goField(forceResolver: true)
"""
The global third party this pattern is mapped to via the common
tracker-pattern catalog. Null when the pattern has its own
org-scoped thirdParty, when it has not been mapped, or when the
matched common pattern has no common third party.
"""
commonThirdParty: CommonThirdParty @goField(forceResolver: true)
detectedTrackers(
first: Int
after: CursorKey
last: Int
before: CursorKey
orderBy: DetectedTrackerOrder
): DetectedTrackerConnection @goField(forceResolver: true)
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type TrackerPatternConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPatternConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [TrackerPatternEdge!]!
pageInfo: PageInfo!
}
"""
A third party associated with a banner's tracker patterns. Either the
tenant-managed ThirdParty (when a pattern was promoted to the org
catalog) or the global CommonThirdParty linked through a common
tracker pattern.
"""
union TrackerPatternThirdPartyLink = ThirdParty | CommonThirdParty
type TrackerPatternEdge {
cursor: CursorKey!
node: TrackerPattern!
}
input TrackerPatternOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPatternOrderBy"
) {
direction: OrderDirection!
field: TrackerPatternOrderField!
}
input TrackerPatternFilter
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerPatternFilter"
) {
query: String
source: CookieSource
trackerType: TrackerType
cookieCategoryId: ID
"""
Filter to tracker patterns linked to a single third party. Accepts
either an org-scoped ThirdParty GID (matched against third_party_id)
or a global CommonThirdParty GID (matched against the indirect link
through common_tracker_patterns). The dispatch happens at the
resolver based on the GID entity-type prefix.
"""
thirdPartyId: ID
}
type DetectedTracker implements Node {
id: ID!
identifier: String!
initiatorUrl: String
maxAgeSeconds: Int
source: CookieSource
lastDetectedAt: Datetime!
createdAt: Datetime!
}
enum DetectedTrackerOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderField"
) {
INITIATOR_URL
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderFieldInitiatorURL"
)
LAST_DETECTED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.DetectedTrackerOrderFieldLastDetectedAt"
)
}
input DetectedTrackerOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.DetectedTrackerOrderBy"
) {
direction: OrderDirection!
field: DetectedTrackerOrderField!
}
type DetectedTrackerConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.DetectedTrackerConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [DetectedTrackerEdge!]!
pageInfo: PageInfo!
}
type DetectedTrackerEdge {
cursor: CursorKey!
node: DetectedTracker!
}
enum TrackerResourceType
@goModel(model: "go.probo.inc/probo/pkg/coredata.TrackerResourceType") {
SCRIPT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeScript"
)
IFRAME
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeIframe"
)
IMAGE
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeImage"
)
STYLESHEET
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeStylesheet"
)
FONT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeFont"
)
BEACON
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeBeacon"
)
FETCH
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeFetch"
)
MEDIA
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeMedia"
)
SERVICE_WORKER
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceTypeServiceWorker"
)
}
enum TrackerResourceOrderField
@goModel(
model: "go.probo.inc/probo/pkg/coredata.TrackerResourceOrderField"
) {
CREATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceOrderFieldCreatedAt"
)
LAST_DETECTED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceOrderFieldLastDetectedAt"
)
ORIGIN
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceOrderFieldOrigin"
)
UPDATED_AT
@goEnum(
value: "go.probo.inc/probo/pkg/coredata.TrackerResourceOrderFieldUpdatedAt"
)
}
type TrackerResource implements Node {
id: ID!
cookieCategory: CookieCategory @goField(forceResolver: true)
type: TrackerResourceType!
origin: String!
path: String!
displayName: String!
description: String!
excluded: Boolean!
lastDetectedAt: Datetime
createdAt: Datetime!
updatedAt: Datetime!
permission(action: String!): Boolean! @goField(forceResolver: true)
}
type TrackerResourceConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerResourceConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [TrackerResourceEdge!]!
pageInfo: PageInfo!
}
type TrackerResourceEdge {
cursor: CursorKey!
node: TrackerResource!
}
input TrackerResourceOrder
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerResourceOrderBy"
) {
direction: OrderDirection!
field: TrackerResourceOrderField!
}
input TrackerResourceFilter
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.TrackerResourceFilter"
) {
query: String
type: TrackerResourceType
}
type CookieBannerVersion implements Node {
id: ID!
version: Int!
state: String!
categories: [CookieBannerVersionCategory!]! @goField(forceResolver: true)
createdAt: Datetime!
updatedAt: Datetime!
}
type CookieBannerVersionCategory {
name: String!
slug: String!
description: String!
kind: CookieCategoryKind!
cookies: [CookieBannerVersionCookie!]!
}
type CookieBannerVersionCookie {
name: String!
maxAgeSeconds: Int
description: String!
}
type CookieBannerConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieBannerConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [CookieBannerEdge!]!
pageInfo: PageInfo!
}
type CookieBannerEdge {
cursor: CursorKey!
node: CookieBanner!
}
type CookieCategoryConnection
@goModel(
model: "go.probo.inc/probo/pkg/server/api/console/v1/types.CookieCategoryConnection"
) {
totalCount: Int! @goField(forceResolver: true)
edges: [CookieCategoryEdge!]!
pageInfo: PageInfo!
}
type CookieCategoryEdge {
cursor: CursorKey!
node: CookieCategory!
}
extend type Mutation {
createCookieBanner(
input: CreateCookieBannerInput!
): CreateCookieBannerPayload!
updateCookieBanner(
input: UpdateCookieBannerInput!
): UpdateCookieBannerPayload!
deleteCookieBanner(
input: DeleteCookieBannerInput!
): DeleteCookieBannerPayload!
activateCookieBanner(
input: ActivateCookieBannerInput!
): ActivateCookieBannerPayload!
deactivateCookieBanner(
input: DeactivateCookieBannerInput!
): DeactivateCookieBannerPayload!
publishCookieBannerVersion(
input: PublishCookieBannerVersionInput!
): PublishCookieBannerVersionPayload!
createCookieCategory(
input: CreateCookieCategoryInput!
): CreateCookieCategoryPayload!
updateCookieCategory(
input: UpdateCookieCategoryInput!
): UpdateCookieCategoryPayload!
deleteCookieCategory(
input: DeleteCookieCategoryInput!
): DeleteCookieCategoryPayload!
reorderCookieCategory(
input: ReorderCookieCategoryInput!
): ReorderCookieCategoryPayload!
upsertCookieBannerTranslation(
input: UpsertCookieBannerTranslationInput!
): UpsertCookieBannerTranslationPayload!
createTrackerPattern(
input: CreateTrackerPatternInput!
): CreateTrackerPatternPayload!
updateTrackerPattern(
input: UpdateTrackerPatternInput!
): UpdateTrackerPatternPayload!
deleteTrackerPattern(
input: DeleteTrackerPatternInput!
): DeleteTrackerPatternPayload!
moveTrackerPatternToCategory(
input: MoveTrackerPatternToCategoryInput!
): MoveTrackerPatternToCategoryPayload!
createTrackerResource(
input: CreateTrackerResourceInput!
): CreateTrackerResourcePayload!
updateTrackerResource(
input: UpdateTrackerResourceInput!
): UpdateTrackerResourcePayload!
deleteTrackerResource(
input: DeleteTrackerResourceInput!
): DeleteTrackerResourcePayload!
moveTrackerResourceToCategory(
input: MoveTrackerResourceToCategoryInput!
): MoveTrackerResourceToCategoryPayload!
}
input CreateCookieBannerInput {
organizationId: ID!
name: String!
origin: String!
privacyPolicyUrl: String
cookiePolicyUrl: String!
consentExpiryDays: Int!
}
input UpdateCookieBannerInput {
cookieBannerId: ID!
name: String
privacyPolicyUrl: String
cookiePolicyUrl: String
consentExpiryDays: Int
defaultLanguage: String
}
input DeleteCookieBannerInput {
cookieBannerId: ID!
}
input ActivateCookieBannerInput {
cookieBannerId: ID!
}
input DeactivateCookieBannerInput {
cookieBannerId: ID!
}
input PublishCookieBannerVersionInput {
cookieBannerId: ID!
}
input CreateCookieCategoryInput {
cookieBannerId: ID!
name: String!
slug: String!
description: String!
rank: Int!
}
input UpdateCookieCategoryInput {
cookieCategoryId: ID!
name: String
slug: String
description: String
gcmConsentTypes: [String!]
posthogConsent: Boolean
}
input DeleteCookieCategoryInput {
cookieCategoryId: ID!
}
input ReorderCookieCategoryInput {
cookieCategoryId: ID!
rank: Int!
}
type CreateCookieBannerPayload {
cookieBannerEdge: CookieBannerEdge!
}
type UpdateCookieBannerPayload {
cookieBanner: CookieBanner!
}
type DeleteCookieBannerPayload {
deletedCookieBannerId: ID!
}
type ActivateCookieBannerPayload {
cookieBanner: CookieBanner!
}
type DeactivateCookieBannerPayload {
cookieBanner: CookieBanner!
}
type PublishCookieBannerVersionPayload {
cookieBannerVersion: CookieBannerVersion!
cookieBanner: CookieBanner!
}
type CreateCookieCategoryPayload {
cookieCategoryEdge: CookieCategoryEdge!
cookieBanner: CookieBanner!
}
type UpdateCookieCategoryPayload {
cookieCategory: CookieCategory!
cookieBanner: CookieBanner!
}
type DeleteCookieCategoryPayload {
deletedCookieCategoryId: ID!
cookieBanner: CookieBanner!
}
type ReorderCookieCategoryPayload {
cookieBanner: CookieBanner!
}
input UpsertCookieBannerTranslationInput {
cookieBannerId: ID!
language: String!
translations: String!
}
type UpsertCookieBannerTranslationPayload {
cookieBannerTranslation: CookieBannerTranslation!
cookieBanner: CookieBanner!
}
input CreateTrackerPatternInput {
cookieCategoryId: ID!
trackerType: TrackerType
pattern: String!
matchType: TrackerPatternMatchType!
displayName: String!
maxAgeSeconds: Int
description: String
}
input UpdateTrackerPatternInput {
trackerPatternId: ID!
maxAgeSeconds: Int @goField(omittable: true)
description: String
excluded: Boolean
}
input DeleteTrackerPatternInput {
trackerPatternId: ID!
}
input MoveTrackerPatternToCategoryInput {
trackerPatternId: ID!
targetCookieCategoryId: ID!
}
type CreateTrackerPatternPayload {
trackerPatternEdge: TrackerPatternEdge!
cookieBanner: CookieBanner!
}
type UpdateTrackerPatternPayload {
trackerPattern: TrackerPattern!
cookieBanner: CookieBanner!
}
type DeleteTrackerPatternPayload {
deletedTrackerPatternId: ID!
cookieBanner: CookieBanner!
}
type MoveTrackerPatternToCategoryPayload {
trackerPattern: TrackerPattern!
cookieBanner: CookieBanner!
}
input CreateTrackerResourceInput {
cookieCategoryId: ID!
type: TrackerResourceType!
origin: String!
path: String!
displayName: String!
description: String
}
input UpdateTrackerResourceInput {
trackerResourceId: ID!
displayName: String
description: String
excluded: Boolean
}
input DeleteTrackerResourceInput {
trackerResourceId: ID!
}
input MoveTrackerResourceToCategoryInput {
trackerResourceId: ID!
targetCookieCategoryId: ID!
}
type CreateTrackerResourcePayload {
trackerResourceEdge: TrackerResourceEdge!
cookieBanner: CookieBanner!
}
type UpdateTrackerResourcePayload {
trackerResource: TrackerResource!
cookieBanner: CookieBanner!
}
type DeleteTrackerResourcePayload {
deletedTrackerResourceId: ID!
cookieBanner: CookieBanner!
}
type MoveTrackerResourceToCategoryPayload {
trackerResource: TrackerResource!
cookieBanner: CookieBanner!
}