Refactor access review campaign source API
Expose campaign sources as first-class nodes, paginate fetch attempts instead of denormalized status fields, and bind entries to their campaign snapshot. Update GraphQL, MCP, CLI, console, and e2e coverage to match. Signed-off-by: Bryan Frimin <bryan@probo.com>
This commit is contained in:
@@ -26,7 +26,7 @@ import (
|
||||
const decideMutation = `
|
||||
mutation($input: RecordAccessReviewEntryDecisionInput!) {
|
||||
recordAccessReviewEntryDecision(input: $input) {
|
||||
accessEntry {
|
||||
accessReviewEntry {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
@@ -47,7 +47,7 @@ type decideResponse struct {
|
||||
Decision string `json:"decision"`
|
||||
DecisionNote *string `json:"decisionNote"`
|
||||
DecidedAt *string `json:"decidedAt"`
|
||||
} `json:"accessEntry"`
|
||||
} `json:"accessReviewEntry"`
|
||||
} `json:"recordAccessReviewEntryDecision"`
|
||||
}
|
||||
|
||||
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
const decideAllMutation = `
|
||||
mutation($input: RecordAccessReviewEntryDecisionsInput!) {
|
||||
recordAccessReviewEntryDecisions(input: $input) {
|
||||
accessEntries {
|
||||
accessReviewEntries {
|
||||
id
|
||||
email
|
||||
decision
|
||||
@@ -41,7 +41,7 @@ type decideAllResponse struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
Decision string `json:"decision"`
|
||||
} `json:"accessEntries"`
|
||||
} `json:"accessReviewEntries"`
|
||||
} `json:"recordAccessReviewEntryDecisions"`
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,6 @@ query(
|
||||
$first: Int,
|
||||
$after: CursorKey,
|
||||
$orderBy: AccessReviewEntryOrder,
|
||||
$campaignSourceId: ID,
|
||||
$filter: AccessReviewEntryFilter
|
||||
) {
|
||||
node(id: $id) {
|
||||
@@ -40,7 +39,6 @@ query(
|
||||
first: $first,
|
||||
after: $after,
|
||||
orderBy: $orderBy,
|
||||
campaignSourceId: $campaignSourceId,
|
||||
filter: $filter
|
||||
) {
|
||||
totalCount
|
||||
@@ -63,8 +61,60 @@ query(
|
||||
flagReasons
|
||||
decision
|
||||
decisionNote
|
||||
accessSource {
|
||||
id
|
||||
campaignSource {
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
}
|
||||
}
|
||||
pageInfo {
|
||||
hasNextPage
|
||||
endCursor
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
`
|
||||
|
||||
const listBySourceQuery = `
|
||||
query(
|
||||
$id: ID!,
|
||||
$first: Int,
|
||||
$after: CursorKey,
|
||||
$orderBy: AccessReviewEntryOrder,
|
||||
$filter: AccessReviewEntryFilter
|
||||
) {
|
||||
node(id: $id) {
|
||||
__typename
|
||||
... on AccessReviewCampaignSource {
|
||||
entries(
|
||||
first: $first,
|
||||
after: $after,
|
||||
orderBy: $orderBy,
|
||||
filter: $filter
|
||||
) {
|
||||
totalCount
|
||||
edges {
|
||||
node {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
role
|
||||
jobTitle
|
||||
isAdmin
|
||||
active
|
||||
mfaStatus
|
||||
authMethod
|
||||
accountType
|
||||
lastLogin
|
||||
externalId
|
||||
incrementalTag
|
||||
flags
|
||||
flagReasons
|
||||
decision
|
||||
decisionNote
|
||||
campaignSource {
|
||||
name
|
||||
}
|
||||
createdAt
|
||||
@@ -81,27 +131,26 @@ query(
|
||||
`
|
||||
|
||||
type entryNode struct {
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Role string `json:"role"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Active *bool `json:"active"`
|
||||
MfaStatus string `json:"mfaStatus"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
AccountType string `json:"accountType"`
|
||||
LastLogin *string `json:"lastLogin"`
|
||||
ExternalID string `json:"externalId"`
|
||||
IncrementalTag string `json:"incrementalTag"`
|
||||
Flags []string `json:"flags"`
|
||||
FlagReasons []string `json:"flagReasons"`
|
||||
Decision string `json:"decision"`
|
||||
DecisionNote *string `json:"decisionNote"`
|
||||
AccessReviewSource struct {
|
||||
ID string `json:"id"`
|
||||
ID string `json:"id"`
|
||||
Email string `json:"email"`
|
||||
FullName string `json:"fullName"`
|
||||
Role string `json:"role"`
|
||||
JobTitle string `json:"jobTitle"`
|
||||
IsAdmin bool `json:"isAdmin"`
|
||||
Active *bool `json:"active"`
|
||||
MfaStatus string `json:"mfaStatus"`
|
||||
AuthMethod string `json:"authMethod"`
|
||||
AccountType string `json:"accountType"`
|
||||
LastLogin *string `json:"lastLogin"`
|
||||
ExternalID string `json:"externalId"`
|
||||
IncrementalTag string `json:"incrementalTag"`
|
||||
Flags []string `json:"flags"`
|
||||
FlagReasons []string `json:"flagReasons"`
|
||||
Decision string `json:"decision"`
|
||||
DecisionNote *string `json:"decisionNote"`
|
||||
CampaignSource struct {
|
||||
Name string `json:"name"`
|
||||
} `json:"accessSource"`
|
||||
} `json:"campaignSource"`
|
||||
CreatedAt string `json:"createdAt"`
|
||||
}
|
||||
|
||||
@@ -122,14 +171,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
)
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "list <campaign-id>",
|
||||
Short: "List access entries for a campaign",
|
||||
Args: cobra.ExactArgs(1),
|
||||
Use: "list [<campaign-id>]",
|
||||
Short: "List access entries for a campaign or campaign source",
|
||||
Args: cobra.MaximumNArgs(1),
|
||||
Example: ` # List all entries for a campaign
|
||||
prb access-review entry list <campaign-id>
|
||||
|
||||
# List entries for a specific source
|
||||
prb access-review entry list <campaign-id> --campaign-source-id <source-id>
|
||||
# List entries for a specific campaign source
|
||||
prb access-review entry list --source-id <campaign-source-id>
|
||||
|
||||
# List only pending entries
|
||||
prb access-review entry list <campaign-id> --decision PENDING
|
||||
@@ -141,6 +190,10 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
return err
|
||||
}
|
||||
|
||||
if flagCampaignSourceID == "" && len(args) == 0 {
|
||||
return fmt.Errorf("campaign ID is required when --source-id is not set")
|
||||
}
|
||||
|
||||
cfg, err := f.Config()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -159,8 +212,14 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
cmdutil.TokenRefreshOption(cfg, host, hc),
|
||||
)
|
||||
|
||||
variables := map[string]any{
|
||||
"id": args[0],
|
||||
variables := map[string]any{}
|
||||
|
||||
if len(args) > 0 {
|
||||
variables["id"] = args[0]
|
||||
}
|
||||
|
||||
if flagCampaignSourceID != "" {
|
||||
variables["id"] = flagCampaignSourceID
|
||||
}
|
||||
|
||||
if err := cmdutil.ValidateEnum("order-direction", flagOrderDir, []string{"ASC", "DESC"}); err != nil {
|
||||
@@ -178,10 +237,6 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
}
|
||||
|
||||
if flagCampaignSourceID != "" {
|
||||
variables["campaignSourceId"] = flagCampaignSourceID
|
||||
}
|
||||
|
||||
filter := map[string]any{}
|
||||
|
||||
if flagDecision != "" {
|
||||
@@ -261,9 +316,19 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
variables["filter"] = filter
|
||||
}
|
||||
|
||||
query := listQuery
|
||||
expectedTypename := "AccessReviewCampaign"
|
||||
notFoundLabel := "campaign"
|
||||
|
||||
if flagCampaignSourceID != "" {
|
||||
query = listBySourceQuery
|
||||
expectedTypename = "AccessReviewCampaignSource"
|
||||
notFoundLabel = "campaign source"
|
||||
}
|
||||
|
||||
entries, totalCount, err := api.Paginate(
|
||||
client,
|
||||
listQuery,
|
||||
query,
|
||||
variables,
|
||||
flagLimit,
|
||||
func(data json.RawMessage) (*api.Connection[entryNode], error) {
|
||||
@@ -278,11 +343,11 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
}
|
||||
|
||||
if resp.Node == nil {
|
||||
return nil, fmt.Errorf("campaign %s not found", args[0])
|
||||
return nil, fmt.Errorf("%s %s not found", notFoundLabel, variables["id"])
|
||||
}
|
||||
|
||||
if resp.Node.Typename != "AccessReviewCampaign" {
|
||||
return nil, fmt.Errorf("expected AccessReviewCampaign node, got %s", resp.Node.Typename)
|
||||
if resp.Node.Typename != expectedTypename {
|
||||
return nil, fmt.Errorf("expected %s node, got %s", expectedTypename, resp.Node.Typename)
|
||||
}
|
||||
|
||||
return &resp.Node.Entries, nil
|
||||
@@ -326,7 +391,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
e.ID,
|
||||
e.Email,
|
||||
e.FullName,
|
||||
e.AccessReviewSource.Name,
|
||||
e.CampaignSource.Name,
|
||||
e.Decision,
|
||||
strings.Join(e.Flags, ","),
|
||||
admin,
|
||||
@@ -354,7 +419,7 @@ func NewCmdList(f *cmdutil.Factory) *cobra.Command {
|
||||
cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of entries to list")
|
||||
cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)")
|
||||
cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)")
|
||||
cmd.Flags().StringVar(&flagCampaignSourceID, "source-id", "", "Filter by access source ID")
|
||||
cmd.Flags().StringVar(&flagCampaignSourceID, "source-id", "", "Campaign source ID to list entries for")
|
||||
cmd.Flags().StringVar(&flagDecision, "decision", "", "Filter by decision (PENDING, APPROVED, REVOKE, DEFER, ESCALATE)")
|
||||
cmd.Flags().StringVar(&flagFlag, "flag", "", "Filter by flag (NONE, ORPHANED, INACTIVE, EXCESSIVE, ROLE_MISMATCH, NEW)")
|
||||
cmd.Flags().StringVar(&flagIncTag, "incremental-tag", "", "Filter by incremental tag (NEW, REMOVED, UNCHANGED)")
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
const flagMutation = `
|
||||
mutation($input: FlagAccessReviewEntryInput!) {
|
||||
flagAccessReviewEntry(input: $input) {
|
||||
accessEntry {
|
||||
accessReviewEntry {
|
||||
id
|
||||
email
|
||||
fullName
|
||||
@@ -48,7 +48,7 @@ type flagResponse struct {
|
||||
Flags []string `json:"flags"`
|
||||
FlagReasons []string `json:"flagReasons"`
|
||||
Decision string `json:"decision"`
|
||||
} `json:"accessEntry"`
|
||||
} `json:"accessReviewEntry"`
|
||||
} `json:"flagAccessReviewEntry"`
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
const createMutation = `
|
||||
mutation($input: CreateAccessReviewSourceInput!) {
|
||||
createAccessReviewSource(input: $input) {
|
||||
accessSourceEdge {
|
||||
accessReviewSourceEdge {
|
||||
node {
|
||||
id
|
||||
name
|
||||
@@ -44,7 +44,7 @@ type createResponse struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"node"`
|
||||
} `json:"accessSourceEdge"`
|
||||
} `json:"accessReviewSourceEdge"`
|
||||
} `json:"createAccessReviewSource"`
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@ import (
|
||||
const updateMutation = `
|
||||
mutation($input: UpdateAccessReviewSourceInput!) {
|
||||
updateAccessReviewSource(input: $input) {
|
||||
accessSource {
|
||||
accessReviewSource {
|
||||
id
|
||||
name
|
||||
}
|
||||
@@ -40,7 +40,7 @@ type updateResponse struct {
|
||||
AccessReviewSource struct {
|
||||
ID string `json:"id"`
|
||||
Name string `json:"name"`
|
||||
} `json:"accessSource"`
|
||||
} `json:"accessReviewSource"`
|
||||
} `json:"updateAccessReviewSource"`
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user