Let n8n create and update campaigns with scope sources

Replace the separate Add Source operation with multi-select source
fields on create and update. Create passes accessReviewSourceIds to
the existing GraphQL input; update gains the same omittable field and
backend source sync so workflows can configure sources in one step.

Load source options from the organization in the n8n UI, and keep
surfacing INVALID errors when start fails for missing sources.

Signed-off-by: Cursor Agent <cursoragent@cursor.com>

Co-authored-by: Bryan FRIMIN <bryan@frimin.fr>
This commit is contained in:
Cursor Agent
2026-07-20 15:37:36 +00:00
parent 2d93b1d793
commit ffaf637397
13 changed files with 335 additions and 107 deletions

View File

@@ -171,6 +171,12 @@ func (s *Service) UpdateCampaign(
return fmt.Errorf("cannot update campaign: %w", err)
}
if req.AccessReviewSourceIDs != nil {
if err := s.syncCampaignSources(ctx, conn, scope, campaign, *req.AccessReviewSourceIDs); err != nil {
return err
}
}
return nil
},
)
@@ -294,6 +300,77 @@ func (s *Service) RemoveCampaignSource(
return campaign, nil
}
func (s *Service) syncCampaignSources(
ctx context.Context,
conn pg.Tx,
scope coredata.Scoper,
campaign *coredata.AccessReviewCampaign,
sourceIDs []gid.GID,
) error {
var campaignSources coredata.AccessReviewCampaignSources
if err := campaignSources.LoadByCampaignID(ctx, conn, scope, campaign.ID); err != nil {
return fmt.Errorf("cannot load campaign sources: %w", err)
}
existingSourceIDs := make([]gid.GID, 0, len(campaignSources))
for _, campaignSource := range campaignSources {
if campaignSource.AccessReviewSourceID != nil {
existingSourceIDs = append(existingSourceIDs, *campaignSource.AccessReviewSourceID)
}
}
for _, sourceID := range sourceIDs {
if containsGID(existingSourceIDs, sourceID) {
continue
}
source := &coredata.AccessReviewSource{}
if err := source.LoadByID(ctx, conn, scope, sourceID); err != nil {
return fmt.Errorf("cannot load access source %s: %w", sourceID, err)
}
if source.OrganizationID != campaign.OrganizationID {
return fmt.Errorf(
"cannot update campaign: access source %s does not belong to the same organization",
sourceID,
)
}
if err := s.upsertCampaignSource(ctx, conn, scope, campaign.ID, source); err != nil {
return fmt.Errorf("cannot snapshot scope source: %w", err)
}
}
for _, existingSourceID := range existingSourceIDs {
if containsGID(sourceIDs, existingSourceID) {
continue
}
campaignSource := &coredata.AccessReviewCampaignSource{}
if err := campaignSource.DeleteByCampaignIDAndAccessReviewSourceID(
ctx,
conn,
scope,
campaign.ID,
existingSourceID,
); err != nil {
return fmt.Errorf("cannot delete campaign source: %w", err)
}
}
return nil
}
func containsGID(ids []gid.GID, id gid.GID) bool {
for _, candidate := range ids {
if candidate == id {
return true
}
}
return false
}
func (s *Service) StartCampaign(
ctx context.Context,
scope coredata.Scoper,

View File

@@ -37,9 +37,10 @@ type (
}
UpdateAccessReviewCampaignRequest struct {
CampaignID gid.GID
Name **string
Description **string
CampaignID gid.GID
Name **string
Description **string
AccessReviewSourceIDs *[]gid.GID
}
AddCampaignSourceRequest struct {

View File

@@ -746,9 +746,10 @@ func (r *mutationResolver) UpdateAccessReviewCampaign(ctx context.Context, input
ctx,
scope,
accessreview.UpdateAccessReviewCampaignRequest{
CampaignID: input.AccessReviewCampaignID,
Name: gqlutils.UnwrapOmittable(input.Name),
Description: gqlutils.UnwrapOmittable(input.Description),
CampaignID: input.AccessReviewCampaignID,
Name: gqlutils.UnwrapOmittable(input.Name),
Description: gqlutils.UnwrapOmittable(input.Description),
AccessReviewSourceIDs: gqlutils.UnwrapOmittable(input.AccessReviewSourceIds),
},
)
if err != nil {
@@ -756,6 +757,10 @@ func (r *mutationResolver) UpdateAccessReviewCampaign(ctx context.Context, input
return nil, gqlutils.NotFound(ctx, err)
}
if strings.HasPrefix(err.Error(), "cannot update campaign:") {
return nil, gqlutils.Invalid(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot update access review campaign", log.Error(err))
return nil, gqlutils.Internal(ctx)

View File

@@ -588,6 +588,7 @@ input UpdateAccessReviewCampaignInput {
accessReviewCampaignId: ID!
name: String @goField(omittable: true)
description: String @goField(omittable: true)
accessReviewSourceIds: [ID!] @goField(omittable: true)
}
type UpdateAccessReviewCampaignPayload {