From ffaf63739742238976da16571de964c866cbd17d Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Mon, 20 Jul 2026 15:37:36 +0000 Subject: [PATCH] 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 Co-authored-by: Bryan FRIMIN --- e2e/console/access_review_test.go | 53 +++++++++ .../n8n-node/nodes/Probo/GenericFunctions.ts | 11 +- packages/n8n-node/nodes/Probo/Probo.node.ts | 4 + .../accessReview/addSource.operation.ts | 87 --------------- .../actions/accessReview/create.operation.ts | 4 + .../nodes/Probo/actions/accessReview/index.ts | 9 -- .../loadAccessReviewSourceOptions.ts | 104 ++++++++++++++++++ .../actions/accessReview/sources.fields.ts | 51 +++++++++ .../actions/accessReview/update.operation.ts | 23 +++- pkg/accessreview/campaign_service.go | 77 +++++++++++++ pkg/accessreview/campaign_types.go | 7 +- .../v1/access_review_campaign_resolvers.go | 11 +- .../v1/graphql/access_review_campaign.graphql | 1 + 13 files changed, 335 insertions(+), 107 deletions(-) delete mode 100644 packages/n8n-node/nodes/Probo/actions/accessReview/addSource.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/accessReview/loadAccessReviewSourceOptions.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/accessReview/sources.fields.ts diff --git a/e2e/console/access_review_test.go b/e2e/console/access_review_test.go index 529081748..918f930c3 100644 --- a/e2e/console/access_review_test.go +++ b/e2e/console/access_review_test.go @@ -396,6 +396,59 @@ func TestAccessReviewCampaign_Update(t *testing.T) { assert.Equal(t, "Renamed Campaign", result.UpdateAccessReviewCampaign.AccessReviewCampaign.Name) } +func TestAccessReviewCampaign_UpdateSources(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + orgID := owner.GetOrganizationID().String() + source1ID := factory.NewAccessReviewSource(owner, orgID). + WithName("Slack Source"). + Create() + source2ID := factory.NewAccessReviewSource(owner, orgID). + WithName("GitHub Source"). + Create() + campaignID := factory.NewAccessReviewCampaign(owner, orgID). + WithName("Campaign Sources Update"). + WithAccessReviewSourceIDs([]string{source1ID}). + Create() + + const query = ` + mutation($input: UpdateAccessReviewCampaignInput!) { + updateAccessReviewCampaign(input: $input) { + accessReviewCampaign { + id + sources { + sourceId + } + } + } + } + ` + + var result struct { + UpdateAccessReviewCampaign struct { + AccessReviewCampaign struct { + ID string `json:"id"` + Sources []struct { + SourceID *string `json:"sourceId"` + } `json:"sources"` + } `json:"accessReviewCampaign"` + } `json:"updateAccessReviewCampaign"` + } + + err := owner.Execute(query, map[string]any{ + "input": map[string]any{ + "accessReviewCampaignId": campaignID, + "accessReviewSourceIds": []string{source2ID}, + }, + }, &result) + require.NoError(t, err) + + assert.Equal(t, campaignID, result.UpdateAccessReviewCampaign.AccessReviewCampaign.ID) + require.Len(t, result.UpdateAccessReviewCampaign.AccessReviewCampaign.Sources, 1) + require.NotNil(t, result.UpdateAccessReviewCampaign.AccessReviewCampaign.Sources[0].SourceID) + assert.Equal(t, source2ID, *result.UpdateAccessReviewCampaign.AccessReviewCampaign.Sources[0].SourceID) +} + func TestAccessReviewCampaign_Delete(t *testing.T) { t.Parallel() owner := testutil.NewClient(t, testutil.RoleOwner) diff --git a/packages/n8n-node/nodes/Probo/GenericFunctions.ts b/packages/n8n-node/nodes/Probo/GenericFunctions.ts index 78aa5d868..447067a2f 100644 --- a/packages/n8n-node/nodes/Probo/GenericFunctions.ts +++ b/packages/n8n-node/nodes/Probo/GenericFunctions.ts @@ -21,6 +21,7 @@ import type { IExecuteFunctions, IHookFunctions, + ILoadOptionsFunctions, IDataObject, JsonObject, IHttpRequestOptions, @@ -35,8 +36,10 @@ type ApiRequestFn = ( variables?: IDataObject, ) => Promise; +type ApiRequestContext = IExecuteFunctions | IHookFunctions | ILoadOptionsFunctions; + async function proboGraphqlRequest( - this: IExecuteFunctions | IHookFunctions, + this: ApiRequestContext, apiPath: string, query: string, variables: IDataObject = {}, @@ -82,7 +85,7 @@ async function proboGraphqlRequest( } export async function proboApiRequest( - this: IExecuteFunctions | IHookFunctions, + this: ApiRequestContext, query: string, variables: IDataObject = {}, ): Promise { @@ -160,7 +163,7 @@ export async function proboApiRequestAllItems( ): Promise { return proboGraphqlRequestAllItems.call( this, - proboApiRequest, + proboApiRequest as ApiRequestFn, query, variables, getConnection, @@ -179,7 +182,7 @@ export async function proboConnectApiRequestAllItems( ): Promise { return proboGraphqlRequestAllItems.call( this, - proboConnectApiRequest, + proboConnectApiRequest as ApiRequestFn, query, variables, getConnection, diff --git a/packages/n8n-node/nodes/Probo/Probo.node.ts b/packages/n8n-node/nodes/Probo/Probo.node.ts index 6367481e6..a03b9b30f 100644 --- a/packages/n8n-node/nodes/Probo/Probo.node.ts +++ b/packages/n8n-node/nodes/Probo/Probo.node.ts @@ -31,6 +31,7 @@ import { getAllResourceFields, getExecuteFunction, } from './actions'; +import { getAccessReviewSources } from './actions/accessReview/loadAccessReviewSourceOptions'; export class Probo implements INodeType { description: INodeTypeDescription = { @@ -273,6 +274,9 @@ export class Probo implements INodeType { } methods = { + loadOptions: { + getAccessReviewSources, + }, listSearch: {}, }; } diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/addSource.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/addSource.operation.ts deleted file mode 100644 index 1056921e5..000000000 --- a/packages/n8n-node/nodes/Probo/actions/accessReview/addSource.operation.ts +++ /dev/null @@ -1,87 +0,0 @@ -// Copyright (c) 2025-2026 Probo Inc . -// -// Permission is hereby granted, free of charge, to any person obtaining a copy -// of this software and associated documentation files (the "Software"), to deal -// in the Software without restriction, including without limitation the rights -// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -// copies of the Software, and to permit persons to whom the Software is -// furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in -// all copies or substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -// SOFTWARE. - -import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; -import { proboApiRequest } from '../../GenericFunctions'; - -export const description: INodeProperties[] = [ - { - displayName: 'Access Review Campaign ID', - name: 'accessReviewCampaignId', - type: 'string', - displayOptions: { - show: { - resource: ['accessReview'], - operation: ['addSource'], - }, - }, - default: '', - description: 'The ID of the access review campaign', - required: true, - }, - { - displayName: 'Access Review Source ID', - name: 'accessReviewSourceId', - type: 'string', - displayOptions: { - show: { - resource: ['accessReview'], - operation: ['addSource'], - }, - }, - default: '', - description: 'The ID of the access review source to add to the campaign', - required: true, - }, -]; - -export async function execute( - this: IExecuteFunctions, - itemIndex: number, -): Promise { - const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; - const accessReviewSourceId = this.getNodeParameter('accessReviewSourceId', itemIndex) as string; - - const query = ` - mutation AddAccessReviewCampaignSource($input: AddAccessReviewCampaignSourceInput!) { - addAccessReviewCampaignSource(input: $input) { - accessReviewCampaign { - id - name - description - status - startedAt - completedAt - createdAt - updatedAt - } - } - } - `; - - const responseData = await proboApiRequest.call(this, query, { - input: { accessReviewCampaignId, accessReviewSourceId }, - }); - - return { - json: responseData, - pairedItem: { item: itemIndex }, - }; -} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts index e4c8e8f80..20739dfcf 100644 --- a/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts @@ -20,6 +20,7 @@ import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import { proboApiRequest } from '../../GenericFunctions'; +import { accessReviewSourceIdsField } from './sources.fields'; export const description: INodeProperties[] = [ { @@ -63,6 +64,7 @@ export const description: INodeProperties[] = [ default: '', description: 'The description of the access review campaign', }, + accessReviewSourceIdsField, ]; export async function execute( @@ -72,6 +74,7 @@ export async function execute( const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; const name = this.getNodeParameter('name', itemIndex) as string; const description = this.getNodeParameter('description', itemIndex, '') as string; + const accessReviewSourceIds = this.getNodeParameter('accessReviewSourceIds', itemIndex, []) as string[]; const query = ` mutation CreateAccessReviewCampaign($input: CreateAccessReviewCampaignInput!) { @@ -97,6 +100,7 @@ export async function execute( organizationId, name, ...(description && { description }), + ...(accessReviewSourceIds.length > 0 && { accessReviewSourceIds }), }, }; diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts index 4a71bd6e5..18eb48ba5 100644 --- a/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts @@ -19,7 +19,6 @@ // SOFTWARE. import type { INodeProperties } from 'n8n-workflow'; -import * as addSourceOp from './addSource.operation'; import * as createOp from './create.operation'; import * as deleteOp from './delete.operation'; import * as getOp from './get.operation'; @@ -41,12 +40,6 @@ export const description: INodeProperties[] = [ }, }, options: [ - { - name: 'Add Source', - value: 'addSource', - description: 'Add a scope source to an access review campaign', - action: 'Add a scope source to an access review campaign', - }, { name: 'Cancel', value: 'cancel', @@ -98,7 +91,6 @@ export const description: INodeProperties[] = [ ], default: 'create', }, - ...addSourceOp.description, ...createOp.description, ...deleteOp.description, ...getOp.description, @@ -110,7 +102,6 @@ export const description: INodeProperties[] = [ ]; export { - addSourceOp as addSource, createOp as create, deleteOp as delete, getOp as get, diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/loadAccessReviewSourceOptions.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/loadAccessReviewSourceOptions.ts new file mode 100644 index 000000000..f85ea53ee --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/loadAccessReviewSourceOptions.ts @@ -0,0 +1,104 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { IDataObject, ILoadOptionsFunctions, INodePropertyOptions } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +const organizationSourcesQuery = ` + query AccessReviewSources($organizationId: ID!) { + organization: node(id: $organizationId) { + ... on Organization { + accessReviewSources(first: 500) { + edges { + node { + id + name + } + } + } + } + } + } +`; + +const campaignOrganizationQuery = ` + query AccessReviewCampaignOrganization($accessReviewCampaignId: ID!) { + campaign: node(id: $accessReviewCampaignId) { + ... on AccessReviewCampaign { + organization { + id + } + } + } + } +`; + +function mapSources(responseData: IDataObject): INodePropertyOptions[] { + const data = responseData.data as IDataObject | undefined; + const organization = data?.organization as IDataObject | undefined; + const accessReviewSources = organization?.accessReviewSources as IDataObject | undefined; + const edges = accessReviewSources?.edges as Array<{ node: IDataObject }> | undefined; + + return (edges ?? []) + .map((edge) => edge.node) + .filter((node): node is IDataObject => node !== undefined && typeof node.id === 'string') + .map((node) => ({ + name: String(node.name ?? node.id), + value: String(node.id), + })); +} + +async function resolveOrganizationId( + this: ILoadOptionsFunctions, +): Promise { + const organizationId = this.getCurrentNodeParameter('organizationId') as string | undefined; + if (organizationId) { + return organizationId; + } + + const accessReviewCampaignId = this.getCurrentNodeParameter('accessReviewCampaignId') as string | undefined; + if (!accessReviewCampaignId) { + return undefined; + } + + const responseData = await proboApiRequest.call(this, campaignOrganizationQuery, { + accessReviewCampaignId, + }); + const data = responseData.data as IDataObject | undefined; + const campaign = data?.campaign as IDataObject | undefined; + const organization = campaign?.organization as IDataObject | undefined; + + return typeof organization?.id === 'string' ? organization.id : undefined; +} + +export async function getAccessReviewSources( + this: ILoadOptionsFunctions, +): Promise { + const organizationId = await resolveOrganizationId.call(this); + if (!organizationId) { + return []; + } + + const responseData = await proboApiRequest.call(this, organizationSourcesQuery, { + organizationId, + }); + + return mapSources(responseData); +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/sources.fields.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/sources.fields.ts new file mode 100644 index 000000000..d0445633c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/sources.fields.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: +// +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +// SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; + +export const accessReviewSourceIdsField: INodeProperties = { + displayName: 'Source Names or IDs', + name: 'accessReviewSourceIds', + type: 'multiOptions', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['create'], + }, + }, + typeOptions: { + loadOptionsMethod: 'getAccessReviewSources', + loadOptionsDependsOn: ['organizationId'], + }, + default: [], + description: 'Scope sources to include in the campaign. Choose from the list, or specify IDs using an expression.', +}; + +export const accessReviewSourceIdsUpdateField: INodeProperties = { + displayName: 'Source Names or IDs', + name: 'accessReviewSourceIds', + type: 'multiOptions', + typeOptions: { + loadOptionsMethod: 'getAccessReviewSources', + loadOptionsDependsOn: ['accessReviewCampaignId'], + }, + default: [], + description: 'Replace the campaign scope sources with this selection. Choose from the list, or specify IDs using an expression.', +}; diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts index 325f7da2c..e98b48d4e 100644 --- a/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts @@ -20,6 +20,7 @@ import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; import { proboApiRequest } from '../../GenericFunctions'; +import { accessReviewSourceIdsUpdateField } from './sources.fields'; export const description: INodeProperties[] = [ { @@ -62,6 +63,20 @@ export const description: INodeProperties[] = [ default: '', description: 'The description of the access review campaign', }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['update'], + }, + }, + options: [accessReviewSourceIdsUpdateField], + }, ]; export async function execute( @@ -71,6 +86,9 @@ export async function execute( const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; const name = this.getNodeParameter('name', itemIndex, '') as string; const description = this.getNodeParameter('description', itemIndex, '') as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + accessReviewSourceIds?: string[]; + }; const query = ` mutation UpdateAccessReviewCampaign($input: UpdateAccessReviewCampaignInput!) { @@ -89,9 +107,12 @@ export async function execute( } `; - const input: Record = { accessReviewCampaignId }; + const input: Record = { accessReviewCampaignId }; if (name) input.name = name; if (description) input.description = description; + if (additionalFields.accessReviewSourceIds !== undefined) { + input.accessReviewSourceIds = additionalFields.accessReviewSourceIds; + } const responseData = await proboApiRequest.call(this, query, { input }); diff --git a/pkg/accessreview/campaign_service.go b/pkg/accessreview/campaign_service.go index 1a37d994f..c96b65a06 100644 --- a/pkg/accessreview/campaign_service.go +++ b/pkg/accessreview/campaign_service.go @@ -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, diff --git a/pkg/accessreview/campaign_types.go b/pkg/accessreview/campaign_types.go index aaa89a0df..54002b6ba 100644 --- a/pkg/accessreview/campaign_types.go +++ b/pkg/accessreview/campaign_types.go @@ -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 { diff --git a/pkg/server/api/console/v1/access_review_campaign_resolvers.go b/pkg/server/api/console/v1/access_review_campaign_resolvers.go index 3edce2568..e4ebfa8a6 100644 --- a/pkg/server/api/console/v1/access_review_campaign_resolvers.go +++ b/pkg/server/api/console/v1/access_review_campaign_resolvers.go @@ -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) diff --git a/pkg/server/api/console/v1/graphql/access_review_campaign.graphql b/pkg/server/api/console/v1/graphql/access_review_campaign.graphql index 38b7a7ff8..ca0351d57 100644 --- a/pkg/server/api/console/v1/graphql/access_review_campaign.graphql +++ b/pkg/server/api/console/v1/graphql/access_review_campaign.graphql @@ -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 {