From 2db37660d36271d966e5b5229471a7d7617cda54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=89mile=20R=C3=A9?= Date: Mon, 20 Jul 2026 19:25:29 +0200 Subject: [PATCH] Expose commitment CRUD on MCP, CLI, n8n MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sync GraphQL commitment group and item operations to the remaining API surfaces so automation can manage compliance portal commitments end to end. Signed-off-by: Émile Ré --- e2e/mcp/commitment_test.go | 318 +++++++++++++ .../trustCenter/createCommitment.operation.ts | 161 +++++++ .../createCommitmentGroup.operation.ts | 105 +++++ .../trustCenter/deleteCommitment.operation.ts | 61 +++ .../deleteCommitmentGroup.operation.ts | 61 +++ .../getAllCommitmentGroups.operation.ts | 124 +++++ .../getAllCommitments.operation.ts | 123 +++++ .../nodes/Probo/actions/trustCenter/index.ts | 72 +++ .../trustCenter/updateCommitment.operation.ts | 175 +++++++ .../updateCommitmentGroup.operation.ts | 118 +++++ pkg/cmd/trust-center/commitment/commitment.go | 45 ++ .../trust-center/commitment/create/create.go | 246 ++++++++++ .../trust-center/commitment/delete/delete.go | 111 +++++ pkg/cmd/trust-center/commitment/list/list.go | 206 +++++++++ .../trust-center/commitment/update/update.go | 184 ++++++++ .../commitmentgroup/commitmentgroup.go | 45 ++ .../commitmentgroup/create/create.go | 216 +++++++++ .../commitmentgroup/delete/delete.go | 111 +++++ .../trust-center/commitmentgroup/list/list.go | 212 +++++++++ .../commitmentgroup/update/update.go | 141 ++++++ pkg/cmd/trust-center/trust_center.go | 4 + pkg/server/api/mcp/v1/schema.resolvers.go | 228 ++++++++++ pkg/server/api/mcp/v1/specification.yaml | 428 ++++++++++++++++++ pkg/server/api/mcp/v1/types/commitment.go | 93 ++++ 24 files changed, 3588 insertions(+) create mode 100644 e2e/mcp/commitment_test.go create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitment.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitmentGroup.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitment.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitmentGroup.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitmentGroups.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitments.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitment.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitmentGroup.operation.ts create mode 100644 pkg/cmd/trust-center/commitment/commitment.go create mode 100644 pkg/cmd/trust-center/commitment/create/create.go create mode 100644 pkg/cmd/trust-center/commitment/delete/delete.go create mode 100644 pkg/cmd/trust-center/commitment/list/list.go create mode 100644 pkg/cmd/trust-center/commitment/update/update.go create mode 100644 pkg/cmd/trust-center/commitmentgroup/commitmentgroup.go create mode 100644 pkg/cmd/trust-center/commitmentgroup/create/create.go create mode 100644 pkg/cmd/trust-center/commitmentgroup/delete/delete.go create mode 100644 pkg/cmd/trust-center/commitmentgroup/list/list.go create mode 100644 pkg/cmd/trust-center/commitmentgroup/update/update.go create mode 100644 pkg/server/api/mcp/v1/types/commitment.go diff --git a/e2e/mcp/commitment_test.go b/e2e/mcp/commitment_test.go new file mode 100644 index 000000000..51ed2d04c --- /dev/null +++ b/e2e/mcp/commitment_test.go @@ -0,0 +1,318 @@ +// Copyright (c) 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. + +package mcp_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.probo.inc/probo/e2e/internal/factory" + "go.probo.inc/probo/e2e/internal/testutil" +) + +type commitmentGroup struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` +} + +type commitment struct { + ID string `json:"id"` + GroupID string `json:"group_id"` + Icon string `json:"icon"` + Eyebrow string `json:"eyebrow"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` +} + +func mcpTrustCenterID(t *testing.T, mc *testutil.MCPClient, orgID string) string { + t.Helper() + + var getResult struct { + TrustCenter struct { + ID string `json:"id"` + } `json:"trust_center"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organization_id": orgID, + }, &getResult) + require.NotEmpty(t, getResult.TrustCenter.ID) + + return getResult.TrustCenter.ID +} + +func TestMCP_AddCommitmentGroup(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var result struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "Security Practices", + "description": "How we protect customer data", + }, &result) + + assert.NotEmpty(t, result.CommitmentGroup.ID) + assert.Equal(t, "Security Practices", result.CommitmentGroup.Title) + assert.Equal(t, "How we protect customer data", result.CommitmentGroup.Description) +} + +func TestMCP_UpdateCommitmentGroup(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var addResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "Original Group", + "description": "Original description", + }, &addResult) + require.NotEmpty(t, addResult.CommitmentGroup.ID) + + var updateResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("updateCommitmentGroup", map[string]any{ + "id": addResult.CommitmentGroup.ID, + "title": "Updated Group", + }, &updateResult) + + assert.Equal(t, addResult.CommitmentGroup.ID, updateResult.CommitmentGroup.ID) + assert.Equal(t, "Updated Group", updateResult.CommitmentGroup.Title) +} + +func TestMCP_DeleteCommitmentGroup(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var addResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "Group to delete", + "description": "Temporary", + }, &addResult) + require.NotEmpty(t, addResult.CommitmentGroup.ID) + + var deleteResult struct { + DeletedCommitmentGroupID string `json:"deleted_commitment_group_id"` + } + mc.CallToolInto("deleteCommitmentGroup", map[string]any{ + "id": addResult.CommitmentGroup.ID, + }, &deleteResult) + + assert.Equal(t, addResult.CommitmentGroup.ID, deleteResult.DeletedCommitmentGroupID) +} + +func TestMCP_ListCommitmentGroups(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + for range 2 { + var result struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": factory.SafeName("Group"), + "description": factory.SafeName("Desc"), + }, &result) + require.NotEmpty(t, result.CommitmentGroup.ID) + } + + var listResult struct { + CommitmentGroups []commitmentGroup `json:"commitment_groups"` + } + mc.CallToolInto("listCommitmentGroups", map[string]any{ + "trust_center_id": tcID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.CommitmentGroups), 2) +} + +func TestMCP_AddCommitment(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var groupResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "Encryption", + "description": "Encryption commitments", + }, &groupResult) + require.NotEmpty(t, groupResult.CommitmentGroup.ID) + + var result struct { + Commitment commitment `json:"commitment"` + } + mc.CallToolInto("addCommitment", map[string]any{ + "group_id": groupResult.CommitmentGroup.ID, + "icon": "LOCK_KEY", + "eyebrow": "Data at rest", + "title": "AES-256 encryption", + "description": "All customer data is encrypted at rest", + }, &result) + + assert.NotEmpty(t, result.Commitment.ID) + assert.Equal(t, groupResult.CommitmentGroup.ID, result.Commitment.GroupID) + assert.Equal(t, "LOCK_KEY", result.Commitment.Icon) + assert.Equal(t, "AES-256 encryption", result.Commitment.Title) +} + +func TestMCP_UpdateCommitment(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var groupResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "Access control", + "description": "Access commitments", + }, &groupResult) + require.NotEmpty(t, groupResult.CommitmentGroup.ID) + + var addResult struct { + Commitment commitment `json:"commitment"` + } + mc.CallToolInto("addCommitment", map[string]any{ + "group_id": groupResult.CommitmentGroup.ID, + "icon": "KEY", + "eyebrow": "SSO", + "title": "Original title", + "description": "Original description", + }, &addResult) + require.NotEmpty(t, addResult.Commitment.ID) + + var updateResult struct { + Commitment commitment `json:"commitment"` + } + mc.CallToolInto("updateCommitment", map[string]any{ + "id": addResult.Commitment.ID, + "title": "Updated title", + "icon": "FINGERPRINT", + }, &updateResult) + + assert.Equal(t, addResult.Commitment.ID, updateResult.Commitment.ID) + assert.Equal(t, "Updated title", updateResult.Commitment.Title) + assert.Equal(t, "FINGERPRINT", updateResult.Commitment.Icon) +} + +func TestMCP_DeleteCommitment(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var groupResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "Temporary group", + "description": "For delete test", + }, &groupResult) + require.NotEmpty(t, groupResult.CommitmentGroup.ID) + + var addResult struct { + Commitment commitment `json:"commitment"` + } + mc.CallToolInto("addCommitment", map[string]any{ + "group_id": groupResult.CommitmentGroup.ID, + "icon": "SHIELD_CHECK", + "eyebrow": "Compliance", + "title": "Commitment to delete", + "description": "Temporary", + }, &addResult) + require.NotEmpty(t, addResult.Commitment.ID) + + var deleteResult struct { + DeletedCommitmentID string `json:"deleted_commitment_id"` + } + mc.CallToolInto("deleteCommitment", map[string]any{ + "id": addResult.Commitment.ID, + }, &deleteResult) + + assert.Equal(t, addResult.Commitment.ID, deleteResult.DeletedCommitmentID) +} + +func TestMCP_ListCommitments(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + tcID := mcpTrustCenterID(t, mc, owner.GetOrganizationID().String()) + + var groupResult struct { + CommitmentGroup commitmentGroup `json:"commitment_group"` + } + mc.CallToolInto("addCommitmentGroup", map[string]any{ + "trust_center_id": tcID, + "title": "List group", + "description": "For list test", + }, &groupResult) + require.NotEmpty(t, groupResult.CommitmentGroup.ID) + + for range 2 { + var result struct { + Commitment commitment `json:"commitment"` + } + mc.CallToolInto("addCommitment", map[string]any{ + "group_id": groupResult.CommitmentGroup.ID, + "icon": "LOCK", + "eyebrow": factory.SafeName("Eye"), + "title": factory.SafeName("Title"), + "description": factory.SafeName("Desc"), + }, &result) + require.NotEmpty(t, result.Commitment.ID) + } + + var listResult struct { + Commitments []commitment `json:"commitments"` + } + mc.CallToolInto("listCommitments", map[string]any{ + "group_id": groupResult.CommitmentGroup.ID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.Commitments), 2) +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitment.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitment.operation.ts new file mode 100644 index 000000000..bdbfe1fe2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitment.operation.ts @@ -0,0 +1,161 @@ +// 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'; + +const commitmentIconOptions = [ + { name: 'Lock Key', value: 'LOCK_KEY' }, + { name: 'Eye Slash', value: 'EYE_SLASH' }, + { name: 'Fingerprint', value: 'FINGERPRINT' }, + { name: 'Shield Warning', value: 'SHIELD_WARNING' }, + { name: 'Shield Check', value: 'SHIELD_CHECK' }, + { name: 'Siren', value: 'SIREN' }, + { name: 'Key', value: 'KEY' }, + { name: 'Lock', value: 'LOCK' }, + { name: 'Cloud', value: 'CLOUD' }, + { name: 'Database', value: 'DATABASE' }, + { name: 'Globe', value: 'GLOBE' }, + { name: 'Eye', value: 'EYE' }, + { name: 'Users', value: 'USERS' }, + { name: 'Certificate', value: 'CERTIFICATE' }, + { name: 'Gavel', value: 'GAVEL' }, + { name: 'Heartbeat', value: 'HEARTBEAT' }, + { name: 'Bell', value: 'BELL' }, + { name: 'Bug', value: 'BUG' }, + { name: 'Code', value: 'CODE' }, + { name: 'Server', value: 'SERVER' }, +]; + +export const description: INodeProperties[] = [ + { + displayName: 'Commitment Group ID', + name: 'groupId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitment'], + }, + }, + default: '', + description: 'The ID of the commitment group', + required: true, + }, + { + displayName: 'Icon', + name: 'icon', + type: 'options', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitment'], + }, + }, + options: commitmentIconOptions, + default: 'LOCK_KEY', + description: 'The icon of the commitment', + required: true, + }, + { + displayName: 'Eyebrow', + name: 'eyebrow', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitment'], + }, + }, + default: '', + description: 'The eyebrow text of the commitment', + required: true, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitment'], + }, + }, + default: '', + description: 'The title of the commitment', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + rows: 4, + }, + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitment'], + }, + }, + default: '', + description: 'The description of the commitment', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const groupId = this.getNodeParameter('groupId', itemIndex) as string; + const icon = this.getNodeParameter('icon', itemIndex) as string; + const eyebrow = this.getNodeParameter('eyebrow', itemIndex) as string; + const title = this.getNodeParameter('title', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex) as string; + + const query = ` + mutation CreateCompliancePortalCommitment($input: CreateCompliancePortalCommitmentInput!) { + createCompliancePortalCommitment(input: $input) { + compliancePortalCommitmentEdge { + node { + id + icon + eyebrow + title + description + rank + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { groupId, icon, eyebrow, title, description }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitmentGroup.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitmentGroup.operation.ts new file mode 100644 index 000000000..9d9eb612e --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/createCommitmentGroup.operation.ts @@ -0,0 +1,105 @@ +// 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: 'Trust Center ID', + name: 'trustCenterId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitmentGroup'], + }, + }, + default: '', + description: 'The ID of the trust center', + required: true, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitmentGroup'], + }, + }, + default: '', + description: 'The title of the commitment group', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + rows: 4, + }, + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createCommitmentGroup'], + }, + }, + default: '', + description: 'The description of the commitment group', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const trustCenterId = this.getNodeParameter('trustCenterId', itemIndex) as string; + const title = this.getNodeParameter('title', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex) as string; + + const query = ` + mutation CreateCompliancePortalCommitmentGroup($input: CreateCompliancePortalCommitmentGroupInput!) { + createCompliancePortalCommitmentGroup(input: $input) { + compliancePortalCommitmentGroupEdge { + node { + id + title + description + rank + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { trustCenterId, title, description }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitment.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitment.operation.ts new file mode 100644 index 000000000..a53df0628 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitment.operation.ts @@ -0,0 +1,61 @@ +// 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: 'Commitment ID', + name: 'commitmentId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['deleteCommitment'], + }, + }, + default: '', + description: 'The ID of the commitment to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const commitmentId = this.getNodeParameter('commitmentId', itemIndex) as string; + + const query = ` + mutation DeleteCompliancePortalCommitment($input: DeleteCompliancePortalCommitmentInput!) { + deleteCompliancePortalCommitment(input: $input) { + deletedCompliancePortalCommitmentId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { id: commitmentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitmentGroup.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitmentGroup.operation.ts new file mode 100644 index 000000000..583abb4ac --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteCommitmentGroup.operation.ts @@ -0,0 +1,61 @@ +// 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: 'Commitment Group ID', + name: 'commitmentGroupId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['deleteCommitmentGroup'], + }, + }, + default: '', + description: 'The ID of the commitment group to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const commitmentGroupId = this.getNodeParameter('commitmentGroupId', itemIndex) as string; + + const query = ` + mutation DeleteCompliancePortalCommitmentGroup($input: DeleteCompliancePortalCommitmentGroupInput!) { + deleteCompliancePortalCommitmentGroup(input: $input) { + deletedCompliancePortalCommitmentGroupId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { id: commitmentGroupId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitmentGroups.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitmentGroups.operation.ts new file mode 100644 index 000000000..81dcd05b2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitmentGroups.operation.ts @@ -0,0 +1,124 @@ +// 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, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllCommitmentGroups'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllCommitmentGroups'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllCommitmentGroups'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetCompliancePortalCommitmentGroups($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + trustCenter { + commitmentGroups(first: $first, after: $after) { + edges { + node { + id + title + description + rank + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + } + `; + + const commitmentGroups = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + const trustCenter = node?.trustCenter as IDataObject | undefined; + return trustCenter?.commitmentGroups as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { commitmentGroups }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitments.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitments.operation.ts new file mode 100644 index 000000000..0eac0be51 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllCommitments.operation.ts @@ -0,0 +1,123 @@ +// 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, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Commitment Group ID', + name: 'groupId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllCommitments'], + }, + }, + default: '', + description: 'The ID of the commitment group', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllCommitments'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllCommitments'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const groupId = this.getNodeParameter('groupId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetCompliancePortalCommitments($groupId: ID!, $first: Int, $after: CursorKey) { + node(id: $groupId) { + ... on CompliancePortalCommitmentGroup { + commitments(first: $first, after: $after) { + edges { + node { + id + icon + eyebrow + title + description + rank + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const commitments = await proboApiRequestAllItems.call( + this, + query, + { groupId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.commitments as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { commitments }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/index.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/index.ts index 814fca01d..e4dcb0534 100644 --- a/packages/n8n-node/nodes/Probo/actions/trustCenter/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/index.ts @@ -28,6 +28,14 @@ import * as getAllFilesOp from './getAllFiles.operation'; import * as deleteFileOp from './deleteFile.operation'; import * as createExternalUrlOp from './createExternalUrl.operation'; import * as deleteExternalUrlOp from './deleteExternalUrl.operation'; +import * as getAllCommitmentGroupsOp from './getAllCommitmentGroups.operation'; +import * as createCommitmentGroupOp from './createCommitmentGroup.operation'; +import * as updateCommitmentGroupOp from './updateCommitmentGroup.operation'; +import * as deleteCommitmentGroupOp from './deleteCommitmentGroup.operation'; +import * as getAllCommitmentsOp from './getAllCommitments.operation'; +import * as createCommitmentOp from './createCommitment.operation'; +import * as updateCommitmentOp from './updateCommitment.operation'; +import * as deleteCommitmentOp from './deleteCommitment.operation'; export const description: INodeProperties[] = [ { @@ -41,6 +49,18 @@ export const description: INodeProperties[] = [ }, }, options: [ + { + name: 'Create Commitment', + value: 'createCommitment', + description: 'Create a new compliance portal commitment', + action: 'Create a compliance portal commitment', + }, + { + name: 'Create Commitment Group', + value: 'createCommitmentGroup', + description: 'Create a new compliance portal commitment group', + action: 'Create a compliance portal commitment group', + }, { name: 'Create External URL', value: 'createExternalUrl', @@ -53,6 +73,18 @@ export const description: INodeProperties[] = [ description: 'Create a new trust center reference', action: 'Create a trust center reference', }, + { + name: 'Delete Commitment', + value: 'deleteCommitment', + description: 'Delete a compliance portal commitment', + action: 'Delete a compliance portal commitment', + }, + { + name: 'Delete Commitment Group', + value: 'deleteCommitmentGroup', + description: 'Delete a compliance portal commitment group', + action: 'Delete a compliance portal commitment group', + }, { name: 'Delete External URL', value: 'deleteExternalUrl', @@ -77,6 +109,18 @@ export const description: INodeProperties[] = [ description: 'Get trust center settings', action: 'Get trust center settings', }, + { + name: 'Get Many Commitment Groups', + value: 'getAllCommitmentGroups', + description: 'Get many compliance portal commitment groups', + action: 'Get many compliance portal commitment groups', + }, + { + name: 'Get Many Commitments', + value: 'getAllCommitments', + description: 'Get many compliance portal commitments', + action: 'Get many compliance portal commitments', + }, { name: 'Get Many Files', value: 'getAllFiles', @@ -95,6 +139,18 @@ export const description: INodeProperties[] = [ description: 'Update trust center settings', action: 'Update trust center settings', }, + { + name: 'Update Commitment', + value: 'updateCommitment', + description: 'Update a compliance portal commitment', + action: 'Update a compliance portal commitment', + }, + { + name: 'Update Commitment Group', + value: 'updateCommitmentGroup', + description: 'Update a compliance portal commitment group', + action: 'Update a compliance portal commitment group', + }, ], default: 'get', }, @@ -107,6 +163,14 @@ export const description: INodeProperties[] = [ ...deleteFileOp.description, ...createExternalUrlOp.description, ...deleteExternalUrlOp.description, + ...getAllCommitmentGroupsOp.description, + ...createCommitmentGroupOp.description, + ...updateCommitmentGroupOp.description, + ...deleteCommitmentGroupOp.description, + ...getAllCommitmentsOp.description, + ...createCommitmentOp.description, + ...updateCommitmentOp.description, + ...deleteCommitmentOp.description, ]; export { @@ -119,4 +183,12 @@ export { deleteFileOp as deleteFile, createExternalUrlOp as createExternalUrl, deleteExternalUrlOp as deleteExternalUrl, + getAllCommitmentGroupsOp as getAllCommitmentGroups, + createCommitmentGroupOp as createCommitmentGroup, + updateCommitmentGroupOp as updateCommitmentGroup, + deleteCommitmentGroupOp as deleteCommitmentGroup, + getAllCommitmentsOp as getAllCommitments, + createCommitmentOp as createCommitment, + updateCommitmentOp as updateCommitment, + deleteCommitmentOp as deleteCommitment, }; diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitment.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitment.operation.ts new file mode 100644 index 000000000..9d690a717 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitment.operation.ts @@ -0,0 +1,175 @@ +// 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'; + +const commitmentIconOptions = [ + { name: '(Unchanged)', value: '' }, + { name: 'Lock Key', value: 'LOCK_KEY' }, + { name: 'Eye Slash', value: 'EYE_SLASH' }, + { name: 'Fingerprint', value: 'FINGERPRINT' }, + { name: 'Shield Warning', value: 'SHIELD_WARNING' }, + { name: 'Shield Check', value: 'SHIELD_CHECK' }, + { name: 'Siren', value: 'SIREN' }, + { name: 'Key', value: 'KEY' }, + { name: 'Lock', value: 'LOCK' }, + { name: 'Cloud', value: 'CLOUD' }, + { name: 'Database', value: 'DATABASE' }, + { name: 'Globe', value: 'GLOBE' }, + { name: 'Eye', value: 'EYE' }, + { name: 'Users', value: 'USERS' }, + { name: 'Certificate', value: 'CERTIFICATE' }, + { name: 'Gavel', value: 'GAVEL' }, + { name: 'Heartbeat', value: 'HEARTBEAT' }, + { name: 'Bell', value: 'BELL' }, + { name: 'Bug', value: 'BUG' }, + { name: 'Code', value: 'CODE' }, + { name: 'Server', value: 'SERVER' }, +]; + +export const description: INodeProperties[] = [ + { + displayName: 'Commitment ID', + name: 'commitmentId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitment'], + }, + }, + default: '', + description: 'The ID of the commitment to update', + required: true, + }, + { + displayName: 'Icon', + name: 'icon', + type: 'options', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitment'], + }, + }, + options: commitmentIconOptions, + default: '', + description: 'The icon of the commitment', + }, + { + displayName: 'Eyebrow', + name: 'eyebrow', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitment'], + }, + }, + default: '', + description: 'The eyebrow text of the commitment', + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitment'], + }, + }, + default: '', + description: 'The title of the commitment', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + rows: 4, + }, + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitment'], + }, + }, + default: '', + description: 'The description of the commitment', + }, + { + displayName: 'Rank', + name: 'rank', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitment'], + }, + }, + default: '', + description: 'The rank of the commitment for ordering', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const commitmentId = this.getNodeParameter('commitmentId', itemIndex) as string; + const icon = this.getNodeParameter('icon', itemIndex, '') as string; + const eyebrow = this.getNodeParameter('eyebrow', itemIndex, '') as string; + const title = this.getNodeParameter('title', itemIndex, '') as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const rank = this.getNodeParameter('rank', itemIndex, '') as string; + + const query = ` + mutation UpdateCompliancePortalCommitment($input: UpdateCompliancePortalCommitmentInput!) { + updateCompliancePortalCommitment(input: $input) { + compliancePortalCommitment { + id + icon + eyebrow + title + description + rank + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: commitmentId }; + if (icon) input.icon = icon; + if (eyebrow) input.eyebrow = eyebrow; + if (title) input.title = title; + if (description) input.description = description; + if (rank) input.rank = Number(rank); + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitmentGroup.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitmentGroup.operation.ts new file mode 100644 index 000000000..fef3564ce --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/updateCommitmentGroup.operation.ts @@ -0,0 +1,118 @@ +// 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: 'Commitment Group ID', + name: 'commitmentGroupId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitmentGroup'], + }, + }, + default: '', + description: 'The ID of the commitment group to update', + required: true, + }, + { + displayName: 'Title', + name: 'title', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitmentGroup'], + }, + }, + default: '', + description: 'The title of the commitment group', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + rows: 4, + }, + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitmentGroup'], + }, + }, + default: '', + description: 'The description of the commitment group', + }, + { + displayName: 'Rank', + name: 'rank', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['updateCommitmentGroup'], + }, + }, + default: '', + description: 'The rank of the commitment group for ordering', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const commitmentGroupId = this.getNodeParameter('commitmentGroupId', itemIndex) as string; + const title = this.getNodeParameter('title', itemIndex, '') as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const rank = this.getNodeParameter('rank', itemIndex, '') as string; + + const query = ` + mutation UpdateCompliancePortalCommitmentGroup($input: UpdateCompliancePortalCommitmentGroupInput!) { + updateCompliancePortalCommitmentGroup(input: $input) { + compliancePortalCommitmentGroup { + id + title + description + rank + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: commitmentGroupId }; + if (title) input.title = title; + if (description) input.description = description; + if (rank) input.rank = Number(rank); + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/pkg/cmd/trust-center/commitment/commitment.go b/pkg/cmd/trust-center/commitment/commitment.go new file mode 100644 index 000000000..44302919d --- /dev/null +++ b/pkg/cmd/trust-center/commitment/commitment.go @@ -0,0 +1,45 @@ +// Copyright (c) 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. + +package commitment + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/trust-center/commitment/create" + "go.probo.inc/probo/pkg/cmd/trust-center/commitment/delete" + "go.probo.inc/probo/pkg/cmd/trust-center/commitment/list" + "go.probo.inc/probo/pkg/cmd/trust-center/commitment/update" +) + +func NewCmdCommitment(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "commitment ", + Short: "Manage compliance portal commitments", + Aliases: []string{"cmt"}, + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/trust-center/commitment/create/create.go b/pkg/cmd/trust-center/commitment/create/create.go new file mode 100644 index 000000000..8cc01d494 --- /dev/null +++ b/pkg/cmd/trust-center/commitment/create/create.go @@ -0,0 +1,246 @@ +// Copyright (c) 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. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +var validIcons = []string{ + "LOCK_KEY", + "EYE_SLASH", + "FINGERPRINT", + "SHIELD_WARNING", + "SHIELD_CHECK", + "SIREN", + "KEY", + "LOCK", + "CLOUD", + "DATABASE", + "GLOBE", + "EYE", + "USERS", + "CERTIFICATE", + "GAVEL", + "HEARTBEAT", + "BELL", + "BUG", + "CODE", + "SERVER", +} + +const createMutation = ` +mutation($input: CreateCompliancePortalCommitmentInput!) { + createCompliancePortalCommitment(input: $input) { + compliancePortalCommitmentEdge { + node { + id + icon + eyebrow + title + description + rank + } + } + } +} +` + +type createResponse struct { + CreateCompliancePortalCommitment struct { + CompliancePortalCommitmentEdge struct { + Node struct { + ID string `json:"id"` + Icon string `json:"icon"` + Eyebrow string `json:"eyebrow"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` + } `json:"node"` + } `json:"compliancePortalCommitmentEdge"` + } `json:"createCompliancePortalCommitment"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagGroup string + flagIcon string + flagEyebrow string + flagTitle string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a compliance portal commitment", + Example: ` # Create a commitment interactively + prb trust-center commitment create --group + + # Create a commitment non-interactively + prb trust-center cmt create --group --icon SHIELD_CHECK --eyebrow "Security" --title "Encryption" --description "Data encrypted at rest"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if f.IOStreams.IsInteractive() { + if flagGroup == "" { + err := huh.NewInput(). + Title("Commitment group ID"). + Value(&flagGroup). + Run() + if err != nil { + return err + } + } + + if flagIcon == "" { + iconOptions := make([]huh.Option[string], 0, len(validIcons)) + for _, icon := range validIcons { + iconOptions = append(iconOptions, huh.NewOption(icon, icon)) + } + + err := huh.NewSelect[string](). + Title("Icon"). + Options(iconOptions...). + Value(&flagIcon). + Run() + if err != nil { + return err + } + } + + if flagEyebrow == "" { + err := huh.NewInput(). + Title("Eyebrow"). + Value(&flagEyebrow). + Run() + if err != nil { + return err + } + } + + if flagTitle == "" { + err := huh.NewInput(). + Title("Title"). + Value(&flagTitle). + Run() + if err != nil { + return err + } + } + + if flagDescription == "" { + err := huh.NewText(). + Title("Description"). + Value(&flagDescription). + Run() + if err != nil { + return err + } + } + } + + if flagGroup == "" { + return fmt.Errorf("group is required; pass --group or run interactively") + } + + if flagIcon == "" { + return fmt.Errorf("icon is required; pass --icon or run interactively") + } + + if err := cmdutil.ValidateEnum("icon", flagIcon, validIcons); err != nil { + return err + } + + if flagEyebrow == "" { + return fmt.Errorf("eyebrow is required; pass --eyebrow or run interactively") + } + + if flagTitle == "" { + return fmt.Errorf("title is required; pass --title or run interactively") + } + + if flagDescription == "" { + return fmt.Errorf("description is required; pass --description or run interactively") + } + + data, err := client.Do( + createMutation, + map[string]any{ + "input": map[string]any{ + "groupId": flagGroup, + "icon": flagIcon, + "eyebrow": flagEyebrow, + "title": flagTitle, + "description": flagDescription, + }, + }, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + c := resp.CreateCompliancePortalCommitment.CompliancePortalCommitmentEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created commitment %s (%s)\n", + c.ID, + c.Title, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagGroup, "group", "", "Commitment group ID (required)") + cmd.Flags().StringVar(&flagIcon, "icon", "", "Commitment icon (required)") + cmd.Flags().StringVar(&flagEyebrow, "eyebrow", "", "Commitment eyebrow (required)") + cmd.Flags().StringVar(&flagTitle, "title", "", "Commitment title (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Commitment description (required)") + + return cmd +} diff --git a/pkg/cmd/trust-center/commitment/delete/delete.go b/pkg/cmd/trust-center/commitment/delete/delete.go new file mode 100644 index 000000000..693fdc006 --- /dev/null +++ b/pkg/cmd/trust-center/commitment/delete/delete.go @@ -0,0 +1,111 @@ +// Copyright (c) 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. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteCompliancePortalCommitmentInput!) { + deleteCompliancePortalCommitment(input: $input) { + deletedCompliancePortalCommitmentId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a compliance portal commitment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete commitment: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete commitment %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "id": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted commitment %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/trust-center/commitment/list/list.go b/pkg/cmd/trust-center/commitment/list/list.go new file mode 100644 index 000000000..af9c1cf16 --- /dev/null +++ b/pkg/cmd/trust-center/commitment/list/list.go @@ -0,0 +1,206 @@ +// Copyright (c) 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. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: CompliancePortalCommitmentOrder) { + node(id: $id) { + __typename + ... on CompliancePortalCommitmentGroup { + commitments(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + icon + eyebrow + title + description + rank + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type commitment struct { + ID string `json:"id"` + Icon string `json:"icon"` + Eyebrow string `json:"eyebrow"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagGroup string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List compliance portal commitments", + Aliases: []string{"ls"}, + Example: ` # List commitments in a group + prb trust-center commitment list --group + + # List commitments sorted by rank + prb trust-center cmt ls --group --order-by RANK`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + if flagGroup == "" { + return fmt.Errorf("group is required; pass --group") + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + variables := map[string]any{ + "id": flagGroup, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"RANK", "CREATED_AT", "UPDATED_AT"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + commitments, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[commitment], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Commitments api.Connection[commitment] `json:"commitments"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("commitment group %s not found", flagGroup) + } + + if resp.Node.Typename != "CompliancePortalCommitmentGroup" { + return nil, fmt.Errorf("expected CompliancePortalCommitmentGroup node, got %s", resp.Node.Typename) + } + + return &resp.Node.Commitments, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, commitments) + } + + if len(commitments) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No commitments found.") + return nil + } + + rows := make([][]string, 0, len(commitments)) + for _, c := range commitments { + rows = append(rows, []string{ + c.ID, + c.Icon, + c.Eyebrow, + c.Title, + fmt.Sprintf("%d", c.Rank), + }) + } + + t := cmdutil.NewTable("ID", "ICON", "EYEBROW", "TITLE", "RANK").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(commitments) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d commitments\n", + len(commitments), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagGroup, "group", "", "Commitment group ID (required)") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of commitments to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (RANK, CREATED_AT, UPDATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/trust-center/commitment/update/update.go b/pkg/cmd/trust-center/commitment/update/update.go new file mode 100644 index 000000000..dddfa4eb3 --- /dev/null +++ b/pkg/cmd/trust-center/commitment/update/update.go @@ -0,0 +1,184 @@ +// Copyright (c) 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. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +var validIcons = []string{ + "LOCK_KEY", + "EYE_SLASH", + "FINGERPRINT", + "SHIELD_WARNING", + "SHIELD_CHECK", + "SIREN", + "KEY", + "LOCK", + "CLOUD", + "DATABASE", + "GLOBE", + "EYE", + "USERS", + "CERTIFICATE", + "GAVEL", + "HEARTBEAT", + "BELL", + "BUG", + "CODE", + "SERVER", +} + +const updateMutation = ` +mutation($input: UpdateCompliancePortalCommitmentInput!) { + updateCompliancePortalCommitment(input: $input) { + compliancePortalCommitment { + id + icon + eyebrow + title + description + rank + } + } +} +` + +type updateResponse struct { + UpdateCompliancePortalCommitment struct { + CompliancePortalCommitment struct { + ID string `json:"id"` + Icon string `json:"icon"` + Eyebrow string `json:"eyebrow"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` + } `json:"compliancePortalCommitment"` + } `json:"updateCompliancePortalCommitment"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagIcon string + flagEyebrow string + flagTitle string + flagDescription string + flagRank int + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a compliance portal commitment", + Example: ` # Update a commitment title + prb trust-center commitment update --title "Encryption at rest" + + # Update icon and rank + prb trust-center cmt update --icon SHIELD_CHECK --rank 1`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("icon") { + if err := cmdutil.ValidateEnum("icon", flagIcon, validIcons); err != nil { + return err + } + + input["icon"] = flagIcon + } + + if cmd.Flags().Changed("eyebrow") { + input["eyebrow"] = flagEyebrow + } + + if cmd.Flags().Changed("title") { + input["title"] = flagTitle + } + + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + + if cmd.Flags().Changed("rank") { + input["rank"] = flagRank + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + c := resp.UpdateCompliancePortalCommitment.CompliancePortalCommitment + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated commitment %s (%s)\n", + c.ID, + c.Title, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagIcon, "icon", "", "Commitment icon") + cmd.Flags().StringVar(&flagEyebrow, "eyebrow", "", "Commitment eyebrow") + cmd.Flags().StringVar(&flagTitle, "title", "", "Commitment title") + cmd.Flags().StringVar(&flagDescription, "description", "", "Commitment description") + cmd.Flags().IntVar(&flagRank, "rank", 0, "Display rank") + + return cmd +} diff --git a/pkg/cmd/trust-center/commitmentgroup/commitmentgroup.go b/pkg/cmd/trust-center/commitmentgroup/commitmentgroup.go new file mode 100644 index 000000000..ec35b3c77 --- /dev/null +++ b/pkg/cmd/trust-center/commitmentgroup/commitmentgroup.go @@ -0,0 +1,45 @@ +// Copyright (c) 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. + +package commitmentgroup + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/trust-center/commitmentgroup/create" + "go.probo.inc/probo/pkg/cmd/trust-center/commitmentgroup/delete" + "go.probo.inc/probo/pkg/cmd/trust-center/commitmentgroup/list" + "go.probo.inc/probo/pkg/cmd/trust-center/commitmentgroup/update" +) + +func NewCmdCommitmentGroup(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "commitment-group ", + Short: "Manage compliance portal commitment groups", + Aliases: []string{"cg"}, + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/trust-center/commitmentgroup/create/create.go b/pkg/cmd/trust-center/commitmentgroup/create/create.go new file mode 100644 index 000000000..dd3e98734 --- /dev/null +++ b/pkg/cmd/trust-center/commitmentgroup/create/create.go @@ -0,0 +1,216 @@ +// Copyright (c) 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. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const trustCenterQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Organization { + trustCenter { + id + } + } + } +} +` + +const createMutation = ` +mutation($input: CreateCompliancePortalCommitmentGroupInput!) { + createCompliancePortalCommitmentGroup(input: $input) { + compliancePortalCommitmentGroupEdge { + node { + id + title + description + rank + } + } + } +} +` + +type trustCenterQueryResponse struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenter *struct { + ID string `json:"id"` + } `json:"trustCenter"` + } `json:"node"` +} + +type createResponse struct { + CreateCompliancePortalCommitmentGroup struct { + CompliancePortalCommitmentGroupEdge struct { + Node struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` + } `json:"node"` + } `json:"compliancePortalCommitmentGroupEdge"` + } `json:"createCompliancePortalCommitmentGroup"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagTitle string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a compliance portal commitment group", + Example: ` # Create a commitment group interactively + prb trust-center commitment-group create + + # Create a commitment group non-interactively + prb trust-center cg create --title "Security" --description "Our security commitments"`, + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + data, err := client.Do( + trustCenterQuery, + map[string]any{"id": flagOrg}, + ) + if err != nil { + return err + } + + var tcResp trustCenterQueryResponse + if err := json.Unmarshal(data, &tcResp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if tcResp.Node == nil { + return fmt.Errorf("organization %s not found", flagOrg) + } + + if tcResp.Node.Typename != "Organization" { + return fmt.Errorf("expected Organization node, got %s", tcResp.Node.Typename) + } + + if tcResp.Node.TrustCenter == nil { + return fmt.Errorf("trust center not found for organization %s", flagOrg) + } + + if f.IOStreams.IsInteractive() { + if flagTitle == "" { + err := huh.NewInput(). + Title("Group title"). + Value(&flagTitle). + Run() + if err != nil { + return err + } + } + + if flagDescription == "" { + err := huh.NewText(). + Title("Description"). + Value(&flagDescription). + Run() + if err != nil { + return err + } + } + } + + if flagTitle == "" { + return fmt.Errorf("title is required; pass --title or run interactively") + } + + if flagDescription == "" { + return fmt.Errorf("description is required; pass --description or run interactively") + } + + data, err = client.Do( + createMutation, + map[string]any{ + "input": map[string]any{ + "trustCenterId": tcResp.Node.TrustCenter.ID, + "title": flagTitle, + "description": flagDescription, + }, + }, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + g := resp.CreateCompliancePortalCommitmentGroup.CompliancePortalCommitmentGroupEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created commitment group %s (%s)\n", + g.ID, + g.Title, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagTitle, "title", "", "Group title (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Group description (required)") + + return cmd +} diff --git a/pkg/cmd/trust-center/commitmentgroup/delete/delete.go b/pkg/cmd/trust-center/commitmentgroup/delete/delete.go new file mode 100644 index 000000000..7812265fa --- /dev/null +++ b/pkg/cmd/trust-center/commitmentgroup/delete/delete.go @@ -0,0 +1,111 @@ +// Copyright (c) 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. + +package delete + +import ( + "fmt" + + "github.com/charmbracelet/huh" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const deleteMutation = ` +mutation($input: DeleteCompliancePortalCommitmentGroupInput!) { + deleteCompliancePortalCommitmentGroup(input: $input) { + deletedCompliancePortalCommitmentGroupId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a compliance portal commitment group", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete commitment group: confirmation required, use --yes to confirm") + } + + var confirmed bool + + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete commitment group %s?", args[0])). + Value(&confirmed). + Run() + if err != nil { + return err + } + + if !confirmed { + return nil + } + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + _, err = client.Do( + deleteMutation, + map[string]any{ + "input": map[string]any{ + "id": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted commitment group %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/trust-center/commitmentgroup/list/list.go b/pkg/cmd/trust-center/commitmentgroup/list/list.go new file mode 100644 index 000000000..6c434fe84 --- /dev/null +++ b/pkg/cmd/trust-center/commitmentgroup/list/list.go @@ -0,0 +1,212 @@ +// Copyright (c) 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. + +package list + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const listQuery = ` +query($id: ID!, $first: Int, $after: CursorKey, $orderBy: CompliancePortalCommitmentGroupOrder) { + node(id: $id) { + __typename + ... on Organization { + trustCenter { + commitmentGroups(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + title + description + rank + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } +} +` + +type commitmentGroup struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List compliance portal commitment groups", + Aliases: []string{"ls"}, + Example: ` # List commitment groups in the default organization + prb trust-center commitment-group list + + # List commitment groups sorted by rank + prb trust-center cg ls --order-by RANK`, + Args: cobra.NoArgs, + RunE: func(cmd *cobra.Command, args []string) error { + if err := cmdutil.ValidateOutputFlag(flagOutput); err != nil { + return err + } + + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + if flagOrg == "" { + flagOrg = hc.Organization + } + + if flagOrg == "" { + return fmt.Errorf("organization is required; pass --org or set a default with 'prb auth login'") + } + + variables := map[string]any{ + "id": flagOrg, + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum("order-by", flagOrderBy, []string{"RANK", "CREATED_AT", "UPDATED_AT"}); err != nil { + return err + } + + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + groups, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[commitmentGroup], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenter *struct { + CommitmentGroups api.Connection[commitmentGroup] `json:"commitmentGroups"` + } `json:"trustCenter"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + + if resp.Node == nil { + return nil, fmt.Errorf("organization %s not found", flagOrg) + } + + if resp.Node.Typename != "Organization" { + return nil, fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) + } + + if resp.Node.TrustCenter == nil { + return nil, fmt.Errorf("trust center not found for organization %s", flagOrg) + } + + return &resp.Node.TrustCenter.CommitmentGroups, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, groups) + } + + if len(groups) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No commitment groups found.") + return nil + } + + rows := make([][]string, 0, len(groups)) + for _, g := range groups { + rows = append(rows, []string{ + g.ID, + g.Title, + fmt.Sprintf("%d", g.Rank), + }) + } + + t := cmdutil.NewTable("ID", "TITLE", "RANK").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(groups) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d commitment groups\n", + len(groups), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of commitment groups to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (RANK, CREATED_AT, UPDATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/trust-center/commitmentgroup/update/update.go b/pkg/cmd/trust-center/commitmentgroup/update/update.go new file mode 100644 index 000000000..38b668cee --- /dev/null +++ b/pkg/cmd/trust-center/commitmentgroup/update/update.go @@ -0,0 +1,141 @@ +// Copyright (c) 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. + +package update + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const updateMutation = ` +mutation($input: UpdateCompliancePortalCommitmentGroupInput!) { + updateCompliancePortalCommitmentGroup(input: $input) { + compliancePortalCommitmentGroup { + id + title + description + rank + } + } +} +` + +type updateResponse struct { + UpdateCompliancePortalCommitmentGroup struct { + CompliancePortalCommitmentGroup struct { + ID string `json:"id"` + Title string `json:"title"` + Description string `json:"description"` + Rank int `json:"rank"` + } `json:"compliancePortalCommitmentGroup"` + } `json:"updateCompliancePortalCommitmentGroup"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagTitle string + flagDescription string + flagRank int + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a compliance portal commitment group", + Example: ` # Update a commitment group title + prb trust-center commitment-group update --title "Privacy" + + # Update rank + prb trust-center cg update --rank 2`, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + cfg, err := f.Config() + if err != nil { + return err + } + + host, hc, err := cfg.DefaultHost() + if err != nil { + return err + } + + client := api.NewClient( + host, + hc.Token, + "/api/console/v1/graphql", + cfg.HTTPTimeoutDuration(), + cmdutil.TokenRefreshOption(cfg, host, hc), + ) + + input := map[string]any{ + "id": args[0], + } + + if cmd.Flags().Changed("title") { + input["title"] = flagTitle + } + + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + + if cmd.Flags().Changed("rank") { + input["rank"] = flagRank + } + + if len(input) == 1 { + return fmt.Errorf("at least one field must be specified for update") + } + + data, err := client.Do( + updateMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp updateResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + g := resp.UpdateCompliancePortalCommitmentGroup.CompliancePortalCommitmentGroup + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated commitment group %s (%s)\n", + g.ID, + g.Title, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagTitle, "title", "", "Group title") + cmd.Flags().StringVar(&flagDescription, "description", "", "Group description") + cmd.Flags().IntVar(&flagRank, "rank", 0, "Display rank") + + return cmd +} diff --git a/pkg/cmd/trust-center/trust_center.go b/pkg/cmd/trust-center/trust_center.go index e14e9f133..56ae52ee3 100644 --- a/pkg/cmd/trust-center/trust_center.go +++ b/pkg/cmd/trust-center/trust_center.go @@ -23,6 +23,8 @@ package trustcenter import ( "github.com/spf13/cobra" "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/trust-center/commitment" + "go.probo.inc/probo/pkg/cmd/trust-center/commitmentgroup" "go.probo.inc/probo/pkg/cmd/trust-center/file" "go.probo.inc/probo/pkg/cmd/trust-center/reference" "go.probo.inc/probo/pkg/cmd/trust-center/update" @@ -39,6 +41,8 @@ func NewCmdTrustCenter(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(view.NewCmdView(f)) cmd.AddCommand(update.NewCmdUpdate(f)) cmd.AddCommand(reference.NewCmdReference(f)) + cmd.AddCommand(commitmentgroup.NewCmdCommitmentGroup(f)) + cmd.AddCommand(commitment.NewCmdCommitment(f)) cmd.AddCommand(file.NewCmdFile(f)) return cmd diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 4e7239f2b..a50877296 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -7083,3 +7083,231 @@ func (r *Resolver) RemoveResourceAliasTool(ctx context.Context, req *mcp.CallToo DeletedResourceID: input.ResourceID, }, nil } + +// ListCommitmentGroupsTool handles the listCommitmentGroups tool +// List all commitment groups for a trust center +func (r *Resolver) ListCommitmentGroupsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListCommitmentGroupsInput) (*mcp.CallToolResult, types.ListCommitmentGroupsOutput, error) { + scope, err := r.Authorize(ctx, input.TrustCenterID, probo.ActionCompliancePortalCommitmentGroupList) + if err != nil { + return nil, types.ListCommitmentGroupsOutput{}, err + } + + prb := r.proboSvc + + pageOrderBy := page.OrderBy[coredata.CompliancePortalCommitmentGroupOrderField]{ + Field: coredata.CompliancePortalCommitmentGroupOrderFieldRank, + Direction: page.OrderDirectionAsc, + } + + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.CompliancePortalCommitmentGroupOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := prb.CompliancePortalCommitmentGroups.ListForTrustCenterID(ctx, scope, input.TrustCenterID, cursor) + if err != nil { + return nil, types.ListCommitmentGroupsOutput{}, fmt.Errorf("cannot list commitment groups: %w", err) + } + + return nil, types.NewListCommitmentGroupsOutput(p), nil +} + +// AddCommitmentGroupTool handles the addCommitmentGroup tool +// Add a new commitment group to a trust center +func (r *Resolver) AddCommitmentGroupTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddCommitmentGroupInput) (*mcp.CallToolResult, types.AddCommitmentGroupOutput, error) { + scope, err := r.Authorize(ctx, input.TrustCenterID, probo.ActionCompliancePortalCommitmentGroupCreate) + if err != nil { + return nil, types.AddCommitmentGroupOutput{}, err + } + + prb := r.proboSvc + + group, err := prb.CompliancePortalCommitmentGroups.Create( + ctx, scope, + &probo.CreateCompliancePortalCommitmentGroupRequest{ + TrustCenterID: input.TrustCenterID, + Title: input.Title, + Description: input.Description, + }, + ) + if err != nil { + return nil, types.AddCommitmentGroupOutput{}, fmt.Errorf("cannot add commitment group: %w", err) + } + + return nil, types.AddCommitmentGroupOutput{CommitmentGroup: types.NewCommitmentGroup(group)}, nil +} + +// UpdateCommitmentGroupTool handles the updateCommitmentGroup tool +// Update an existing commitment group +func (r *Resolver) UpdateCommitmentGroupTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateCommitmentGroupInput) (*mcp.CallToolResult, types.UpdateCommitmentGroupOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionCompliancePortalCommitmentGroupUpdate) + if err != nil { + return nil, types.UpdateCommitmentGroupOutput{}, err + } + + prb := r.proboSvc + + updateReq := &probo.UpdateCompliancePortalCommitmentGroupRequest{ + ID: input.ID, + } + + if title := UnwrapOmittable(input.Title); title != nil { + updateReq.Title = *title + } + + if description := UnwrapOmittable(input.Description); description != nil { + updateReq.Description = *description + } + + if rank := UnwrapOmittable(input.Rank); rank != nil { + updateReq.Rank = *rank + } + + group, err := prb.CompliancePortalCommitmentGroups.Update(ctx, scope, updateReq) + if err != nil { + return nil, types.UpdateCommitmentGroupOutput{}, fmt.Errorf("cannot update commitment group: %w", err) + } + + return nil, types.UpdateCommitmentGroupOutput{CommitmentGroup: types.NewCommitmentGroup(group)}, nil +} + +// DeleteCommitmentGroupTool handles the deleteCommitmentGroup tool +// Delete a commitment group +func (r *Resolver) DeleteCommitmentGroupTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteCommitmentGroupInput) (*mcp.CallToolResult, types.DeleteCommitmentGroupOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionCompliancePortalCommitmentGroupDelete) + if err != nil { + return nil, types.DeleteCommitmentGroupOutput{}, err + } + + prb := r.proboSvc + + err = prb.CompliancePortalCommitmentGroups.Delete(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteCommitmentGroupOutput{}, fmt.Errorf("cannot delete commitment group: %w", err) + } + + return nil, types.DeleteCommitmentGroupOutput{DeletedCommitmentGroupID: input.ID}, nil +} + +// ListCommitmentsTool handles the listCommitments tool +// List all commitments in a commitment group +func (r *Resolver) ListCommitmentsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListCommitmentsInput) (*mcp.CallToolResult, types.ListCommitmentsOutput, error) { + scope, err := r.Authorize(ctx, input.GroupID, probo.ActionCompliancePortalCommitmentList) + if err != nil { + return nil, types.ListCommitmentsOutput{}, err + } + + prb := r.proboSvc + + pageOrderBy := page.OrderBy[coredata.CompliancePortalCommitmentOrderField]{ + Field: coredata.CompliancePortalCommitmentOrderFieldRank, + Direction: page.OrderDirectionAsc, + } + + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.CompliancePortalCommitmentOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := prb.CompliancePortalCommitments.ListForGroupID(ctx, scope, input.GroupID, cursor) + if err != nil { + return nil, types.ListCommitmentsOutput{}, fmt.Errorf("cannot list commitments: %w", err) + } + + return nil, types.NewListCommitmentsOutput(p), nil +} + +// AddCommitmentTool handles the addCommitment tool +// Add a new commitment to a commitment group +func (r *Resolver) AddCommitmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddCommitmentInput) (*mcp.CallToolResult, types.AddCommitmentOutput, error) { + scope, err := r.Authorize(ctx, input.GroupID, probo.ActionCompliancePortalCommitmentCreate) + if err != nil { + return nil, types.AddCommitmentOutput{}, err + } + + prb := r.proboSvc + + commitment, err := prb.CompliancePortalCommitments.Create( + ctx, scope, + &probo.CreateCompliancePortalCommitmentRequest{ + GroupID: input.GroupID, + Icon: input.Icon, + Eyebrow: input.Eyebrow, + Title: input.Title, + Description: input.Description, + }, + ) + if err != nil { + return nil, types.AddCommitmentOutput{}, fmt.Errorf("cannot add commitment: %w", err) + } + + return nil, types.AddCommitmentOutput{Commitment: types.NewCommitment(commitment)}, nil +} + +// UpdateCommitmentTool handles the updateCommitment tool +// Update an existing commitment +func (r *Resolver) UpdateCommitmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateCommitmentInput) (*mcp.CallToolResult, types.UpdateCommitmentOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionCompliancePortalCommitmentUpdate) + if err != nil { + return nil, types.UpdateCommitmentOutput{}, err + } + + prb := r.proboSvc + + updateReq := &probo.UpdateCompliancePortalCommitmentRequest{ + ID: input.ID, + } + + if icon := UnwrapOmittable(input.Icon); icon != nil { + updateReq.Icon = *icon + } + + if eyebrow := UnwrapOmittable(input.Eyebrow); eyebrow != nil { + updateReq.Eyebrow = *eyebrow + } + + if title := UnwrapOmittable(input.Title); title != nil { + updateReq.Title = *title + } + + if description := UnwrapOmittable(input.Description); description != nil { + updateReq.Description = *description + } + + if rank := UnwrapOmittable(input.Rank); rank != nil { + updateReq.Rank = *rank + } + + commitment, err := prb.CompliancePortalCommitments.Update(ctx, scope, updateReq) + if err != nil { + return nil, types.UpdateCommitmentOutput{}, fmt.Errorf("cannot update commitment: %w", err) + } + + return nil, types.UpdateCommitmentOutput{Commitment: types.NewCommitment(commitment)}, nil +} + +// DeleteCommitmentTool handles the deleteCommitment tool +// Delete a commitment +func (r *Resolver) DeleteCommitmentTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteCommitmentInput) (*mcp.CallToolResult, types.DeleteCommitmentOutput, error) { + scope, err := r.Authorize(ctx, input.ID, probo.ActionCompliancePortalCommitmentDelete) + if err != nil { + return nil, types.DeleteCommitmentOutput{}, err + } + + prb := r.proboSvc + + err = prb.CompliancePortalCommitments.Delete(ctx, scope, input.ID) + if err != nil { + return nil, types.DeleteCommitmentOutput{}, fmt.Errorf("cannot delete commitment: %w", err) + } + + return nil, types.DeleteCommitmentOutput{DeletedCommitmentID: input.ID}, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 58f44b23d..e5b66b509 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -9140,6 +9140,366 @@ components: $ref: "#/components/schemas/GID" description: Deleted trust center reference ID + CompliancePortalCommitmentIcon: + type: string + enum: + - LOCK_KEY + - EYE_SLASH + - FINGERPRINT + - SHIELD_WARNING + - SHIELD_CHECK + - SIREN + - KEY + - LOCK + - CLOUD + - DATABASE + - GLOBE + - EYE + - USERS + - CERTIFICATE + - GAVEL + - HEARTBEAT + - BELL + - BUG + - CODE + - SERVER + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CompliancePortalCommitmentIcon + + CompliancePortalCommitmentGroupOrderField: + type: string + enum: + - RANK + - CREATED_AT + - UPDATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CompliancePortalCommitmentGroupOrderField + + CompliancePortalCommitmentGroupOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/CompliancePortalCommitmentGroupOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + CompliancePortalCommitmentOrderField: + type: string + enum: + - RANK + - CREATED_AT + - UPDATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CompliancePortalCommitmentOrderField + + CompliancePortalCommitmentOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/CompliancePortalCommitmentOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + CommitmentGroup: + type: object + required: + - id + - title + - description + - rank + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + title: + type: string + description: + type: string + rank: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + Commitment: + type: object + required: + - id + - group_id + - icon + - eyebrow + - title + - description + - rank + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + group_id: + $ref: "#/components/schemas/GID" + icon: + $ref: "#/components/schemas/CompliancePortalCommitmentIcon" + eyebrow: + type: string + title: + type: string + description: + type: string + rank: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ListCommitmentGroupsInput: + type: object + required: + - trust_center_id + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + order_by: + $ref: "#/components/schemas/CompliancePortalCommitmentGroupOrderBy" + description: Commitment group order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListCommitmentGroupsOutput: + type: object + required: + - commitment_groups + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + commitment_groups: + type: array + items: + $ref: "#/components/schemas/CommitmentGroup" + + AddCommitmentGroupInput: + type: object + required: + - trust_center_id + - title + - description + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + title: + type: string + description: Group title + description: + type: string + description: Group description + + AddCommitmentGroupOutput: + type: object + required: + - commitment_group + properties: + commitment_group: + $ref: "#/components/schemas/CommitmentGroup" + + UpdateCommitmentGroupInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Commitment group ID + title: + type: + - string + - "null" + description: Group title + go.probo.inc/mcpgen/omittable: true + description: + type: + - string + - "null" + description: Group description + go.probo.inc/mcpgen/omittable: true + rank: + type: + - integer + - "null" + description: Group rank + go.probo.inc/mcpgen/omittable: true + + UpdateCommitmentGroupOutput: + type: object + required: + - commitment_group + properties: + commitment_group: + $ref: "#/components/schemas/CommitmentGroup" + + DeleteCommitmentGroupInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Commitment group ID + + DeleteCommitmentGroupOutput: + type: object + required: + - deleted_commitment_group_id + properties: + deleted_commitment_group_id: + $ref: "#/components/schemas/GID" + description: Deleted commitment group ID + + ListCommitmentsInput: + type: object + required: + - group_id + properties: + group_id: + $ref: "#/components/schemas/GID" + description: Commitment group ID + order_by: + $ref: "#/components/schemas/CompliancePortalCommitmentOrderBy" + description: Commitment order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListCommitmentsOutput: + type: object + required: + - commitments + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + commitments: + type: array + items: + $ref: "#/components/schemas/Commitment" + + AddCommitmentInput: + type: object + required: + - group_id + - icon + - eyebrow + - title + - description + properties: + group_id: + $ref: "#/components/schemas/GID" + description: Commitment group ID + icon: + $ref: "#/components/schemas/CompliancePortalCommitmentIcon" + description: Commitment icon + eyebrow: + type: string + description: Commitment eyebrow + title: + type: string + description: Commitment title + description: + type: string + description: Commitment description + + AddCommitmentOutput: + type: object + required: + - commitment + properties: + commitment: + $ref: "#/components/schemas/Commitment" + + UpdateCommitmentInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Commitment ID + icon: + anyOf: + - $ref: "#/components/schemas/CompliancePortalCommitmentIcon" + - type: "null" + description: Commitment icon + go.probo.inc/mcpgen/omittable: true + eyebrow: + type: + - string + - "null" + description: Commitment eyebrow + go.probo.inc/mcpgen/omittable: true + title: + type: + - string + - "null" + description: Commitment title + go.probo.inc/mcpgen/omittable: true + description: + type: + - string + - "null" + description: Commitment description + go.probo.inc/mcpgen/omittable: true + rank: + type: + - integer + - "null" + description: Commitment rank + go.probo.inc/mcpgen/omittable: true + + UpdateCommitmentOutput: + type: object + required: + - commitment + properties: + commitment: + $ref: "#/components/schemas/Commitment" + + DeleteCommitmentInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Commitment ID + + DeleteCommitmentOutput: + type: object + required: + - deleted_commitment_id + properties: + deleted_commitment_id: + $ref: "#/components/schemas/GID" + description: Deleted commitment ID + ResourceAlias: type: object required: @@ -13656,6 +14016,74 @@ tools: $ref: "#/components/schemas/DeleteTrustCenterReferenceInput" outputSchema: $ref: "#/components/schemas/DeleteTrustCenterReferenceOutput" + - name: listCommitmentGroups + description: List all commitment groups for a trust center + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListCommitmentGroupsInput" + outputSchema: + $ref: "#/components/schemas/ListCommitmentGroupsOutput" + - name: addCommitmentGroup + description: Add a new commitment group to a trust center + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddCommitmentGroupInput" + outputSchema: + $ref: "#/components/schemas/AddCommitmentGroupOutput" + - name: updateCommitmentGroup + description: Update an existing commitment group + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateCommitmentGroupInput" + outputSchema: + $ref: "#/components/schemas/UpdateCommitmentGroupOutput" + - name: deleteCommitmentGroup + description: Delete a commitment group + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteCommitmentGroupInput" + outputSchema: + $ref: "#/components/schemas/DeleteCommitmentGroupOutput" + - name: listCommitments + description: List all commitments in a commitment group + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListCommitmentsInput" + outputSchema: + $ref: "#/components/schemas/ListCommitmentsOutput" + - name: addCommitment + description: Add a new commitment to a commitment group + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddCommitmentInput" + outputSchema: + $ref: "#/components/schemas/AddCommitmentOutput" + - name: updateCommitment + description: Update an existing commitment + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateCommitmentInput" + outputSchema: + $ref: "#/components/schemas/UpdateCommitmentOutput" + - name: deleteCommitment + description: Delete a commitment + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteCommitmentInput" + outputSchema: + $ref: "#/components/schemas/DeleteCommitmentOutput" - name: setResourceAlias description: Set a resource alias for a resource hints: diff --git a/pkg/server/api/mcp/v1/types/commitment.go b/pkg/server/api/mcp/v1/types/commitment.go new file mode 100644 index 000000000..02a1dcae1 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/commitment.go @@ -0,0 +1,93 @@ +// Copyright (c) 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. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewCommitmentGroup(g *coredata.CompliancePortalCommitmentGroup) *CommitmentGroup { + return &CommitmentGroup{ + ID: g.ID, + Title: g.Title, + Description: g.Description, + Rank: g.Rank, + CreatedAt: g.CreatedAt, + UpdatedAt: g.UpdatedAt, + } +} + +func NewListCommitmentGroupsOutput( + p *page.Page[*coredata.CompliancePortalCommitmentGroup, coredata.CompliancePortalCommitmentGroupOrderField], +) ListCommitmentGroupsOutput { + groups := make([]*CommitmentGroup, 0, len(p.Data)) + for _, g := range p.Data { + groups = append(groups, NewCommitmentGroup(g)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListCommitmentGroupsOutput{ + NextCursor: nextCursor, + CommitmentGroups: groups, + } +} + +func NewCommitment(c *coredata.CompliancePortalCommitment) *Commitment { + return &Commitment{ + ID: c.ID, + GroupID: c.GroupID, + Icon: c.Icon, + Eyebrow: c.Eyebrow, + Title: c.Title, + Description: c.Description, + Rank: c.Rank, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} + +func NewListCommitmentsOutput( + p *page.Page[*coredata.CompliancePortalCommitment, coredata.CompliancePortalCommitmentOrderField], +) ListCommitmentsOutput { + commitments := make([]*Commitment, 0, len(p.Data)) + for _, c := range p.Data { + commitments = append(commitments, NewCommitment(c)) + } + + var nextCursor *page.CursorKey + + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListCommitmentsOutput{ + NextCursor: nextCursor, + Commitments: commitments, + } +}