diff --git a/e2e/internal/testutil/mcp.go b/e2e/internal/testutil/mcp.go new file mode 100644 index 000000000..3e6222529 --- /dev/null +++ b/e2e/internal/testutil/mcp.go @@ -0,0 +1,222 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package testutil + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "time" + + "github.com/stretchr/testify/require" +) + +// MCPClient wraps an authenticated MCP session for e2e testing. +type MCPClient struct { + t require.TestingT + baseURL string + apiToken string + sessionID string + httpClient *http.Client +} + +// CreateAPIKey creates a personal API key via the connect GraphQL API. +// It returns the raw bearer token string. +func (c *Client) CreateAPIKey(name string) string { + const query = ` + mutation($input: CreatePersonalAPIKeyInput!) { + createPersonalAPIKey(input: $input) { + token + } + } + ` + + var result struct { + CreatePersonalAPIKey struct { + Token string `json:"token"` + } `json:"createPersonalAPIKey"` + } + + err := c.ExecuteConnect(query, map[string]any{ + "input": map[string]any{ + "name": name, + "expiresAt": time.Now().Add(1 * time.Hour).Format(time.RFC3339), + }, + }, &result) + require.NoError(c.T, err, "createPersonalAPIKey failed") + require.NotEmpty(c.T, result.CreatePersonalAPIKey.Token, "API key token is empty") + + return result.CreatePersonalAPIKey.Token +} + +// NewMCPClient creates an MCP client authenticated with an API key. +// It initializes an MCP session and stores the session ID. +func NewMCPClient(t require.TestingT, owner *Client) *MCPClient { + token := owner.CreateAPIKey("e2e-mcp-test") + + mc := &MCPClient{ + t: t, + baseURL: owner.BaseURL() + "/mcp/v1", + apiToken: token, + httpClient: &http.Client{ + Timeout: 30 * time.Second, + }, + } + + mc.initialize() + + return mc +} + +// jsonrpcRequest is a JSON-RPC 2.0 request. +type jsonrpcRequest struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Method string `json:"method"` + Params any `json:"params,omitempty"` +} + +// jsonrpcResponse is a JSON-RPC 2.0 response. +type jsonrpcResponse struct { + JSONRPC string `json:"jsonrpc"` + ID int `json:"id"` + Result json.RawMessage `json:"result,omitempty"` + Error *jsonrpcError `json:"error,omitempty"` +} + +type jsonrpcError struct { + Code int `json:"code"` + Message string `json:"message"` +} + +func (e *jsonrpcError) Error() string { + return fmt.Sprintf("JSON-RPC error %d: %s", e.Code, e.Message) +} + +func (mc *MCPClient) doRequest(method string, params any) (json.RawMessage, error) { + reqBody := jsonrpcRequest{ + JSONRPC: "2.0", + ID: 1, + Method: method, + Params: params, + } + + body, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("cannot marshal request: %w", err) + } + + req, err := http.NewRequest("POST", mc.baseURL, bytes.NewReader(body)) + if err != nil { + return nil, fmt.Errorf("cannot create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("Accept", "application/json, text/event-stream") + req.Header.Set("Authorization", "Bearer "+mc.apiToken) + if mc.sessionID != "" { + req.Header.Set("Mcp-Session-Id", mc.sessionID) + } + + resp, err := mc.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("request failed: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read response: %w", err) + } + + if resp.StatusCode != http.StatusOK { + return nil, fmt.Errorf("unexpected status %d: %s", resp.StatusCode, string(respBody)) + } + + // Store session ID from response + if sid := resp.Header.Get("Mcp-Session-Id"); sid != "" { + mc.sessionID = sid + } + + var rpcResp jsonrpcResponse + if err := json.Unmarshal(respBody, &rpcResp); err != nil { + return nil, fmt.Errorf("cannot decode response: %w (body: %s)", err, string(respBody)) + } + + if rpcResp.Error != nil { + return nil, rpcResp.Error + } + + return rpcResp.Result, nil +} + +func (mc *MCPClient) initialize() { + result, err := mc.doRequest("initialize", map[string]any{ + "protocolVersion": "2025-03-26", + "capabilities": map[string]any{}, + "clientInfo": map[string]any{ + "name": "probo-e2e-test", + "version": "1.0.0", + }, + }) + require.NoError(mc.t, err, "MCP initialize failed") + require.NotNil(mc.t, result, "MCP initialize returned nil result") +} + +// MCPToolResult represents the result of a tools/call response. +type MCPToolResult struct { + Content []MCPContent `json:"content"` + IsError bool `json:"isError"` +} + +// MCPContent represents content within a tool result. +type MCPContent struct { + Type string `json:"type"` + Text json.RawMessage `json:"text"` +} + +// CallTool invokes an MCP tool and returns the parsed result. +func (mc *MCPClient) CallTool(toolName string, args map[string]any) *MCPToolResult { + result, err := mc.doRequest("tools/call", map[string]any{ + "name": toolName, + "arguments": args, + }) + require.NoError(mc.t, err, "MCP tools/call %s failed", toolName) + + var toolResult MCPToolResult + err = json.Unmarshal(result, &toolResult) + require.NoError(mc.t, err, "cannot unmarshal tool result for %s", toolName) + + return &toolResult +} + +// CallToolInto invokes an MCP tool and unmarshals the first text content into dest. +func (mc *MCPClient) CallToolInto(toolName string, args map[string]any, dest any) { + tr := mc.CallTool(toolName, args) + require.False(mc.t, tr.IsError, "tool %s returned error: %v", toolName, tr.Content) + require.NotEmpty(mc.t, tr.Content, "tool %s returned no content", toolName) + + // The text field in MCP content is a JSON-encoded string of the output. + // First unmarshal the raw JSON to get the string. + var textStr string + err := json.Unmarshal(tr.Content[0].Text, &textStr) + require.NoError(mc.t, err, "cannot unmarshal text content for %s", toolName) + + // Then unmarshal that string as JSON into the destination. + err = json.Unmarshal([]byte(textStr), dest) + require.NoError(mc.t, err, "cannot unmarshal tool output for %s", toolName) +} diff --git a/e2e/mcp/asset_test.go b/e2e/mcp/asset_test.go new file mode 100644 index 000000000..8c0bcc531 --- /dev/null +++ b/e2e/mcp/asset_test.go @@ -0,0 +1,95 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Asset_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + profileID := factory.CreateUser(owner) + + // Create + var addResult struct { + Asset struct { + ID string `json:"id"` + Name string `json:"name"` + AssetType string `json:"assetType"` + } `json:"asset"` + } + mc.CallToolInto("addAsset", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Asset"), + "amount": 5, + "ownerId": profileID, + "assetType": "VIRTUAL", + "dataTypesStored": "PII", + }, &addResult) + require.NotEmpty(t, addResult.Asset.ID) + assert.Equal(t, "VIRTUAL", addResult.Asset.AssetType) + + // Get + var getResult struct { + Asset struct { + ID string `json:"id"` + } `json:"asset"` + } + mc.CallToolInto("getAsset", map[string]any{ + "id": addResult.Asset.ID, + }, &getResult) + assert.Equal(t, addResult.Asset.ID, getResult.Asset.ID) + + // Update + var updateResult struct { + Asset struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"asset"` + } + mc.CallToolInto("updateAsset", map[string]any{ + "id": addResult.Asset.ID, + "name": "Updated Asset", + }, &updateResult) + assert.Equal(t, "Updated Asset", updateResult.Asset.Name) + + // List + var listResult struct { + Assets []struct { + ID string `json:"id"` + } `json:"assets"` + } + mc.CallToolInto("listAssets", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Assets) + + // Delete + var deleteResult struct { + DeletedAssetID string `json:"deletedAssetId"` + } + mc.CallToolInto("deleteAsset", map[string]any{ + "id": addResult.Asset.ID, + }, &deleteResult) + assert.Equal(t, addResult.Asset.ID, deleteResult.DeletedAssetID) +} diff --git a/e2e/mcp/audit_test.go b/e2e/mcp/audit_test.go new file mode 100644 index 000000000..cda7603f5 --- /dev/null +++ b/e2e/mcp/audit_test.go @@ -0,0 +1,109 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Audit_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + frameworkID := factory.CreateFramework(owner) + + // Create + var addResult struct { + Audit struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"audit"` + } + mc.CallToolInto("addAudit", map[string]any{ + "frameworkId": frameworkID, + "name": factory.SafeName("Audit"), + }, &addResult) + require.NotEmpty(t, addResult.Audit.ID) + + // Get + var getResult struct { + Audit struct { + ID string `json:"id"` + } `json:"audit"` + } + mc.CallToolInto("getAudit", map[string]any{ + "id": addResult.Audit.ID, + }, &getResult) + assert.Equal(t, addResult.Audit.ID, getResult.Audit.ID) + + // Update + var updateResult struct { + Audit struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"audit"` + } + mc.CallToolInto("updateAudit", map[string]any{ + "id": addResult.Audit.ID, + "name": "Updated Audit", + }, &updateResult) + assert.Equal(t, "Updated Audit", updateResult.Audit.Name) + + // List + var listResult struct { + Audits []struct { + ID string `json:"id"` + } `json:"audits"` + } + mc.CallToolInto("listAudits", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Audits) + + // Delete + var deleteResult struct { + DeletedAuditID string `json:"deletedAuditId"` + } + mc.CallToolInto("deleteAudit", map[string]any{ + "id": addResult.Audit.ID, + }, &deleteResult) + assert.Equal(t, addResult.Audit.ID, deleteResult.DeletedAuditID) +} + +func TestMCP_AuditLog(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Creating something generates audit log entries + factory.CreateVendor(owner) + + var listResult struct { + AuditLogEntries []struct { + ID string `json:"id"` + } `json:"auditLogEntries"` + } + mc.CallToolInto("listAuditLogEntries", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.AuditLogEntries) +} diff --git a/e2e/mcp/datum_test.go b/e2e/mcp/datum_test.go new file mode 100644 index 000000000..d86c19db7 --- /dev/null +++ b/e2e/mcp/datum_test.go @@ -0,0 +1,91 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Datum_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + profileID := factory.CreateUser(owner) + + // Create + var addResult struct { + Datum struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"datum"` + } + mc.CallToolInto("addDatum", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Datum"), + "ownerId": profileID, + "dataClassification": "PUBLIC", + }, &addResult) + require.NotEmpty(t, addResult.Datum.ID) + + // Get + var getResult struct { + Datum struct { + ID string `json:"id"` + } `json:"datum"` + } + mc.CallToolInto("getDatum", map[string]any{ + "id": addResult.Datum.ID, + }, &getResult) + assert.Equal(t, addResult.Datum.ID, getResult.Datum.ID) + + // Update + var updateResult struct { + Datum struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"datum"` + } + mc.CallToolInto("updateDatum", map[string]any{ + "id": addResult.Datum.ID, + "name": "Updated Datum", + }, &updateResult) + assert.Equal(t, "Updated Datum", updateResult.Datum.Name) + + // List + var listResult struct { + Data []struct { + ID string `json:"id"` + } `json:"data"` + } + mc.CallToolInto("listData", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Data) + + // Delete + var deleteResult struct { + DeletedDatumID string `json:"deletedDatumId"` + } + mc.CallToolInto("deleteDatum", map[string]any{ + "id": addResult.Datum.ID, + }, &deleteResult) + assert.Equal(t, addResult.Datum.ID, deleteResult.DeletedDatumID) +} diff --git a/e2e/mcp/document_test.go b/e2e/mcp/document_test.go new file mode 100644 index 000000000..f6b237900 --- /dev/null +++ b/e2e/mcp/document_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Document_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Document struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"document"` + } + mc.CallToolInto("addDocument", map[string]any{ + "organizationId": orgID, + "title": factory.SafeName("Document"), + "documentType": "POLICY", + }, &addResult) + require.NotEmpty(t, addResult.Document.ID) + + // Get + var getResult struct { + Document struct { + ID string `json:"id"` + } `json:"document"` + } + mc.CallToolInto("getDocument", map[string]any{ + "id": addResult.Document.ID, + }, &getResult) + assert.Equal(t, addResult.Document.ID, getResult.Document.ID) + + // Update + var updateResult struct { + Document struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"document"` + } + mc.CallToolInto("updateDocument", map[string]any{ + "id": addResult.Document.ID, + "title": "Updated Document", + }, &updateResult) + assert.Equal(t, "Updated Document", updateResult.Document.Title) + + // List + var listResult struct { + Documents []struct { + ID string `json:"id"` + } `json:"documents"` + } + mc.CallToolInto("listDocuments", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Documents) + + // Delete + var deleteResult struct { + DeletedDocumentID string `json:"deletedDocumentId"` + } + mc.CallToolInto("deleteDocument", map[string]any{ + "id": addResult.Document.ID, + }, &deleteResult) + assert.Equal(t, addResult.Document.ID, deleteResult.DeletedDocumentID) +} diff --git a/e2e/mcp/dpia_tia_test.go b/e2e/mcp/dpia_tia_test.go new file mode 100644 index 000000000..8d077b2ad --- /dev/null +++ b/e2e/mcp/dpia_tia_test.go @@ -0,0 +1,152 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_DPIA_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + paID := factory.CreateProcessingActivity(owner) + + // Create + var addResult struct { + DataProtectionImpactAssessment struct { + ID string `json:"id"` + } `json:"dataProtectionImpactAssessment"` + } + mc.CallToolInto("addDataProtectionImpactAssessment", map[string]any{ + "processingActivityId": paID, + }, &addResult) + require.NotEmpty(t, addResult.DataProtectionImpactAssessment.ID) + + // Get + var getResult struct { + DataProtectionImpactAssessment struct { + ID string `json:"id"` + } `json:"dataProtectionImpactAssessment"` + } + mc.CallToolInto("getDataProtectionImpactAssessment", map[string]any{ + "id": addResult.DataProtectionImpactAssessment.ID, + }, &getResult) + assert.Equal(t, addResult.DataProtectionImpactAssessment.ID, getResult.DataProtectionImpactAssessment.ID) + + // Update + var updateResult struct { + DataProtectionImpactAssessment struct { + ID string `json:"id"` + Description string `json:"description"` + } `json:"dataProtectionImpactAssessment"` + } + mc.CallToolInto("updateDataProtectionImpactAssessment", map[string]any{ + "id": addResult.DataProtectionImpactAssessment.ID, + "description": "Updated DPIA", + }, &updateResult) + assert.Equal(t, "Updated DPIA", updateResult.DataProtectionImpactAssessment.Description) + + // List + var listResult struct { + DataProtectionImpactAssessments []struct { + ID string `json:"id"` + } `json:"dataProtectionImpactAssessments"` + } + mc.CallToolInto("listDataProtectionImpactAssessments", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.DataProtectionImpactAssessments) + + // Delete + var deleteResult struct { + DeletedDataProtectionImpactAssessmentID string `json:"deletedDataProtectionImpactAssessmentId"` + } + mc.CallToolInto("deleteDataProtectionImpactAssessment", map[string]any{ + "id": addResult.DataProtectionImpactAssessment.ID, + }, &deleteResult) + assert.Equal(t, addResult.DataProtectionImpactAssessment.ID, deleteResult.DeletedDataProtectionImpactAssessmentID) +} + +func TestMCP_TIA_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + paID := factory.CreateProcessingActivity(owner) + + // Create + var addResult struct { + TransferImpactAssessment struct { + ID string `json:"id"` + } `json:"transferImpactAssessment"` + } + mc.CallToolInto("addTransferImpactAssessment", map[string]any{ + "processingActivityId": paID, + }, &addResult) + require.NotEmpty(t, addResult.TransferImpactAssessment.ID) + + // Get + var getResult struct { + TransferImpactAssessment struct { + ID string `json:"id"` + } `json:"transferImpactAssessment"` + } + mc.CallToolInto("getTransferImpactAssessment", map[string]any{ + "id": addResult.TransferImpactAssessment.ID, + }, &getResult) + assert.Equal(t, addResult.TransferImpactAssessment.ID, getResult.TransferImpactAssessment.ID) + + // Update + var updateResult struct { + TransferImpactAssessment struct { + ID string `json:"id"` + DataSubjects string `json:"dataSubjects"` + } `json:"transferImpactAssessment"` + } + mc.CallToolInto("updateTransferImpactAssessment", map[string]any{ + "id": addResult.TransferImpactAssessment.ID, + "dataSubjects": "EU Residents", + }, &updateResult) + assert.Equal(t, "EU Residents", updateResult.TransferImpactAssessment.DataSubjects) + + // List + var listResult struct { + TransferImpactAssessments []struct { + ID string `json:"id"` + } `json:"transferImpactAssessments"` + } + mc.CallToolInto("listTransferImpactAssessments", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.TransferImpactAssessments) + + // Delete + var deleteResult struct { + DeletedTransferImpactAssessmentID string `json:"deletedTransferImpactAssessmentId"` + } + mc.CallToolInto("deleteTransferImpactAssessment", map[string]any{ + "id": addResult.TransferImpactAssessment.ID, + }, &deleteResult) + assert.Equal(t, addResult.TransferImpactAssessment.ID, deleteResult.DeletedTransferImpactAssessmentID) +} diff --git a/e2e/mcp/finding_test.go b/e2e/mcp/finding_test.go new file mode 100644 index 000000000..2972a1581 --- /dev/null +++ b/e2e/mcp/finding_test.go @@ -0,0 +1,88 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Finding_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Finding struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"finding"` + } + mc.CallToolInto("addFinding", map[string]any{ + "organizationId": orgID, + "title": factory.SafeName("Finding"), + }, &addResult) + require.NotEmpty(t, addResult.Finding.ID) + + // Get + var getResult struct { + Finding struct { + ID string `json:"id"` + } `json:"finding"` + } + mc.CallToolInto("getFinding", map[string]any{ + "id": addResult.Finding.ID, + }, &getResult) + assert.Equal(t, addResult.Finding.ID, getResult.Finding.ID) + + // Update + var updateResult struct { + Finding struct { + ID string `json:"id"` + Title string `json:"title"` + } `json:"finding"` + } + mc.CallToolInto("updateFinding", map[string]any{ + "id": addResult.Finding.ID, + "title": "Updated Finding", + }, &updateResult) + assert.Equal(t, "Updated Finding", updateResult.Finding.Title) + + // List + var listResult struct { + Findings []struct { + ID string `json:"id"` + } `json:"findings"` + } + mc.CallToolInto("listFindings", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Findings) + + // Delete + var deleteResult struct { + DeletedFindingID string `json:"deletedFindingId"` + } + mc.CallToolInto("deleteFinding", map[string]any{ + "id": addResult.Finding.ID, + }, &deleteResult) + assert.Equal(t, addResult.Finding.ID, deleteResult.DeletedFindingID) +} diff --git a/e2e/mcp/framework_test.go b/e2e/mcp/framework_test.go new file mode 100644 index 000000000..a651b4b02 --- /dev/null +++ b/e2e/mcp/framework_test.go @@ -0,0 +1,135 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Framework_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Framework struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"framework"` + } + mc.CallToolInto("addFramework", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Framework"), + }, &addResult) + require.NotEmpty(t, addResult.Framework.ID) + + // Get + var getResult struct { + Framework struct { + ID string `json:"id"` + } `json:"framework"` + } + mc.CallToolInto("getFramework", map[string]any{ + "id": addResult.Framework.ID, + }, &getResult) + assert.Equal(t, addResult.Framework.ID, getResult.Framework.ID) + + // Update + var updateResult struct { + Framework struct { + ID string `json:"id"` + Description string `json:"description"` + } `json:"framework"` + } + mc.CallToolInto("updateFramework", map[string]any{ + "id": addResult.Framework.ID, + "description": "Updated description", + }, &updateResult) + assert.Equal(t, "Updated description", updateResult.Framework.Description) + + // List + var listResult struct { + Frameworks []struct { + ID string `json:"id"` + } `json:"frameworks"` + } + mc.CallToolInto("listFrameworks", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Frameworks) +} + +func TestMCP_Control_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + + frameworkID := factory.CreateFramework(owner) + + // Create + var addResult struct { + Control struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"control"` + } + mc.CallToolInto("addControl", map[string]any{ + "frameworkId": frameworkID, + "name": factory.SafeName("Control"), + }, &addResult) + require.NotEmpty(t, addResult.Control.ID) + + // Get + var getResult struct { + Control struct { + ID string `json:"id"` + } `json:"control"` + } + mc.CallToolInto("getControl", map[string]any{ + "id": addResult.Control.ID, + }, &getResult) + assert.Equal(t, addResult.Control.ID, getResult.Control.ID) + + // Update + var updateResult struct { + Control struct { + ID string `json:"id"` + Description string `json:"description"` + } `json:"control"` + } + mc.CallToolInto("updateControl", map[string]any{ + "id": addResult.Control.ID, + "description": "Updated control", + }, &updateResult) + assert.Equal(t, "Updated control", updateResult.Control.Description) + + // List + var listResult struct { + Controls []struct { + ID string `json:"id"` + } `json:"controls"` + } + mc.CallToolInto("listControls", map[string]any{ + "frameworkId": frameworkID, + }, &listResult) + assert.NotEmpty(t, listResult.Controls) +} diff --git a/e2e/mcp/main_test.go b/e2e/mcp/main_test.go new file mode 100644 index 000000000..369e3cfe8 --- /dev/null +++ b/e2e/mcp/main_test.go @@ -0,0 +1,29 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package mcp_test + +import ( + "os" + "testing" + + "go.probo.inc/probo/e2e/internal/testutil" +) + +func TestMain(m *testing.M) { + testutil.Setup() + code := m.Run() + testutil.Teardown() + os.Exit(code) +} diff --git a/e2e/mcp/measure_test.go b/e2e/mcp/measure_test.go new file mode 100644 index 000000000..68c05f760 --- /dev/null +++ b/e2e/mcp/measure_test.go @@ -0,0 +1,110 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Measure_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Measure struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"measure"` + } + mc.CallToolInto("addMeasure", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Measure"), + }, &addResult) + require.NotEmpty(t, addResult.Measure.ID) + + // Get + var getResult struct { + Measure struct { + ID string `json:"id"` + } `json:"measure"` + } + mc.CallToolInto("getMeasure", map[string]any{ + "id": addResult.Measure.ID, + }, &getResult) + assert.Equal(t, addResult.Measure.ID, getResult.Measure.ID) + + // Update + var updateResult struct { + Measure struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"measure"` + } + mc.CallToolInto("updateMeasure", map[string]any{ + "id": addResult.Measure.ID, + "name": "Updated Measure", + }, &updateResult) + assert.Equal(t, "Updated Measure", updateResult.Measure.Name) + + // List + var listResult struct { + Measures []struct { + ID string `json:"id"` + } `json:"measures"` + } + mc.CallToolInto("listMeasures", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Measures) + + // Sub-resources (empty lists are fine, just verify the tools work) + var risksResult struct { + Risks []struct{ ID string } `json:"risks"` + } + mc.CallToolInto("listMeasureRisks", map[string]any{ + "measureId": addResult.Measure.ID, + }, &risksResult) + + var controlsResult struct { + Controls []struct{ ID string } `json:"controls"` + } + mc.CallToolInto("listMeasureControls", map[string]any{ + "measureId": addResult.Measure.ID, + }, &controlsResult) + + var tasksResult struct { + Tasks []struct{ ID string } `json:"tasks"` + } + mc.CallToolInto("listMeasureTasks", map[string]any{ + "measureId": addResult.Measure.ID, + }, &tasksResult) + + // Delete + var deleteResult struct { + DeletedMeasureID string `json:"deletedMeasureId"` + } + mc.CallToolInto("deleteMeasure", map[string]any{ + "id": addResult.Measure.ID, + }, &deleteResult) + assert.Equal(t, addResult.Measure.ID, deleteResult.DeletedMeasureID) +} diff --git a/e2e/mcp/obligation_test.go b/e2e/mcp/obligation_test.go new file mode 100644 index 000000000..b4b84db25 --- /dev/null +++ b/e2e/mcp/obligation_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Obligation_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Obligation struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"obligation"` + } + mc.CallToolInto("addObligation", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Obligation"), + "description": "Test obligation", + }, &addResult) + require.NotEmpty(t, addResult.Obligation.ID) + + // Get + var getResult struct { + Obligation struct { + ID string `json:"id"` + } `json:"obligation"` + } + mc.CallToolInto("getObligation", map[string]any{ + "id": addResult.Obligation.ID, + }, &getResult) + assert.Equal(t, addResult.Obligation.ID, getResult.Obligation.ID) + + // Update + var updateResult struct { + Obligation struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"obligation"` + } + mc.CallToolInto("updateObligation", map[string]any{ + "id": addResult.Obligation.ID, + "name": "Updated Obligation", + }, &updateResult) + assert.Equal(t, "Updated Obligation", updateResult.Obligation.Name) + + // List + var listResult struct { + Obligations []struct { + ID string `json:"id"` + } `json:"obligations"` + } + mc.CallToolInto("listObligations", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Obligations) + + // Delete + var deleteResult struct { + DeletedObligationID string `json:"deletedObligationId"` + } + mc.CallToolInto("deleteObligation", map[string]any{ + "id": addResult.Obligation.ID, + }, &deleteResult) + assert.Equal(t, addResult.Obligation.ID, deleteResult.DeletedObligationID) +} diff --git a/e2e/mcp/organization_context_test.go b/e2e/mcp/organization_context_test.go new file mode 100644 index 000000000..a415eb25b --- /dev/null +++ b/e2e/mcp/organization_context_test.go @@ -0,0 +1,52 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package mcp_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.probo.inc/probo/e2e/internal/testutil" +) + +func TestMCP_OrganizationContext(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get + var getResult struct { + OrganizationContext struct { + ID string `json:"id"` + } `json:"organizationContext"` + } + mc.CallToolInto("getOrganizationContext", map[string]any{ + "organizationId": orgID, + }, &getResult) + assert.NotEmpty(t, getResult.OrganizationContext.ID) + + // Update + var updateResult struct { + OrganizationContext struct { + ID string `json:"id"` + } `json:"organizationContext"` + } + mc.CallToolInto("updateOrganizationContext", map[string]any{ + "id": getResult.OrganizationContext.ID, + "companyLegalName": "Test Company LLC", + }, &updateResult) + assert.Equal(t, getResult.OrganizationContext.ID, updateResult.OrganizationContext.ID) +} diff --git a/e2e/mcp/organization_test.go b/e2e/mcp/organization_test.go new file mode 100644 index 000000000..bf3b837d5 --- /dev/null +++ b/e2e/mcp/organization_test.go @@ -0,0 +1,38 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package mcp_test + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "go.probo.inc/probo/e2e/internal/testutil" +) + +func TestMCP_ListOrganizations(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + + var result struct { + Organizations []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"organizations"` + } + mc.CallToolInto("listOrganizations", map[string]any{}, &result) + + assert.NotEmpty(t, result.Organizations) +} diff --git a/e2e/mcp/processing_activity_test.go b/e2e/mcp/processing_activity_test.go new file mode 100644 index 000000000..0b1260612 --- /dev/null +++ b/e2e/mcp/processing_activity_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_ProcessingActivity_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + ProcessingActivity struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"processingActivity"` + } + mc.CallToolInto("addProcessingActivity", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("PA"), + "lawfulBasis": "CONSENT", + }, &addResult) + require.NotEmpty(t, addResult.ProcessingActivity.ID) + + // Get + var getResult struct { + ProcessingActivity struct { + ID string `json:"id"` + } `json:"processingActivity"` + } + mc.CallToolInto("getProcessingActivity", map[string]any{ + "id": addResult.ProcessingActivity.ID, + }, &getResult) + assert.Equal(t, addResult.ProcessingActivity.ID, getResult.ProcessingActivity.ID) + + // Update + var updateResult struct { + ProcessingActivity struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"processingActivity"` + } + mc.CallToolInto("updateProcessingActivity", map[string]any{ + "id": addResult.ProcessingActivity.ID, + "name": "Updated PA", + }, &updateResult) + assert.Equal(t, "Updated PA", updateResult.ProcessingActivity.Name) + + // List + var listResult struct { + ProcessingActivities []struct { + ID string `json:"id"` + } `json:"processingActivities"` + } + mc.CallToolInto("listProcessingActivities", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.ProcessingActivities) + + // Delete + var deleteResult struct { + DeletedProcessingActivityID string `json:"deletedProcessingActivityId"` + } + mc.CallToolInto("deleteProcessingActivity", map[string]any{ + "id": addResult.ProcessingActivity.ID, + }, &deleteResult) + assert.Equal(t, addResult.ProcessingActivity.ID, deleteResult.DeletedProcessingActivityID) +} diff --git a/e2e/mcp/rights_request_test.go b/e2e/mcp/rights_request_test.go new file mode 100644 index 000000000..6ec656692 --- /dev/null +++ b/e2e/mcp/rights_request_test.go @@ -0,0 +1,204 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 rightsRequest struct { + ID string `json:"id"` + RequestType string `json:"requestType"` + RequestState string `json:"requestState"` + DataSubject string `json:"dataSubject"` + Contact *string `json:"contact"` + Details *string `json:"details"` +} + +func TestMCP_AddRightsRequest(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + var result struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("addRightsRequest", map[string]any{ + "organizationId": orgID, + "requestType": "ACCESS", + "requestState": "TODO", + "dataSubject": "John Doe", + "contact": "john@example.com", + "details": "Request for data access", + }, &result) + + assert.NotEmpty(t, result.RightsRequest.ID) + assert.Equal(t, "ACCESS", result.RightsRequest.RequestType) + assert.Equal(t, "TODO", result.RightsRequest.RequestState) + assert.Equal(t, "John Doe", result.RightsRequest.DataSubject) +} + +func TestMCP_GetRightsRequest(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("addRightsRequest", map[string]any{ + "organizationId": orgID, + "requestType": "DELETION", + "requestState": "TODO", + "dataSubject": "Jane Doe", + }, &addResult) + require.NotEmpty(t, addResult.RightsRequest.ID) + + // Get + var getResult struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("getRightsRequest", map[string]any{ + "id": addResult.RightsRequest.ID, + }, &getResult) + + assert.Equal(t, addResult.RightsRequest.ID, getResult.RightsRequest.ID) + assert.Equal(t, "DELETION", getResult.RightsRequest.RequestType) + assert.Equal(t, "Jane Doe", getResult.RightsRequest.DataSubject) +} + +func TestMCP_UpdateRightsRequest(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("addRightsRequest", map[string]any{ + "organizationId": orgID, + "requestType": "ACCESS", + "requestState": "TODO", + "dataSubject": "Test Subject", + }, &addResult) + require.NotEmpty(t, addResult.RightsRequest.ID) + + // Update + var updateResult struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("updateRightsRequest", map[string]any{ + "id": addResult.RightsRequest.ID, + "requestState": "IN_PROGRESS", + "dataSubject": "Updated Subject", + }, &updateResult) + + assert.Equal(t, addResult.RightsRequest.ID, updateResult.RightsRequest.ID) + assert.Equal(t, "IN_PROGRESS", updateResult.RightsRequest.RequestState) + assert.Equal(t, "Updated Subject", updateResult.RightsRequest.DataSubject) +} + +func TestMCP_DeleteRightsRequest(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("addRightsRequest", map[string]any{ + "organizationId": orgID, + "requestType": "PORTABILITY", + "requestState": "TODO", + "dataSubject": "Delete Subject", + }, &addResult) + require.NotEmpty(t, addResult.RightsRequest.ID) + + // Delete + var deleteResult struct { + DeletedRightsRequestID string `json:"deletedRightsRequestId"` + } + mc.CallToolInto("deleteRightsRequest", map[string]any{ + "id": addResult.RightsRequest.ID, + }, &deleteResult) + + assert.Equal(t, addResult.RightsRequest.ID, deleteResult.DeletedRightsRequestID) +} + +func TestMCP_ListRightsRequests(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create multiple rights requests + for _, reqType := range []string{"ACCESS", "DELETION", "PORTABILITY"} { + var result struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("addRightsRequest", map[string]any{ + "organizationId": orgID, + "requestType": reqType, + "requestState": "TODO", + "dataSubject": factory.SafeName("Subject"), + }, &result) + require.NotEmpty(t, result.RightsRequest.ID) + } + + // List + var listResult struct { + RightsRequests []rightsRequest `json:"rightsRequests"` + } + mc.CallToolInto("listRightsRequests", map[string]any{ + "organizationId": orgID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.RightsRequests), 3) +} + +func TestMCP_RightsRequest_Types(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + for _, reqType := range []string{"ACCESS", "DELETION", "PORTABILITY"} { + t.Run(reqType, func(t *testing.T) { + var result struct { + RightsRequest rightsRequest `json:"rightsRequest"` + } + mc.CallToolInto("addRightsRequest", map[string]any{ + "organizationId": orgID, + "requestType": reqType, + "requestState": "TODO", + "dataSubject": factory.SafeName("Subject"), + }, &result) + + assert.Equal(t, reqType, result.RightsRequest.RequestType) + }) + } +} diff --git a/e2e/mcp/risk_test.go b/e2e/mcp/risk_test.go new file mode 100644 index 000000000..2e2b955e6 --- /dev/null +++ b/e2e/mcp/risk_test.go @@ -0,0 +1,89 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Risk_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Risk struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"risk"` + } + mc.CallToolInto("addRisk", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Risk"), + }, &addResult) + require.NotEmpty(t, addResult.Risk.ID) + + // Get + var getResult struct { + Risk struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"risk"` + } + mc.CallToolInto("getRisk", map[string]any{ + "id": addResult.Risk.ID, + }, &getResult) + assert.Equal(t, addResult.Risk.ID, getResult.Risk.ID) + + // Update + var updateResult struct { + Risk struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"risk"` + } + mc.CallToolInto("updateRisk", map[string]any{ + "id": addResult.Risk.ID, + "name": "Updated Risk", + }, &updateResult) + assert.Equal(t, "Updated Risk", updateResult.Risk.Name) + + // List + var listResult struct { + Risks []struct { + ID string `json:"id"` + } `json:"risks"` + } + mc.CallToolInto("listRisks", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Risks) + + // Delete + var deleteResult struct { + DeletedRiskID string `json:"deletedRiskId"` + } + mc.CallToolInto("deleteRisk", map[string]any{ + "id": addResult.Risk.ID, + }, &deleteResult) + assert.Equal(t, addResult.Risk.ID, deleteResult.DeletedRiskID) +} diff --git a/e2e/mcp/snapshot_test.go b/e2e/mcp/snapshot_test.go new file mode 100644 index 000000000..9eb3fc218 --- /dev/null +++ b/e2e/mcp/snapshot_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Snapshot(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create a vendor so the snapshot has data + factory.CreateVendor(owner) + + // Take snapshot + var takeResult struct { + Snapshot struct { + ID string `json:"id"` + } `json:"snapshot"` + } + mc.CallToolInto("takeSnapshot", map[string]any{ + "organizationId": orgID, + "name": factory.SafeName("Snapshot"), + "snapshotsType": "VENDORS", + }, &takeResult) + require.NotEmpty(t, takeResult.Snapshot.ID) + + // Get + var getResult struct { + Snapshot struct { + ID string `json:"id"` + } `json:"snapshot"` + } + mc.CallToolInto("getSnapshot", map[string]any{ + "id": takeResult.Snapshot.ID, + }, &getResult) + assert.Equal(t, takeResult.Snapshot.ID, getResult.Snapshot.ID) + + // List + var listResult struct { + Snapshots []struct { + ID string `json:"id"` + } `json:"snapshots"` + } + mc.CallToolInto("listSnapshots", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Snapshots) +} diff --git a/e2e/mcp/task_test.go b/e2e/mcp/task_test.go new file mode 100644 index 000000000..36338ab52 --- /dev/null +++ b/e2e/mcp/task_test.go @@ -0,0 +1,88 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Task_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + measureID := factory.CreateMeasure(owner) + + // Create + var addResult struct { + Task struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"task"` + } + mc.CallToolInto("addTask", map[string]any{ + "measureId": measureID, + "name": factory.SafeName("Task"), + }, &addResult) + require.NotEmpty(t, addResult.Task.ID) + + // Get + var getResult struct { + Task struct { + ID string `json:"id"` + } `json:"task"` + } + mc.CallToolInto("getTask", map[string]any{ + "id": addResult.Task.ID, + }, &getResult) + assert.Equal(t, addResult.Task.ID, getResult.Task.ID) + + // Update + var updateResult struct { + Task struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"task"` + } + mc.CallToolInto("updateTask", map[string]any{ + "id": addResult.Task.ID, + "name": "Updated Task", + }, &updateResult) + assert.Equal(t, "Updated Task", updateResult.Task.Name) + + // List + var listResult struct { + Tasks []struct { + ID string `json:"id"` + } `json:"tasks"` + } + mc.CallToolInto("listTasks", map[string]any{ + "measureId": measureID, + }, &listResult) + assert.NotEmpty(t, listResult.Tasks) + + // Delete + var deleteResult struct { + DeletedTaskID string `json:"deletedTaskId"` + } + mc.CallToolInto("deleteTask", map[string]any{ + "id": addResult.Task.ID, + }, &deleteResult) + assert.Equal(t, addResult.Task.ID, deleteResult.DeletedTaskID) +} diff --git a/e2e/mcp/trust_center_test.go b/e2e/mcp/trust_center_test.go new file mode 100644 index 000000000..f3554fca8 --- /dev/null +++ b/e2e/mcp/trust_center_test.go @@ -0,0 +1,410 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 trustCenter struct { + ID string `json:"id"` + CompanyName string `json:"companyName"` + PageTitle string `json:"pageTitle"` + TrustCenterVisible bool `json:"trustCenterVisible"` +} + +type trustCenterReference struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` + Order int `json:"order"` +} + +type complianceExternalURL struct { + ID string `json:"id"` + Name string `json:"name"` + URL string `json:"url"` +} + +func TestMCP_GetTrustCenter(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + var result struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &result) + + assert.NotEmpty(t, result.TrustCenter.ID) +} + +func TestMCP_UpdateTrustCenter(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + require.NotEmpty(t, getResult.TrustCenter.ID) + + // Update + var updateResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("updateTrustCenter", map[string]any{ + "id": getResult.TrustCenter.ID, + "companyName": "Updated Company", + "pageTitle": "Updated Trust Center", + }, &updateResult) + + assert.Equal(t, getResult.TrustCenter.ID, updateResult.TrustCenter.ID) + assert.Equal(t, "Updated Company", updateResult.TrustCenter.CompanyName) + assert.Equal(t, "Updated Trust Center", updateResult.TrustCenter.PageTitle) +} + +func TestMCP_AddTrustCenterReference(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + var result struct { + TrustCenterReference trustCenterReference `json:"trustCenterReference"` + } + mc.CallToolInto("addTrustCenterReference", map[string]any{ + "trustCenterId": tcID, + "name": "SOC 2 Report", + "url": "https://example.com/soc2", + }, &result) + + assert.NotEmpty(t, result.TrustCenterReference.ID) + assert.Equal(t, "SOC 2 Report", result.TrustCenterReference.Name) +} + +func TestMCP_UpdateTrustCenterReference(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // Create reference + var addResult struct { + TrustCenterReference trustCenterReference `json:"trustCenterReference"` + } + mc.CallToolInto("addTrustCenterReference", map[string]any{ + "trustCenterId": tcID, + "name": "Original Reference", + "url": "https://example.com/original", + }, &addResult) + require.NotEmpty(t, addResult.TrustCenterReference.ID) + + // Update reference + var updateResult struct { + TrustCenterReference trustCenterReference `json:"trustCenterReference"` + } + mc.CallToolInto("updateTrustCenterReference", map[string]any{ + "id": addResult.TrustCenterReference.ID, + "name": "Updated Reference", + "url": "https://example.com/updated", + }, &updateResult) + + assert.Equal(t, addResult.TrustCenterReference.ID, updateResult.TrustCenterReference.ID) + assert.Equal(t, "Updated Reference", updateResult.TrustCenterReference.Name) +} + +func TestMCP_DeleteTrustCenterReference(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // Create reference + var addResult struct { + TrustCenterReference trustCenterReference `json:"trustCenterReference"` + } + mc.CallToolInto("addTrustCenterReference", map[string]any{ + "trustCenterId": tcID, + "name": "Reference to delete", + "url": "https://example.com/delete", + }, &addResult) + require.NotEmpty(t, addResult.TrustCenterReference.ID) + + // Delete + var deleteResult struct { + DeletedTrustCenterReferenceID string `json:"deletedTrustCenterReferenceId"` + } + mc.CallToolInto("deleteTrustCenterReference", map[string]any{ + "id": addResult.TrustCenterReference.ID, + }, &deleteResult) + + assert.Equal(t, addResult.TrustCenterReference.ID, deleteResult.DeletedTrustCenterReferenceID) +} + +func TestMCP_ListTrustCenterReferences(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // Create references + for i := range 2 { + var result struct { + TrustCenterReference trustCenterReference `json:"trustCenterReference"` + } + mc.CallToolInto("addTrustCenterReference", map[string]any{ + "trustCenterId": tcID, + "name": factory.SafeName("Ref"), + "url": "https://example.com/" + factory.SafeName("path"), + }, &result) + require.NotEmpty(t, result.TrustCenterReference.ID) + _ = i + } + + // List + var listResult struct { + TrustCenterReferences []trustCenterReference `json:"trustCenterReferences"` + } + mc.CallToolInto("listTrustCenterReferences", map[string]any{ + "trustCenterId": tcID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.TrustCenterReferences), 2) +} + +func TestMCP_ListTrustCenterFiles(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // List files (may be empty, just verify the tool works) + var listResult struct { + TrustCenterFiles []struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"trustCenterFiles"` + } + mc.CallToolInto("listTrustCenterFiles", map[string]any{ + "trustCenterId": tcID, + }, &listResult) + + // Just assert the call succeeded — files require multipart upload + assert.NotNil(t, listResult.TrustCenterFiles) +} + +func TestMCP_AddComplianceExternalURL(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + var result struct { + ComplianceExternalURL complianceExternalURL `json:"complianceExternalUrl"` + } + mc.CallToolInto("addComplianceExternalURL", map[string]any{ + "trustCenterId": tcID, + "name": "ISO 27001 Certificate", + "url": "https://example.com/iso27001", + }, &result) + + assert.NotEmpty(t, result.ComplianceExternalURL.ID) + assert.Equal(t, "ISO 27001 Certificate", result.ComplianceExternalURL.Name) +} + +func TestMCP_UpdateComplianceExternalURL(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // Create + var addResult struct { + ComplianceExternalURL complianceExternalURL `json:"complianceExternalUrl"` + } + mc.CallToolInto("addComplianceExternalURL", map[string]any{ + "trustCenterId": tcID, + "name": "Original URL", + "url": "https://example.com/original", + }, &addResult) + require.NotEmpty(t, addResult.ComplianceExternalURL.ID) + + // Update + var updateResult struct { + ComplianceExternalURL complianceExternalURL `json:"complianceExternalUrl"` + } + mc.CallToolInto("updateComplianceExternalURL", map[string]any{ + "id": addResult.ComplianceExternalURL.ID, + "name": "Updated URL", + "url": "https://example.com/updated", + }, &updateResult) + + assert.Equal(t, addResult.ComplianceExternalURL.ID, updateResult.ComplianceExternalURL.ID) + assert.Equal(t, "Updated URL", updateResult.ComplianceExternalURL.Name) +} + +func TestMCP_DeleteComplianceExternalURL(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // Create + var addResult struct { + ComplianceExternalURL complianceExternalURL `json:"complianceExternalUrl"` + } + mc.CallToolInto("addComplianceExternalURL", map[string]any{ + "trustCenterId": tcID, + "name": "URL to delete", + "url": "https://example.com/delete", + }, &addResult) + require.NotEmpty(t, addResult.ComplianceExternalURL.ID) + + // Delete + var deleteResult struct { + DeletedComplianceExternalURLID string `json:"deletedComplianceExternalUrlId"` + } + mc.CallToolInto("deleteComplianceExternalURL", map[string]any{ + "id": addResult.ComplianceExternalURL.ID, + }, &deleteResult) + + assert.Equal(t, addResult.ComplianceExternalURL.ID, deleteResult.DeletedComplianceExternalURLID) +} + +func TestMCP_ListComplianceExternalURLs(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Get trust center ID + var getResult struct { + TrustCenter trustCenter `json:"trustCenter"` + } + mc.CallToolInto("getTrustCenter", map[string]any{ + "organizationId": orgID, + }, &getResult) + tcID := getResult.TrustCenter.ID + + // Create URLs + for i := range 2 { + var result struct { + ComplianceExternalURL complianceExternalURL `json:"complianceExternalUrl"` + } + mc.CallToolInto("addComplianceExternalURL", map[string]any{ + "trustCenterId": tcID, + "name": factory.SafeName("URL"), + "url": "https://example.com/" + factory.SafeName("path"), + }, &result) + require.NotEmpty(t, result.ComplianceExternalURL.ID) + _ = i + } + + // List + var listResult struct { + ComplianceExternalURLs []complianceExternalURL `json:"complianceExternalUrls"` + } + mc.CallToolInto("listComplianceExternalURLs", map[string]any{ + "trustCenterId": tcID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.ComplianceExternalURLs), 2) +} diff --git a/e2e/mcp/user_test.go b/e2e/mcp/user_test.go new file mode 100644 index 000000000..68a3fba80 --- /dev/null +++ b/e2e/mcp/user_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_User_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var createResult struct { + User struct { + ID string `json:"id"` + FullName string `json:"fullName"` + } `json:"user"` + } + mc.CallToolInto("createUser", map[string]any{ + "organizationId": orgID, + "fullName": "Test User", + "emailAddress": factory.SafeEmail(), + "role": "EMPLOYEE", + "kind": "EMPLOYEE", + }, &createResult) + require.NotEmpty(t, createResult.User.ID) + + // Get + var getResult struct { + User struct { + ID string `json:"id"` + } `json:"user"` + } + mc.CallToolInto("getUser", map[string]any{ + "id": createResult.User.ID, + }, &getResult) + assert.Equal(t, createResult.User.ID, getResult.User.ID) + + // List + var listResult struct { + Users []struct { + ID string `json:"id"` + } `json:"users"` + } + mc.CallToolInto("listUsers", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Users) +} diff --git a/e2e/mcp/vendor_contact_test.go b/e2e/mcp/vendor_contact_test.go new file mode 100644 index 000000000..2f545d37d --- /dev/null +++ b/e2e/mcp/vendor_contact_test.go @@ -0,0 +1,142 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 vendorContact struct { + ID string `json:"id"` + Name string `json:"name"` + Email *string `json:"email"` + Phone *string `json:"phone"` + Role *string `json:"role"` +} + +func TestMCP_AddVendorContact(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + var result struct { + VendorContact vendorContact `json:"vendorContact"` + } + mc.CallToolInto("addVendorContact", map[string]any{ + "vendorId": vendorID, + "name": "Alice Smith", + "email": "alice@example.com", + "phone": "+1-555-0100", + "role": "Account Manager", + }, &result) + + assert.NotEmpty(t, result.VendorContact.ID) + assert.Equal(t, "Alice Smith", result.VendorContact.Name) +} + +func TestMCP_UpdateVendorContact(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + // Create + var addResult struct { + VendorContact vendorContact `json:"vendorContact"` + } + mc.CallToolInto("addVendorContact", map[string]any{ + "vendorId": vendorID, + "name": "Bob Jones", + "email": "bob@example.com", + }, &addResult) + require.NotEmpty(t, addResult.VendorContact.ID) + + // Update + var updateResult struct { + VendorContact vendorContact `json:"vendorContact"` + } + mc.CallToolInto("updateVendorContact", map[string]any{ + "id": addResult.VendorContact.ID, + "name": "Robert Jones", + "role": "CTO", + }, &updateResult) + + assert.Equal(t, addResult.VendorContact.ID, updateResult.VendorContact.ID) + assert.Equal(t, "Robert Jones", updateResult.VendorContact.Name) +} + +func TestMCP_DeleteVendorContact(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + // Create + var addResult struct { + VendorContact vendorContact `json:"vendorContact"` + } + mc.CallToolInto("addVendorContact", map[string]any{ + "vendorId": vendorID, + "name": "Contact to delete", + }, &addResult) + require.NotEmpty(t, addResult.VendorContact.ID) + + // Delete + var deleteResult struct { + DeletedVendorContactID string `json:"deletedVendorContactId"` + } + mc.CallToolInto("deleteVendorContact", map[string]any{ + "id": addResult.VendorContact.ID, + }, &deleteResult) + + assert.Equal(t, addResult.VendorContact.ID, deleteResult.DeletedVendorContactID) +} + +func TestMCP_ListVendorContacts(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + // Create contacts + for i := range 3 { + var result struct { + VendorContact vendorContact `json:"vendorContact"` + } + mc.CallToolInto("addVendorContact", map[string]any{ + "vendorId": vendorID, + "name": factory.SafeName("Contact"), + "email": factory.SafeEmail(), + }, &result) + require.NotEmpty(t, result.VendorContact.ID) + _ = i + } + + // List + var listResult struct { + VendorContacts []vendorContact `json:"vendorContacts"` + } + mc.CallToolInto("listVendorContacts", map[string]any{ + "vendorId": vendorID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.VendorContacts), 3) +} diff --git a/e2e/mcp/vendor_service_test.go b/e2e/mcp/vendor_service_test.go new file mode 100644 index 000000000..84aeb3b67 --- /dev/null +++ b/e2e/mcp/vendor_service_test.go @@ -0,0 +1,135 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 vendorService struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` +} + +func TestMCP_AddVendorService(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + var result struct { + VendorService vendorService `json:"vendorService"` + } + mc.CallToolInto("addVendorService", map[string]any{ + "vendorId": vendorID, + "name": "Cloud Storage", + "description": "Object storage service", + }, &result) + + assert.NotEmpty(t, result.VendorService.ID) + assert.Equal(t, "Cloud Storage", result.VendorService.Name) +} + +func TestMCP_UpdateVendorService(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + // Create + var addResult struct { + VendorService vendorService `json:"vendorService"` + } + mc.CallToolInto("addVendorService", map[string]any{ + "vendorId": vendorID, + "name": "Original Service", + }, &addResult) + require.NotEmpty(t, addResult.VendorService.ID) + + // Update + var updateResult struct { + VendorService vendorService `json:"vendorService"` + } + mc.CallToolInto("updateVendorService", map[string]any{ + "id": addResult.VendorService.ID, + "name": "Updated Service", + }, &updateResult) + + assert.Equal(t, addResult.VendorService.ID, updateResult.VendorService.ID) + assert.Equal(t, "Updated Service", updateResult.VendorService.Name) +} + +func TestMCP_DeleteVendorService(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + // Create + var addResult struct { + VendorService vendorService `json:"vendorService"` + } + mc.CallToolInto("addVendorService", map[string]any{ + "vendorId": vendorID, + "name": "Service to delete", + }, &addResult) + require.NotEmpty(t, addResult.VendorService.ID) + + // Delete + var deleteResult struct { + DeletedVendorServiceID string `json:"deletedVendorServiceId"` + } + mc.CallToolInto("deleteVendorService", map[string]any{ + "id": addResult.VendorService.ID, + }, &deleteResult) + + assert.Equal(t, addResult.VendorService.ID, deleteResult.DeletedVendorServiceID) +} + +func TestMCP_ListVendorServices(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + vendorID := factory.CreateVendor(owner) + + // Create services + for i := range 3 { + var result struct { + VendorService vendorService `json:"vendorService"` + } + mc.CallToolInto("addVendorService", map[string]any{ + "vendorId": vendorID, + "name": factory.SafeName("Service"), + }, &result) + require.NotEmpty(t, result.VendorService.ID) + _ = i + } + + // List + var listResult struct { + VendorServices []vendorService `json:"vendorServices"` + } + mc.CallToolInto("listVendorServices", map[string]any{ + "vendorId": vendorID, + }, &listResult) + + assert.GreaterOrEqual(t, len(listResult.VendorServices), 3) +} diff --git a/e2e/mcp/vendor_test.go b/e2e/mcp/vendor_test.go new file mode 100644 index 000000000..b6a6ecda3 --- /dev/null +++ b/e2e/mcp/vendor_test.go @@ -0,0 +1,79 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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" +) + +func TestMCP_Vendor_CRUD(t *testing.T) { + t.Parallel() + owner := testutil.NewClient(t, testutil.RoleOwner) + mc := testutil.NewMCPClient(t, owner) + orgID := owner.GetOrganizationID().String() + + // Create + var addResult struct { + Vendor struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"vendor"` + } + name := factory.SafeName("Vendor") + mc.CallToolInto("addVendor", map[string]any{ + "organizationId": orgID, + "name": name, + }, &addResult) + require.NotEmpty(t, addResult.Vendor.ID) + assert.Equal(t, name, addResult.Vendor.Name) + + // Update + var updateResult struct { + Vendor struct { + ID string `json:"id"` + Name string `json:"name"` + } `json:"vendor"` + } + mc.CallToolInto("updateVendor", map[string]any{ + "id": addResult.Vendor.ID, + "name": "Updated Vendor", + }, &updateResult) + assert.Equal(t, "Updated Vendor", updateResult.Vendor.Name) + + // List + var listResult struct { + Vendors []struct { + ID string `json:"id"` + } `json:"vendors"` + } + mc.CallToolInto("listVendors", map[string]any{ + "organizationId": orgID, + }, &listResult) + assert.NotEmpty(t, listResult.Vendors) + + // Delete + var deleteResult struct { + DeletedVendorID string `json:"deletedVendorId"` + } + mc.CallToolInto("deleteVendor", map[string]any{ + "id": addResult.Vendor.ID, + }, &deleteResult) + assert.Equal(t, addResult.Vendor.ID, deleteResult.DeletedVendorID) +} diff --git a/packages/n8n-node/nodes/Probo/Probo.node.ts b/packages/n8n-node/nodes/Probo/Probo.node.ts index 2aafb1749..601a2cf2f 100644 --- a/packages/n8n-node/nodes/Probo/Probo.node.ts +++ b/packages/n8n-node/nodes/Probo/Probo.node.ts @@ -76,6 +76,11 @@ export class Probo implements INodeType { type: 'options', noDataExpression: true, options: [ + { + name: 'Access Review', + value: 'accessReview', + description: 'Manage access review campaigns', + }, { name: 'Asset', value: 'asset', @@ -86,6 +91,11 @@ export class Probo implements INodeType { value: 'audit', description: 'Manage audits', }, + { + name: 'Audit Log', + value: 'auditLog', + description: 'View audit log entries', + }, { name: 'Control', value: 'control', @@ -101,11 +111,26 @@ export class Probo implements INodeType { value: 'document', description: 'Manage documents, versions, and signatures', }, + { + name: 'DPIA', + value: 'dpia', + description: 'Manage data protection impact assessments', + }, + { + name: 'Evidence', + value: 'evidence', + description: 'Manage evidences', + }, { name: 'Execute', value: 'execute', description: 'Execute a GraphQL query or mutation', }, + { + name: 'Finding', + value: 'finding', + description: 'Manage findings', + }, { name: 'Framework', value: 'framework', @@ -116,21 +141,61 @@ export class Probo implements INodeType { value: 'measure', description: 'Manage measures', }, + { + name: 'Obligation', + value: 'obligation', + description: 'Manage obligations', + }, { name: 'Organization', value: 'organization', description: 'Manage organizations', }, + { + name: 'Organization Context', + value: 'organizationContext', + description: 'Manage organization context', + }, + { + name: 'Processing Activity', + value: 'processingActivity', + description: 'Manage processing activities', + }, + { + name: 'Rights Request', + value: 'rightsRequest', + description: 'Manage rights requests', + }, { name: 'Risk', value: 'risk', description: 'Manage risks', }, + { + name: 'Snapshot', + value: 'snapshot', + description: 'Manage snapshots', + }, { name: 'Statement of Applicability', value: 'statementOfApplicability', description: 'Manage statements of applicability', }, + { + name: 'Task', + value: 'task', + description: 'Manage tasks', + }, + { + name: 'TIA', + value: 'tia', + description: 'Manage transfer impact assessments', + }, + { + name: 'Trust Center', + value: 'trustCenter', + description: 'Manage trust center', + }, { name: 'User', value: 'user', diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/cancel.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/cancel.operation.ts new file mode 100644 index 000000000..0e28e2bc5 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/cancel.operation.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Access Review Campaign ID', + name: 'accessReviewCampaignId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['cancel'], + }, + }, + default: '', + description: 'The ID of the access review campaign to cancel', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; + + const query = ` + mutation CancelAccessReviewCampaign($input: CancelAccessReviewCampaignInput!) { + cancelAccessReviewCampaign(input: $input) { + accessReviewCampaign { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { accessReviewCampaignId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/close.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/close.operation.ts new file mode 100644 index 000000000..ee55077e2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/close.operation.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Access Review Campaign ID', + name: 'accessReviewCampaignId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['close'], + }, + }, + default: '', + description: 'The ID of the access review campaign to close', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; + + const query = ` + mutation CloseAccessReviewCampaign($input: CloseAccessReviewCampaignInput!) { + closeAccessReviewCampaign(input: $input) { + accessReviewCampaign { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { accessReviewCampaignId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts new file mode 100644 index 000000000..876a39867 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/create.operation.ts @@ -0,0 +1,103 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['create'], + }, + }, + default: '', + description: 'The name of the access review campaign', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['create'], + }, + }, + default: '', + description: 'The description of the access review campaign', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + + const query = ` + mutation CreateAccessReviewCampaign($input: CreateAccessReviewCampaignInput!) { + createAccessReviewCampaign(input: $input) { + accessReviewCampaignEdge { + node { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + organizationId, + name, + ...(description && { description }), + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/delete.operation.ts new file mode 100644 index 000000000..efb0fb883 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Access Review Campaign ID', + name: 'accessReviewCampaignId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the access review campaign to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; + + const query = ` + mutation DeleteAccessReviewCampaign($input: DeleteAccessReviewCampaignInput!) { + deleteAccessReviewCampaign(input: $input) { + deletedAccessReviewCampaignId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { accessReviewCampaignId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/get.operation.ts new file mode 100644 index 000000000..fa2cde2d7 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/get.operation.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Access Review Campaign ID', + name: 'accessReviewCampaignId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the access review campaign', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; + + const query = ` + query GetAccessReviewCampaign($accessReviewCampaignId: ID!) { + node(id: $accessReviewCampaignId) { + ... on AccessReviewCampaign { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + } + `; + + const variables = { + accessReviewCampaignId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/getAll.operation.ts new file mode 100644 index 000000000..43ed5a321 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/getAll.operation.ts @@ -0,0 +1,117 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['accessReview'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['getAll'], + 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 GetAccessReviewCampaigns($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + accessReviewCampaigns(first: $first, after: $after) { + edges { + node { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const accessReviewCampaigns = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.accessReviewCampaigns as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { accessReviewCampaigns }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts new file mode 100644 index 000000000..43709997a --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/index.ts @@ -0,0 +1,107 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; +import * as updateOp from './update.operation'; +import * as startOp from './start.operation'; +import * as closeOp from './close.operation'; +import * as cancelOp from './cancel.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['accessReview'], + }, + }, + options: [ + { + name: 'Cancel', + value: 'cancel', + description: 'Cancel an access review campaign', + action: 'Cancel an access review campaign', + }, + { + name: 'Close', + value: 'close', + description: 'Close an access review campaign', + action: 'Close an access review campaign', + }, + { + name: 'Create', + value: 'create', + description: 'Create a new access review campaign', + action: 'Create an access review campaign', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an access review campaign', + action: 'Delete an access review campaign', + }, + { + name: 'Get', + value: 'get', + description: 'Get an access review campaign', + action: 'Get an access review campaign', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many access review campaigns', + action: 'Get many access review campaigns', + }, + { + name: 'Start', + value: 'start', + description: 'Start an access review campaign', + action: 'Start an access review campaign', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing access review campaign', + action: 'Update an access review campaign', + }, + ], + default: 'create', + }, + ...createOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, + ...updateOp.description, + ...startOp.description, + ...closeOp.description, + ...cancelOp.description, +]; + +export { + createOp as create, + deleteOp as delete, + getOp as get, + getAllOp as getAll, + updateOp as update, + startOp as start, + closeOp as close, + cancelOp as cancel, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/start.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/start.operation.ts new file mode 100644 index 000000000..c980df682 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/start.operation.ts @@ -0,0 +1,64 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Access Review Campaign ID', + name: 'accessReviewCampaignId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['start'], + }, + }, + default: '', + description: 'The ID of the access review campaign to start', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; + + const query = ` + mutation StartAccessReviewCampaign($input: StartAccessReviewCampaignInput!) { + startAccessReviewCampaign(input: $input) { + accessReviewCampaign { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { accessReviewCampaignId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts new file mode 100644 index 000000000..54fc52ad0 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/accessReview/update.operation.ts @@ -0,0 +1,96 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Access Review Campaign ID', + name: 'accessReviewCampaignId', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the access review campaign to update', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['update'], + }, + }, + default: '', + description: 'The name of the access review campaign', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['accessReview'], + operation: ['update'], + }, + }, + default: '', + description: 'The description of the access review campaign', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const accessReviewCampaignId = this.getNodeParameter('accessReviewCampaignId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex, '') as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + + const query = ` + mutation UpdateAccessReviewCampaign($input: UpdateAccessReviewCampaignInput!) { + updateAccessReviewCampaign(input: $input) { + accessReviewCampaign { + id + name + description + status + startedAt + completedAt + createdAt + updatedAt + } + } + } + `; + + const input: Record = { accessReviewCampaignId }; + if (name) input.name = name; + if (description) input.description = description; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/auditLog/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/auditLog/get.operation.ts new file mode 100644 index 000000000..8ccf9d060 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/auditLog/get.operation.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Audit Log Entry ID', + name: 'auditLogEntryId', + type: 'string', + displayOptions: { + show: { + resource: ['auditLog'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the audit log entry', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const auditLogEntryId = this.getNodeParameter('auditLogEntryId', itemIndex) as string; + + const query = ` + query GetAuditLogEntry($auditLogEntryId: ID!) { + node(id: $auditLogEntryId) { + ... on AuditLogEntry { + id + actorId + actorType + action + resourceType + resourceId + metadata + createdAt + } + } + } + `; + + const variables = { + auditLogEntryId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/auditLog/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/auditLog/getAll.operation.ts new file mode 100644 index 000000000..00817086b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/auditLog/getAll.operation.ts @@ -0,0 +1,117 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['auditLog'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['auditLog'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['auditLog'], + operation: ['getAll'], + 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 GetAuditLogEntries($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + auditLogEntries(first: $first, after: $after) { + edges { + node { + id + actorId + actorType + action + resourceType + resourceId + metadata + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const auditLogEntries = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.auditLogEntries as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { auditLogEntries }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/auditLog/index.ts b/packages/n8n-node/nodes/Probo/actions/auditLog/index.ts new file mode 100644 index 000000000..8a9d50d1f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/auditLog/index.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['auditLog'], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get an audit log entry', + action: 'Get an audit log entry', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many audit log entries', + action: 'Get many audit log entries', + }, + ], + default: 'get', + }, + ...getOp.description, + ...getAllOp.description, +]; + +export { getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/control/index.ts b/packages/n8n-node/nodes/Probo/actions/control/index.ts index 507a81c2d..f912d23ad 100644 --- a/packages/n8n-node/nodes/Probo/actions/control/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/control/index.ts @@ -18,6 +18,16 @@ import * as updateOp from './update.operation'; import * as deleteOp from './delete.operation'; import * as getOp from './get.operation'; import * as getAllOp from './getAll.operation'; +import * as linkMeasureOp from './linkMeasure.operation'; +import * as unlinkMeasureOp from './unlinkMeasure.operation'; +import * as linkDocumentOp from './linkDocument.operation'; +import * as unlinkDocumentOp from './unlinkDocument.operation'; +import * as linkAuditOp from './linkAudit.operation'; +import * as unlinkAuditOp from './unlinkAudit.operation'; +import * as linkObligationOp from './linkObligation.operation'; +import * as unlinkObligationOp from './unlinkObligation.operation'; +import * as linkSnapshotOp from './linkSnapshot.operation'; +import * as unlinkSnapshotOp from './unlinkSnapshot.operation'; export const description: INodeProperties[] = [ { @@ -55,6 +65,66 @@ export const description: INodeProperties[] = [ description: 'Get many controls', action: 'Get many controls', }, + { + name: 'Link Audit', + value: 'linkAudit', + description: 'Link an audit to a control', + action: 'Link an audit to a control', + }, + { + name: 'Link Document', + value: 'linkDocument', + description: 'Link a document to a control', + action: 'Link a document to a control', + }, + { + name: 'Link Measure', + value: 'linkMeasure', + description: 'Link a measure to a control', + action: 'Link a measure to a control', + }, + { + name: 'Link Obligation', + value: 'linkObligation', + description: 'Link an obligation to a control', + action: 'Link an obligation to a control', + }, + { + name: 'Link Snapshot', + value: 'linkSnapshot', + description: 'Link a snapshot to a control', + action: 'Link a snapshot to a control', + }, + { + name: 'Unlink Audit', + value: 'unlinkAudit', + description: 'Unlink an audit from a control', + action: 'Unlink an audit from a control', + }, + { + name: 'Unlink Document', + value: 'unlinkDocument', + description: 'Unlink a document from a control', + action: 'Unlink a document from a control', + }, + { + name: 'Unlink Measure', + value: 'unlinkMeasure', + description: 'Unlink a measure from a control', + action: 'Unlink a measure from a control', + }, + { + name: 'Unlink Obligation', + value: 'unlinkObligation', + description: 'Unlink an obligation from a control', + action: 'Unlink an obligation from a control', + }, + { + name: 'Unlink Snapshot', + value: 'unlinkSnapshot', + description: 'Unlink a snapshot from a control', + action: 'Unlink a snapshot from a control', + }, { name: 'Update', value: 'update', @@ -69,6 +139,32 @@ export const description: INodeProperties[] = [ ...deleteOp.description, ...getOp.description, ...getAllOp.description, + ...linkMeasureOp.description, + ...unlinkMeasureOp.description, + ...linkDocumentOp.description, + ...unlinkDocumentOp.description, + ...linkAuditOp.description, + ...unlinkAuditOp.description, + ...linkObligationOp.description, + ...unlinkObligationOp.description, + ...linkSnapshotOp.description, + ...unlinkSnapshotOp.description, ]; -export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; +export { + createOp as create, + updateOp as update, + deleteOp as delete, + getOp as get, + getAllOp as getAll, + linkMeasureOp as linkMeasure, + unlinkMeasureOp as unlinkMeasure, + linkDocumentOp as linkDocument, + unlinkDocumentOp as unlinkDocument, + linkAuditOp as linkAudit, + unlinkAuditOp as unlinkAudit, + linkObligationOp as linkObligation, + unlinkObligationOp as unlinkObligation, + linkSnapshotOp as linkSnapshot, + unlinkSnapshotOp as unlinkSnapshot, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/control/linkAudit.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/linkAudit.operation.ts new file mode 100644 index 000000000..9f8003535 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/linkAudit.operation.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkAudit'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkAudit'], + }, + }, + default: '', + description: 'The ID of the audit to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + + const query = ` + mutation CreateControlAuditMapping($input: CreateControlAuditMappingInput!) { + createControlAuditMapping(input: $input) { + controlEdge { + node { + id + name + } + } + auditEdge { + node { + id + name + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, auditId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/linkDocument.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/linkDocument.operation.ts new file mode 100644 index 000000000..2e8ec45da --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/linkDocument.operation.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkDocument'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Document ID', + name: 'documentId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkDocument'], + }, + }, + default: '', + description: 'The ID of the document to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const documentId = this.getNodeParameter('documentId', itemIndex) as string; + + const query = ` + mutation CreateControlDocumentMapping($input: CreateControlDocumentMappingInput!) { + createControlDocumentMapping(input: $input) { + controlEdge { + node { + id + name + } + } + documentEdge { + node { + id + title + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, documentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/linkMeasure.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/linkMeasure.operation.ts new file mode 100644 index 000000000..fa02822b2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/linkMeasure.operation.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkMeasure'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkMeasure'], + }, + }, + default: '', + description: 'The ID of the measure to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + + const query = ` + mutation CreateControlMeasureMapping($input: CreateControlMeasureMappingInput!) { + createControlMeasureMapping(input: $input) { + controlEdge { + node { + id + name + } + } + measureEdge { + node { + id + name + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, measureId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/linkObligation.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/linkObligation.operation.ts new file mode 100644 index 000000000..9203581d5 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/linkObligation.operation.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkObligation'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Obligation ID', + name: 'obligationId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkObligation'], + }, + }, + default: '', + description: 'The ID of the obligation to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const obligationId = this.getNodeParameter('obligationId', itemIndex) as string; + + const query = ` + mutation CreateControlObligationMapping($input: CreateControlObligationMappingInput!) { + createControlObligationMapping(input: $input) { + controlEdge { + node { + id + name + } + } + obligationEdge { + node { + id + name + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, obligationId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/linkSnapshot.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/linkSnapshot.operation.ts new file mode 100644 index 000000000..d2738b234 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/linkSnapshot.operation.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkSnapshot'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Snapshot ID', + name: 'snapshotId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['linkSnapshot'], + }, + }, + default: '', + description: 'The ID of the snapshot to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string; + + const query = ` + mutation CreateControlSnapshotMapping($input: CreateControlSnapshotMappingInput!) { + createControlSnapshotMapping(input: $input) { + controlEdge { + node { + id + name + } + } + snapshotEdge { + node { + id + name + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, snapshotId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/unlinkAudit.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/unlinkAudit.operation.ts new file mode 100644 index 000000000..b98e91979 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/unlinkAudit.operation.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkAudit'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkAudit'], + }, + }, + default: '', + description: 'The ID of the audit to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + + const query = ` + mutation DeleteControlAuditMapping($input: DeleteControlAuditMappingInput!) { + deleteControlAuditMapping(input: $input) { + deletedControlId + deletedAuditId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, auditId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/unlinkDocument.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/unlinkDocument.operation.ts new file mode 100644 index 000000000..59dda1eac --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/unlinkDocument.operation.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkDocument'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Document ID', + name: 'documentId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkDocument'], + }, + }, + default: '', + description: 'The ID of the document to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const documentId = this.getNodeParameter('documentId', itemIndex) as string; + + const query = ` + mutation DeleteControlDocumentMapping($input: DeleteControlDocumentMappingInput!) { + deleteControlDocumentMapping(input: $input) { + deletedControlId + deletedDocumentId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, documentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/unlinkMeasure.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/unlinkMeasure.operation.ts new file mode 100644 index 000000000..b9bc1ce98 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/unlinkMeasure.operation.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkMeasure'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkMeasure'], + }, + }, + default: '', + description: 'The ID of the measure to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + + const query = ` + mutation DeleteControlMeasureMapping($input: DeleteControlMeasureMappingInput!) { + deleteControlMeasureMapping(input: $input) { + deletedControlId + deletedMeasureId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, measureId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/unlinkObligation.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/unlinkObligation.operation.ts new file mode 100644 index 000000000..a424a03e8 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/unlinkObligation.operation.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkObligation'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Obligation ID', + name: 'obligationId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkObligation'], + }, + }, + default: '', + description: 'The ID of the obligation to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const obligationId = this.getNodeParameter('obligationId', itemIndex) as string; + + const query = ` + mutation DeleteControlObligationMapping($input: DeleteControlObligationMappingInput!) { + deleteControlObligationMapping(input: $input) { + deletedControlId + deletedObligationId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, obligationId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/control/unlinkSnapshot.operation.ts b/packages/n8n-node/nodes/Probo/actions/control/unlinkSnapshot.operation.ts new file mode 100644 index 000000000..a6205c0cb --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/control/unlinkSnapshot.operation.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Control ID', + name: 'controlId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkSnapshot'], + }, + }, + default: '', + description: 'The ID of the control', + required: true, + }, + { + displayName: 'Snapshot ID', + name: 'snapshotId', + type: 'string', + displayOptions: { + show: { + resource: ['control'], + operation: ['unlinkSnapshot'], + }, + }, + default: '', + description: 'The ID of the snapshot to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const controlId = this.getNodeParameter('controlId', itemIndex) as string; + const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string; + + const query = ` + mutation DeleteControlSnapshotMapping($input: DeleteControlSnapshotMappingInput!) { + deleteControlSnapshotMapping(input: $input) { + deletedControlId + deletedSnapshotId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { controlId, snapshotId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/dpia/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/dpia/create.operation.ts new file mode 100644 index 000000000..fbb0803f7 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/dpia/create.operation.ts @@ -0,0 +1,166 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Processing Activity ID', + name: 'processingActivityId', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the processing activity', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['create'], + }, + }, + default: '', + description: 'The description of the DPIA', + required: true, + }, + { + displayName: 'Necessity and Proportionality', + name: 'necessityAndProportionality', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['create'], + }, + }, + default: '', + description: 'The necessity and proportionality assessment', + required: true, + }, + { + displayName: 'Potential Risk', + name: 'potentialRisk', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['create'], + }, + }, + default: '', + description: 'The potential risk assessment', + required: true, + }, + { + displayName: 'Mitigations', + name: 'mitigations', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['create'], + }, + }, + default: '', + description: 'The mitigations for the identified risks', + required: true, + }, + { + displayName: 'Residual Risk', + name: 'residualRisk', + type: 'options', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Low', + value: 'LOW', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'High', + value: 'HIGH', + }, + ], + default: 'LOW', + description: 'The residual risk level', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processingActivityId = this.getNodeParameter('processingActivityId', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex) as string; + const necessityAndProportionality = this.getNodeParameter('necessityAndProportionality', itemIndex) as string; + const potentialRisk = this.getNodeParameter('potentialRisk', itemIndex) as string; + const mitigations = this.getNodeParameter('mitigations', itemIndex) as string; + const residualRisk = this.getNodeParameter('residualRisk', itemIndex) as string; + + const query = ` + mutation CreateDataProtectionImpactAssessment($input: CreateDataProtectionImpactAssessmentInput!) { + createDataProtectionImpactAssessment(input: $input) { + dataProtectionImpactAssessmentEdge { + node { + id + description + necessityAndProportionality + potentialRisk + mitigations + residualRisk + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + processingActivityId, + description, + necessityAndProportionality, + potentialRisk, + mitigations, + residualRisk, + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/dpia/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/dpia/delete.operation.ts new file mode 100644 index 000000000..e79aa5f2b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/dpia/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'DPIA ID', + name: 'dataProtectionImpactAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the DPIA to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const dataProtectionImpactAssessmentId = this.getNodeParameter('dataProtectionImpactAssessmentId', itemIndex) as string; + + const query = ` + mutation DeleteDataProtectionImpactAssessment($input: DeleteDataProtectionImpactAssessmentInput!) { + deleteDataProtectionImpactAssessment(input: $input) { + deletedDataProtectionImpactAssessmentId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { dataProtectionImpactAssessmentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/dpia/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/dpia/get.operation.ts new file mode 100644 index 000000000..7b28c2bc8 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/dpia/get.operation.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'DPIA ID', + name: 'dataProtectionImpactAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the DPIA', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const dataProtectionImpactAssessmentId = this.getNodeParameter('dataProtectionImpactAssessmentId', itemIndex) as string; + + const query = ` + query GetDataProtectionImpactAssessment($dataProtectionImpactAssessmentId: ID!) { + node(id: $dataProtectionImpactAssessmentId) { + ... on DataProtectionImpactAssessment { + id + description + necessityAndProportionality + potentialRisk + mitigations + residualRisk + createdAt + updatedAt + } + } + } + `; + + const variables = { + dataProtectionImpactAssessmentId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/dpia/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/dpia/getAll.operation.ts new file mode 100644 index 000000000..8d8fcc9ab --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/dpia/getAll.operation.ts @@ -0,0 +1,117 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['dpia'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['getAll'], + 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 GetDataProtectionImpactAssessments($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + dataProtectionImpactAssessments(first: $first, after: $after) { + edges { + node { + id + description + necessityAndProportionality + potentialRisk + mitigations + residualRisk + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const dataProtectionImpactAssessments = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.dataProtectionImpactAssessments as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { dataProtectionImpactAssessments }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/dpia/index.ts b/packages/n8n-node/nodes/Probo/actions/dpia/index.ts new file mode 100644 index 000000000..195a1f898 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/dpia/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['dpia'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new DPIA', + action: 'Create a DPIA', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a DPIA', + action: 'Delete a DPIA', + }, + { + name: 'Get', + value: 'get', + description: 'Get a DPIA', + action: 'Get a DPIA', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many DPIAs', + action: 'Get many dpias', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing DPIA', + action: 'Update a DPIA', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/dpia/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/dpia/update.operation.ts new file mode 100644 index 000000000..d25fb5f86 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/dpia/update.operation.ts @@ -0,0 +1,159 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'DPIA ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the DPIA to update', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['update'], + }, + }, + default: '', + description: 'The description of the DPIA', + }, + { + displayName: 'Necessity and Proportionality', + name: 'necessityAndProportionality', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['update'], + }, + }, + default: '', + description: 'The necessity and proportionality assessment', + }, + { + displayName: 'Potential Risk', + name: 'potentialRisk', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['update'], + }, + }, + default: '', + description: 'The potential risk assessment', + }, + { + displayName: 'Mitigations', + name: 'mitigations', + type: 'string', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['update'], + }, + }, + default: '', + description: 'The mitigations for the identified risks', + }, + { + displayName: 'Residual Risk', + name: 'residualRisk', + type: 'options', + displayOptions: { + show: { + resource: ['dpia'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Low', + value: 'LOW', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'High', + value: 'HIGH', + }, + ], + default: '', + description: 'The residual risk level', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const id = this.getNodeParameter('id', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const necessityAndProportionality = this.getNodeParameter('necessityAndProportionality', itemIndex, '') as string; + const potentialRisk = this.getNodeParameter('potentialRisk', itemIndex, '') as string; + const mitigations = this.getNodeParameter('mitigations', itemIndex, '') as string; + const residualRisk = this.getNodeParameter('residualRisk', itemIndex, '') as string; + + const query = ` + mutation UpdateDataProtectionImpactAssessment($input: UpdateDataProtectionImpactAssessmentInput!) { + updateDataProtectionImpactAssessment(input: $input) { + dataProtectionImpactAssessment { + id + description + necessityAndProportionality + potentialRisk + mitigations + residualRisk + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id }; + if (description) input.description = description; + if (necessityAndProportionality) input.necessityAndProportionality = necessityAndProportionality; + if (potentialRisk) input.potentialRisk = potentialRisk; + if (mitigations) input.mitigations = mitigations; + if (residualRisk) input.residualRisk = residualRisk; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/evidence/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/evidence/delete.operation.ts new file mode 100644 index 000000000..611d1dd11 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/evidence/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Evidence ID', + name: 'evidenceId', + type: 'string', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the evidence to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const evidenceId = this.getNodeParameter('evidenceId', itemIndex) as string; + + const query = ` + mutation DeleteEvidence($input: DeleteEvidenceInput!) { + deleteEvidence(input: $input) { + deletedEvidenceId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { evidenceId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/evidence/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/evidence/get.operation.ts new file mode 100644 index 000000000..c29adb920 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/evidence/get.operation.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Evidence ID', + name: 'evidenceId', + type: 'string', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the evidence', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const evidenceId = this.getNodeParameter('evidenceId', itemIndex) as string; + + const query = ` + query GetEvidence($evidenceId: ID!) { + node(id: $evidenceId) { + ... on Evidence { + id + state + type + description + createdAt + updatedAt + } + } + } + `; + + const variables = { + evidenceId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/evidence/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/evidence/getAll.operation.ts new file mode 100644 index 000000000..3da185a70 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/evidence/getAll.operation.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the measure to list evidences for', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['getAll'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetEvidences($measureId: ID!, $first: Int, $after: CursorKey) { + node(id: $measureId) { + ... on Measure { + evidences(first: $first, after: $after) { + edges { + node { + id + state + type + description + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const evidences = await proboApiRequestAllItems.call( + this, + query, + { measureId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.evidences as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { evidences }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/evidence/index.ts b/packages/n8n-node/nodes/Probo/actions/evidence/index.ts new file mode 100644 index 000000000..299aa5620 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/evidence/index.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; +import * as uploadOp from './upload.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['evidence'], + }, + }, + options: [ + { + name: 'Delete', + value: 'delete', + description: 'Delete an evidence', + action: 'Delete an evidence', + }, + { + name: 'Get', + value: 'get', + description: 'Get an evidence', + action: 'Get an evidence', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many evidences for a measure', + action: 'Get many evidences', + }, + { + name: 'Upload', + value: 'upload', + description: 'Upload evidence for a measure', + action: 'Upload evidence', + }, + ], + default: 'getAll', + }, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, + ...uploadOp.description, +]; + +export { deleteOp as delete, getOp as get, getAllOp as getAll, uploadOp as upload }; diff --git a/packages/n8n-node/nodes/Probo/actions/evidence/upload.operation.ts b/packages/n8n-node/nodes/Probo/actions/evidence/upload.operation.ts new file mode 100644 index 000000000..6a21e3a9c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/evidence/upload.operation.ts @@ -0,0 +1,100 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiMultipartRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['upload'], + }, + }, + default: '', + description: 'The ID of the measure to upload evidence for', + required: true, + }, + { + displayName: 'Input Data Field Name', + name: 'binaryPropertyName', + type: 'string', + displayOptions: { + show: { + resource: ['evidence'], + operation: ['upload'], + }, + }, + default: 'data', + description: 'The name of the input field containing the binary file data to upload', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + const binaryPropertyName = this.getNodeParameter('binaryPropertyName', itemIndex) as string; + + const binaryData = this.helpers.assertBinaryData(itemIndex, binaryPropertyName); + const fileBuffer = await this.helpers.getBinaryDataBuffer(itemIndex, binaryPropertyName); + + const fileName = binaryData.fileName || 'evidence'; + const mimeType = binaryData.mimeType || 'application/octet-stream'; + + const query = ` + mutation UploadMeasureEvidence($input: UploadMeasureEvidenceInput!) { + uploadMeasureEvidence(input: $input) { + evidenceEdge { + node { + id + state + type + description + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + measureId, + file: null, + }, + }; + + const responseData = await proboApiMultipartRequest.call( + this, + query, + variables, + 'variables.input.file', + fileBuffer, + fileName, + mimeType, + ); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/create.operation.ts new file mode 100644 index 000000000..636f1b2f4 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/create.operation.ts @@ -0,0 +1,272 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Kind', + name: 'kind', + type: 'options', + displayOptions: { + show: { + resource: ['finding'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Minor Nonconformity', + value: 'MINOR_NONCONFORMITY', + }, + { + name: 'Major Nonconformity', + value: 'MAJOR_NONCONFORMITY', + }, + { + name: 'Observation', + value: 'OBSERVATION', + }, + { + name: 'Exception', + value: 'EXCEPTION', + }, + ], + default: 'MINOR_NONCONFORMITY', + description: 'The kind of finding', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['create'], + }, + }, + default: '', + description: 'The description of the finding', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['finding'], + operation: ['create'], + }, + }, + options: [ + { + displayName: 'Corrective Action', + name: 'correctiveAction', + type: 'string', + default: '', + description: 'The corrective action for the finding', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'string', + default: '', + description: 'The due date for the finding (ISO 8601 format)', + }, + { + displayName: 'Effectiveness Check', + name: 'effectivenessCheck', + type: 'string', + default: '', + description: 'The effectiveness check for the finding', + }, + { + displayName: 'Identified On', + name: 'identifiedOn', + type: 'string', + default: '', + description: 'The date the finding was identified (ISO 8601 format)', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'string', + default: '', + description: 'The ID of the person who owns this finding', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + options: [ + { + name: 'Low', + value: 'LOW', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'High', + value: 'HIGH', + }, + ], + default: 'MEDIUM', + description: 'The priority of the finding', + }, + { + displayName: 'Risk ID', + name: 'riskId', + type: 'string', + default: '', + description: 'The ID of the associated risk', + }, + { + displayName: 'Root Cause', + name: 'rootCause', + type: 'string', + default: '', + description: 'The root cause of the finding', + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + description: 'The source of the finding', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: 'Closed', + value: 'CLOSED', + }, + { + name: 'False Positive', + value: 'FALSE_POSITIVE', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Mitigated', + value: 'MITIGATED', + }, + { + name: 'Open', + value: 'OPEN', + }, + { + name: 'Risk Accepted', + value: 'RISK_ACCEPTED', + }, + ], + default: 'OPEN', + description: 'The status of the finding', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const kind = this.getNodeParameter('kind', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + source?: string; + identifiedOn?: string; + rootCause?: string; + correctiveAction?: string; + ownerId?: string; + dueDate?: string; + status?: string; + priority?: string; + riskId?: string; + effectivenessCheck?: string; + }; + + const query = ` + mutation CreateFinding($input: CreateFindingInput!) { + createFinding(input: $input) { + findingEdge { + node { + id + kind + description + source + identifiedOn + rootCause + correctiveAction + dueDate + status + priority + effectivenessCheck + createdAt + updatedAt + } + } + } + } + `; + + const input: Record = { + organizationId, + kind, + description, + }; + if (additionalFields.source) input.source = additionalFields.source; + if (additionalFields.identifiedOn) input.identifiedOn = additionalFields.identifiedOn; + if (additionalFields.rootCause) input.rootCause = additionalFields.rootCause; + if (additionalFields.correctiveAction) input.correctiveAction = additionalFields.correctiveAction; + if (additionalFields.ownerId) input.ownerId = additionalFields.ownerId; + if (additionalFields.dueDate) input.dueDate = additionalFields.dueDate; + if (additionalFields.status) input.status = additionalFields.status; + if (additionalFields.priority) input.priority = additionalFields.priority; + if (additionalFields.riskId) input.riskId = additionalFields.riskId; + if (additionalFields.effectivenessCheck) input.effectivenessCheck = additionalFields.effectivenessCheck; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/delete.operation.ts new file mode 100644 index 000000000..6c3306816 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Finding ID', + name: 'findingId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the finding to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const findingId = this.getNodeParameter('findingId', itemIndex) as string; + + const query = ` + mutation DeleteFinding($input: DeleteFindingInput!) { + deleteFinding(input: $input) { + deletedFindingId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { findingId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/get.operation.ts new file mode 100644 index 000000000..02f80303d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/get.operation.ts @@ -0,0 +1,73 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Finding ID', + name: 'findingId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the finding', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const findingId = this.getNodeParameter('findingId', itemIndex) as string; + + const query = ` + query GetFinding($findingId: ID!) { + node(id: $findingId) { + ... on Finding { + id + kind + description + source + identifiedOn + rootCause + correctiveAction + dueDate + status + priority + effectivenessCheck + createdAt + updatedAt + } + } + } + `; + + const variables = { + findingId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/getAll.operation.ts new file mode 100644 index 000000000..d0f1a6934 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/getAll.operation.ts @@ -0,0 +1,122 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['finding'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['finding'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['finding'], + operation: ['getAll'], + 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 GetFindings($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + findings(first: $first, after: $after) { + edges { + node { + id + kind + description + source + identifiedOn + rootCause + correctiveAction + dueDate + status + priority + effectivenessCheck + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const findings = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.findings as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { findings }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/index.ts b/packages/n8n-node/nodes/Probo/actions/finding/index.ts new file mode 100644 index 000000000..b8c338801 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/index.ts @@ -0,0 +1,98 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; +import * as linkAuditOp from './linkAudit.operation'; +import * as unlinkAuditOp from './unlinkAudit.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['finding'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new finding', + action: 'Create a finding', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a finding', + action: 'Delete a finding', + }, + { + name: 'Get', + value: 'get', + description: 'Get a finding', + action: 'Get a finding', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many findings', + action: 'Get many findings', + }, + { + name: 'Link Audit', + value: 'linkAudit', + description: 'Link an audit to a finding', + action: 'Link an audit to a finding', + }, + { + name: 'Unlink Audit', + value: 'unlinkAudit', + description: 'Unlink an audit from a finding', + action: 'Unlink an audit from a finding', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing finding', + action: 'Update a finding', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, + ...linkAuditOp.description, + ...unlinkAuditOp.description, +]; + +export { + createOp as create, + updateOp as update, + deleteOp as delete, + getOp as get, + getAllOp as getAll, + linkAuditOp as linkAudit, + unlinkAuditOp as unlinkAudit, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/finding/linkAudit.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/linkAudit.operation.ts new file mode 100644 index 000000000..ec41d1b91 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/linkAudit.operation.ts @@ -0,0 +1,87 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Finding ID', + name: 'findingId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['linkAudit'], + }, + }, + default: '', + description: 'The ID of the finding', + required: true, + }, + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['linkAudit'], + }, + }, + default: '', + description: 'The ID of the audit to link', + required: true, + }, + { + displayName: 'Reference ID', + name: 'referenceId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['linkAudit'], + }, + }, + default: '', + description: 'The reference ID for the mapping', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const findingId = this.getNodeParameter('findingId', itemIndex) as string; + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + const referenceId = this.getNodeParameter('referenceId', itemIndex) as string; + + const query = ` + mutation CreateFindingAuditMapping($input: CreateFindingAuditMappingInput!) { + createFindingAuditMapping(input: $input) { + finding { + id + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { findingId, auditId, referenceId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/unlinkAudit.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/unlinkAudit.operation.ts new file mode 100644 index 000000000..778d1fc08 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/unlinkAudit.operation.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Finding ID', + name: 'findingId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['unlinkAudit'], + }, + }, + default: '', + description: 'The ID of the finding', + required: true, + }, + { + displayName: 'Audit ID', + name: 'auditId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['unlinkAudit'], + }, + }, + default: '', + description: 'The ID of the audit to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const findingId = this.getNodeParameter('findingId', itemIndex) as string; + const auditId = this.getNodeParameter('auditId', itemIndex) as string; + + const query = ` + mutation DeleteFindingAuditMapping($input: DeleteFindingAuditMappingInput!) { + deleteFindingAuditMapping(input: $input) { + finding { + id + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { findingId, auditId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/finding/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/finding/update.operation.ts new file mode 100644 index 000000000..1be3ba969 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/finding/update.operation.ts @@ -0,0 +1,235 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Finding ID', + name: 'findingId', + type: 'string', + displayOptions: { + show: { + resource: ['finding'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the finding to update', + required: true, + }, + { + displayName: 'Additional Fields', + name: 'additionalFields', + type: 'collection', + placeholder: 'Add Field', + default: {}, + displayOptions: { + show: { + resource: ['finding'], + operation: ['update'], + }, + }, + options: [ + { + displayName: 'Corrective Action', + name: 'correctiveAction', + type: 'string', + default: '', + description: 'The corrective action for the finding', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + default: '', + description: 'The description of the finding', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'string', + default: '', + description: 'The due date for the finding (ISO 8601 format)', + }, + { + displayName: 'Effectiveness Check', + name: 'effectivenessCheck', + type: 'string', + default: '', + description: 'The effectiveness check for the finding', + }, + { + displayName: 'Identified On', + name: 'identifiedOn', + type: 'string', + default: '', + description: 'The date the finding was identified (ISO 8601 format)', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'string', + default: '', + description: 'The ID of the person who owns this finding', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Low', + value: 'LOW', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'High', + value: 'HIGH', + }, + ], + default: '', + description: 'The priority of the finding', + }, + { + displayName: 'Risk ID', + name: 'riskId', + type: 'string', + default: '', + description: 'The ID of the associated risk', + }, + { + displayName: 'Root Cause', + name: 'rootCause', + type: 'string', + default: '', + description: 'The root cause of the finding', + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + default: '', + description: 'The source of the finding', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Closed', + value: 'CLOSED', + }, + { + name: 'False Positive', + value: 'FALSE_POSITIVE', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Mitigated', + value: 'MITIGATED', + }, + { + name: 'Open', + value: 'OPEN', + }, + { + name: 'Risk Accepted', + value: 'RISK_ACCEPTED', + }, + ], + default: '', + description: 'The status of the finding', + }, + ], + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const findingId = this.getNodeParameter('findingId', itemIndex) as string; + const additionalFields = this.getNodeParameter('additionalFields', itemIndex, {}) as { + description?: string; + source?: string; + identifiedOn?: string; + rootCause?: string; + correctiveAction?: string; + ownerId?: string; + dueDate?: string; + status?: string; + priority?: string; + riskId?: string; + effectivenessCheck?: string; + }; + + const query = ` + mutation UpdateFinding($input: UpdateFindingInput!) { + updateFinding(input: $input) { + finding { + id + kind + description + source + identifiedOn + rootCause + correctiveAction + dueDate + status + priority + effectivenessCheck + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id: findingId }; + if (additionalFields.description !== undefined) input.description = additionalFields.description === '' ? null : additionalFields.description; + if (additionalFields.source !== undefined) input.source = additionalFields.source === '' ? null : additionalFields.source; + if (additionalFields.identifiedOn !== undefined) input.identifiedOn = additionalFields.identifiedOn === '' ? null : additionalFields.identifiedOn; + if (additionalFields.rootCause !== undefined) input.rootCause = additionalFields.rootCause === '' ? null : additionalFields.rootCause; + if (additionalFields.correctiveAction !== undefined) input.correctiveAction = additionalFields.correctiveAction === '' ? null : additionalFields.correctiveAction; + if (additionalFields.ownerId !== undefined) input.ownerId = additionalFields.ownerId === '' ? null : additionalFields.ownerId; + if (additionalFields.dueDate !== undefined) input.dueDate = additionalFields.dueDate === '' ? null : additionalFields.dueDate; + if (additionalFields.status !== undefined) input.status = additionalFields.status; + if (additionalFields.priority !== undefined) input.priority = additionalFields.priority; + if (additionalFields.riskId !== undefined) input.riskId = additionalFields.riskId === '' ? null : additionalFields.riskId; + if (additionalFields.effectivenessCheck !== undefined) input.effectivenessCheck = additionalFields.effectivenessCheck === '' ? null : additionalFields.effectivenessCheck; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/index.ts b/packages/n8n-node/nodes/Probo/actions/index.ts index 02fb55bce..7e59a3f10 100644 --- a/packages/n8n-node/nodes/Probo/actions/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/index.ts @@ -13,18 +13,31 @@ // PERFORMANCE OF THIS SOFTWARE. import type { IExecuteFunctions, INodeExecutionData, INodeProperties } from 'n8n-workflow'; +import * as accessReview from './accessReview'; import * as asset from './asset'; import * as audit from './audit'; +import * as auditLog from './auditLog'; import * as control from './control'; import * as datum from './datum'; import * as document from './document'; +import * as dpia from './dpia'; +import * as evidence from './evidence'; import * as execute from './execute'; +import * as finding from './finding'; import * as framework from './framework'; import * as measure from './measure'; +import * as obligation from './obligation'; import * as organization from './organization'; +import * as organizationContext from './organizationContext'; +import * as processingActivity from './processingActivity'; +import * as rightsRequest from './rightsRequest'; import * as user from './user'; import * as risk from './risk'; +import * as snapshot from './snapshot'; import * as statementOfApplicability from './statementOfApplicability'; +import * as task from './task'; +import * as tia from './tia'; +import * as trustCenter from './trustCenter'; import * as vendor from './vendor'; import * as webhook from './webhook'; @@ -39,18 +52,31 @@ export interface OperationModule { } export const resources: Record = { + accessReview: accessReview as ResourceModule, asset: asset as ResourceModule, audit: audit as ResourceModule, + auditLog: auditLog as ResourceModule, control: control as ResourceModule, datum: datum as ResourceModule, document: document as ResourceModule, + dpia: dpia as ResourceModule, + evidence: evidence as ResourceModule, execute: execute as ResourceModule, + finding: finding as ResourceModule, framework: framework as ResourceModule, measure: measure as ResourceModule, + obligation: obligation as ResourceModule, organization: organization as ResourceModule, + organizationContext: organizationContext as ResourceModule, + processingActivity: processingActivity as ResourceModule, + rightsRequest: rightsRequest as ResourceModule, user: user as ResourceModule, risk: risk as ResourceModule, + snapshot: snapshot as ResourceModule, statementOfApplicability: statementOfApplicability as ResourceModule, + task: task as ResourceModule, + tia: tia as ResourceModule, + trustCenter: trustCenter as ResourceModule, vendor: vendor as ResourceModule, webhook: webhook as ResourceModule, }; diff --git a/packages/n8n-node/nodes/Probo/actions/measure/index.ts b/packages/n8n-node/nodes/Probo/actions/measure/index.ts index f2d43a058..bffdf054d 100644 --- a/packages/n8n-node/nodes/Probo/actions/measure/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/measure/index.ts @@ -18,6 +18,8 @@ import * as updateOp from './update.operation'; import * as deleteOp from './delete.operation'; import * as getOp from './get.operation'; import * as getAllOp from './getAll.operation'; +import * as linkDocumentOp from './linkDocument.operation'; +import * as unlinkDocumentOp from './unlinkDocument.operation'; export const description: INodeProperties[] = [ { @@ -55,6 +57,18 @@ export const description: INodeProperties[] = [ description: 'Get many measures', action: 'Get many measures', }, + { + name: 'Link Document', + value: 'linkDocument', + description: 'Link a document to a measure', + action: 'Link a document to a measure', + }, + { + name: 'Unlink Document', + value: 'unlinkDocument', + description: 'Unlink a document from a measure', + action: 'Unlink a document from a measure', + }, { name: 'Update', value: 'update', @@ -69,6 +83,16 @@ export const description: INodeProperties[] = [ ...deleteOp.description, ...getOp.description, ...getAllOp.description, + ...linkDocumentOp.description, + ...unlinkDocumentOp.description, ]; -export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; +export { + createOp as create, + updateOp as update, + deleteOp as delete, + getOp as get, + getAllOp as getAll, + linkDocumentOp as linkDocument, + unlinkDocumentOp as unlinkDocument, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/measure/linkDocument.operation.ts b/packages/n8n-node/nodes/Probo/actions/measure/linkDocument.operation.ts new file mode 100644 index 000000000..acd4e97a0 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/measure/linkDocument.operation.ts @@ -0,0 +1,81 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['measure'], + operation: ['linkDocument'], + }, + }, + default: '', + description: 'The ID of the measure', + required: true, + }, + { + displayName: 'Document ID', + name: 'documentId', + type: 'string', + displayOptions: { + show: { + resource: ['measure'], + operation: ['linkDocument'], + }, + }, + default: '', + description: 'The ID of the document to link', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + const documentId = this.getNodeParameter('documentId', itemIndex) as string; + + const query = ` + mutation CreateMeasureDocumentMapping($input: CreateMeasureDocumentMappingInput!) { + createMeasureDocumentMapping(input: $input) { + measureEdge { + node { + id + name + } + } + documentEdge { + node { + id + title + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { measureId, documentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/measure/unlinkDocument.operation.ts b/packages/n8n-node/nodes/Probo/actions/measure/unlinkDocument.operation.ts new file mode 100644 index 000000000..3b2afc17d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/measure/unlinkDocument.operation.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['measure'], + operation: ['unlinkDocument'], + }, + }, + default: '', + description: 'The ID of the measure', + required: true, + }, + { + displayName: 'Document ID', + name: 'documentId', + type: 'string', + displayOptions: { + show: { + resource: ['measure'], + operation: ['unlinkDocument'], + }, + }, + default: '', + description: 'The ID of the document to unlink', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + const documentId = this.getNodeParameter('documentId', itemIndex) as string; + + const query = ` + mutation DeleteMeasureDocumentMapping($input: DeleteMeasureDocumentMappingInput!) { + deleteMeasureDocumentMapping(input: $input) { + deletedMeasureId + deletedDocumentId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { measureId, documentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/obligation/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/obligation/create.operation.ts new file mode 100644 index 000000000..6c45d933f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/obligation/create.operation.ts @@ -0,0 +1,253 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Area', + name: 'area', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The area of the obligation', + required: true, + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The source of the obligation', + required: true, + }, + { + displayName: 'Requirement', + name: 'requirement', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The requirement of the obligation', + required: true, + }, + { + displayName: 'Actions to Be Implemented', + name: 'actionsToBeImplemented', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The actions to be implemented for the obligation', + }, + { + displayName: 'Regulator', + name: 'regulator', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The regulator of the obligation', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the owner', + }, + { + displayName: 'Last Review Date', + name: 'lastReviewDate', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The last review date of the obligation', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + default: '', + description: 'The due date of the obligation', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Non Compliant', + value: 'NON_COMPLIANT', + }, + { + name: 'Partially Compliant', + value: 'PARTIALLY_COMPLIANT', + }, + { + name: 'Compliant', + value: 'COMPLIANT', + }, + ], + default: 'NON_COMPLIANT', + description: 'The status of the obligation', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Legal', + value: 'LEGAL', + }, + { + name: 'Contractual', + value: 'CONTRACTUAL', + }, + ], + default: 'LEGAL', + description: 'The type of the obligation', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const area = this.getNodeParameter('area', itemIndex) as string; + const source = this.getNodeParameter('source', itemIndex) as string; + const requirement = this.getNodeParameter('requirement', itemIndex) as string; + const actionsToBeImplemented = this.getNodeParameter('actionsToBeImplemented', itemIndex, '') as string; + const regulator = this.getNodeParameter('regulator', itemIndex, '') as string; + const ownerId = this.getNodeParameter('ownerId', itemIndex, '') as string; + const lastReviewDate = this.getNodeParameter('lastReviewDate', itemIndex, '') as string; + const dueDate = this.getNodeParameter('dueDate', itemIndex, '') as string; + const status = this.getNodeParameter('status', itemIndex, '') as string; + const type = this.getNodeParameter('type', itemIndex, '') as string; + + const query = ` + mutation CreateObligation($input: CreateObligationInput!) { + createObligation(input: $input) { + obligationEdge { + node { + id + area + source + requirement + actionsToBeImplemented + regulator + lastReviewDate + dueDate + status + type + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + organizationId, + area, + source, + requirement, + ...(actionsToBeImplemented && { actionsToBeImplemented }), + ...(regulator && { regulator }), + ...(ownerId && { ownerId }), + ...(lastReviewDate && { lastReviewDate }), + ...(dueDate && { dueDate }), + ...(status && { status }), + ...(type && { type }), + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/obligation/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/obligation/delete.operation.ts new file mode 100644 index 000000000..2de87d794 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/obligation/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Obligation ID', + name: 'obligationId', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the obligation to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const obligationId = this.getNodeParameter('obligationId', itemIndex) as string; + + const query = ` + mutation DeleteObligation($input: DeleteObligationInput!) { + deleteObligation(input: $input) { + deletedObligationId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { obligationId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/obligation/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/obligation/get.operation.ts new file mode 100644 index 000000000..4e1a8d420 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/obligation/get.operation.ts @@ -0,0 +1,72 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Obligation ID', + name: 'obligationId', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the obligation', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const obligationId = this.getNodeParameter('obligationId', itemIndex) as string; + + const query = ` + query GetObligation($obligationId: ID!) { + node(id: $obligationId) { + ... on Obligation { + id + area + source + requirement + actionsToBeImplemented + regulator + lastReviewDate + dueDate + status + type + createdAt + updatedAt + } + } + } + `; + + const variables = { + obligationId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/obligation/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/obligation/getAll.operation.ts new file mode 100644 index 000000000..8cf3b5d3d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/obligation/getAll.operation.ts @@ -0,0 +1,121 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['obligation'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['getAll'], + 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 GetObligations($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + obligations(first: $first, after: $after) { + edges { + node { + id + area + source + requirement + actionsToBeImplemented + regulator + lastReviewDate + dueDate + status + type + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const obligations = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.obligations as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { obligations }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/obligation/index.ts b/packages/n8n-node/nodes/Probo/actions/obligation/index.ts new file mode 100644 index 000000000..fcd28db34 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/obligation/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['obligation'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new obligation', + action: 'Create an obligation', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete an obligation', + action: 'Delete an obligation', + }, + { + name: 'Get', + value: 'get', + description: 'Get an obligation', + action: 'Get an obligation', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many obligations', + action: 'Get many obligations', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing obligation', + action: 'Update an obligation', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/obligation/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/obligation/update.operation.ts new file mode 100644 index 000000000..e6ba0bd09 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/obligation/update.operation.ts @@ -0,0 +1,252 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Obligation ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the obligation to update', + required: true, + }, + { + displayName: 'Area', + name: 'area', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The area of the obligation', + }, + { + displayName: 'Source', + name: 'source', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The source of the obligation', + }, + { + displayName: 'Requirement', + name: 'requirement', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The requirement of the obligation', + }, + { + displayName: 'Actions to Be Implemented', + name: 'actionsToBeImplemented', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The actions to be implemented for the obligation', + }, + { + displayName: 'Regulator', + name: 'regulator', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The regulator of the obligation', + }, + { + displayName: 'Owner ID', + name: 'ownerId', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the owner', + }, + { + displayName: 'Last Review Date', + name: 'lastReviewDate', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The last review date of the obligation', + }, + { + displayName: 'Due Date', + name: 'dueDate', + type: 'string', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + default: '', + description: 'The due date of the obligation', + }, + { + displayName: 'Status', + name: 'status', + type: 'options', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Non Compliant', + value: 'NON_COMPLIANT', + }, + { + name: 'Partially Compliant', + value: 'PARTIALLY_COMPLIANT', + }, + { + name: 'Compliant', + value: 'COMPLIANT', + }, + ], + default: '', + description: 'The status of the obligation', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + displayOptions: { + show: { + resource: ['obligation'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Legal', + value: 'LEGAL', + }, + { + name: 'Contractual', + value: 'CONTRACTUAL', + }, + ], + default: '', + description: 'The type of the obligation', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const id = this.getNodeParameter('id', itemIndex) as string; + const area = this.getNodeParameter('area', itemIndex, '') as string; + const source = this.getNodeParameter('source', itemIndex, '') as string; + const requirement = this.getNodeParameter('requirement', itemIndex, '') as string; + const actionsToBeImplemented = this.getNodeParameter('actionsToBeImplemented', itemIndex, '') as string; + const regulator = this.getNodeParameter('regulator', itemIndex, '') as string; + const ownerId = this.getNodeParameter('ownerId', itemIndex, '') as string; + const lastReviewDate = this.getNodeParameter('lastReviewDate', itemIndex, '') as string; + const dueDate = this.getNodeParameter('dueDate', itemIndex, '') as string; + const status = this.getNodeParameter('status', itemIndex, '') as string; + const type = this.getNodeParameter('type', itemIndex, '') as string; + + const query = ` + mutation UpdateObligation($input: UpdateObligationInput!) { + updateObligation(input: $input) { + obligation { + id + area + source + requirement + actionsToBeImplemented + regulator + lastReviewDate + dueDate + status + type + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id }; + if (area) input.area = area; + if (source) input.source = source; + if (requirement) input.requirement = requirement; + if (actionsToBeImplemented) input.actionsToBeImplemented = actionsToBeImplemented; + if (regulator) input.regulator = regulator; + if (ownerId) input.ownerId = ownerId; + if (lastReviewDate) input.lastReviewDate = lastReviewDate; + if (dueDate) input.dueDate = dueDate; + if (status) input.status = status; + if (type) input.type = type; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/organizationContext/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/organizationContext/get.operation.ts new file mode 100644 index 000000000..74588cdeb --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/organizationContext/get.operation.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + + const query = ` + query GetOrganizationContext($organizationId: ID!) { + node(id: $organizationId) { + ... on Organization { + context { + organizationId + product + architecture + team + processes + customers + } + } + } + } + `; + + const variables = { + organizationId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/organizationContext/index.ts b/packages/n8n-node/nodes/Probo/actions/organizationContext/index.ts new file mode 100644 index 000000000..d39cd9272 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/organizationContext/index.ts @@ -0,0 +1,50 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as getOp from './get.operation'; +import * as updateOp from './update.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['organizationContext'], + }, + }, + options: [ + { + name: 'Get', + value: 'get', + description: 'Get the organization context', + action: 'Get the organization context', + }, + { + name: 'Update', + value: 'update', + description: 'Update the organization context', + action: 'Update the organization context', + }, + ], + default: 'get', + }, + ...getOp.description, + ...updateOp.description, +]; + +export { getOp as get, updateOp as update }; diff --git a/packages/n8n-node/nodes/Probo/actions/organizationContext/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/organizationContext/update.operation.ts new file mode 100644 index 000000000..bf2a6b5a7 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/organizationContext/update.operation.ts @@ -0,0 +1,139 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Product', + name: 'product', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['update'], + }, + }, + default: '', + description: 'The product description of the organization', + }, + { + displayName: 'Architecture', + name: 'architecture', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['update'], + }, + }, + default: '', + description: 'The architecture description of the organization', + }, + { + displayName: 'Team', + name: 'team', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['update'], + }, + }, + default: '', + description: 'The team description of the organization', + }, + { + displayName: 'Processes', + name: 'processes', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['update'], + }, + }, + default: '', + description: 'The processes description of the organization', + }, + { + displayName: 'Customers', + name: 'customers', + type: 'string', + displayOptions: { + show: { + resource: ['organizationContext'], + operation: ['update'], + }, + }, + default: '', + description: 'The customers description of the organization', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const product = this.getNodeParameter('product', itemIndex, '') as string; + const architecture = this.getNodeParameter('architecture', itemIndex, '') as string; + const team = this.getNodeParameter('team', itemIndex, '') as string; + const processes = this.getNodeParameter('processes', itemIndex, '') as string; + const customers = this.getNodeParameter('customers', itemIndex, '') as string; + + const query = ` + mutation UpdateOrganizationContext($input: UpdateOrganizationContextInput!) { + updateOrganizationContext(input: $input) { + context { + organizationId + product + architecture + team + processes + customers + } + } + } + `; + + const input: Record = { organizationId }; + if (product) input.product = product; + if (architecture) input.architecture = architecture; + if (team) input.team = team; + if (processes) input.processes = processes; + if (customers) input.customers = customers; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/processingActivity/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/processingActivity/create.operation.ts new file mode 100644 index 000000000..5c1deeaae --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/processingActivity/create.operation.ts @@ -0,0 +1,171 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['create'], + }, + }, + default: '', + description: 'The name of the processing activity', + required: true, + }, + { + displayName: 'Purpose', + name: 'purpose', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['create'], + }, + }, + default: '', + description: 'The purpose of the processing activity', + required: true, + }, + { + displayName: 'Role', + name: 'role', + type: 'options', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Controller', + value: 'CONTROLLER', + }, + { + name: 'Processor', + value: 'PROCESSOR', + }, + ], + default: 'CONTROLLER', + description: 'The role for the processing activity', + required: true, + }, + { + displayName: 'Lawful Basis', + name: 'lawfulBasis', + type: 'options', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Consent', + value: 'CONSENT', + }, + { + name: 'Contractual Necessity', + value: 'CONTRACTUAL_NECESSITY', + }, + { + name: 'Legal Obligation', + value: 'LEGAL_OBLIGATION', + }, + { + name: 'Legitimate Interest', + value: 'LEGITIMATE_INTEREST', + }, + { + name: 'Public Task', + value: 'PUBLIC_TASK', + }, + { + name: 'Vital Interests', + value: 'VITAL_INTERESTS', + }, + ], + default: 'LEGITIMATE_INTEREST', + description: 'The lawful basis for the processing activity', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const purpose = this.getNodeParameter('purpose', itemIndex) as string; + const role = this.getNodeParameter('role', itemIndex) as string; + const lawfulBasis = this.getNodeParameter('lawfulBasis', itemIndex) as string; + + const query = ` + mutation CreateProcessingActivity($input: CreateProcessingActivityInput!) { + createProcessingActivity(input: $input) { + processingActivityEdge { + node { + id + name + purpose + role + lawfulBasis + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + organizationId, + name, + purpose, + role, + lawfulBasis, + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/processingActivity/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/processingActivity/delete.operation.ts new file mode 100644 index 000000000..695767fd9 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/processingActivity/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Processing Activity ID', + name: 'processingActivityId', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the processing activity to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processingActivityId = this.getNodeParameter('processingActivityId', itemIndex) as string; + + const query = ` + mutation DeleteProcessingActivity($input: DeleteProcessingActivityInput!) { + deleteProcessingActivity(input: $input) { + deletedProcessingActivityId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { processingActivityId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/processingActivity/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/processingActivity/get.operation.ts new file mode 100644 index 000000000..76bb937f7 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/processingActivity/get.operation.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Processing Activity ID', + name: 'processingActivityId', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the processing activity', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processingActivityId = this.getNodeParameter('processingActivityId', itemIndex) as string; + + const query = ` + query GetProcessingActivity($processingActivityId: ID!) { + node(id: $processingActivityId) { + ... on ProcessingActivity { + id + name + purpose + role + lawfulBasis + createdAt + updatedAt + } + } + } + `; + + const variables = { + processingActivityId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/processingActivity/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/processingActivity/getAll.operation.ts new file mode 100644 index 000000000..3a23b8207 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/processingActivity/getAll.operation.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['processingActivity'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['getAll'], + 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 GetProcessingActivities($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + processingActivities(first: $first, after: $after) { + edges { + node { + id + name + purpose + role + lawfulBasis + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const processingActivities = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.processingActivities as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { processingActivities }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/processingActivity/index.ts b/packages/n8n-node/nodes/Probo/actions/processingActivity/index.ts new file mode 100644 index 000000000..606be22f2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/processingActivity/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['processingActivity'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new processing activity', + action: 'Create a processing activity', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a processing activity', + action: 'Delete a processing activity', + }, + { + name: 'Get', + value: 'get', + description: 'Get a processing activity', + action: 'Get a processing activity', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many processing activities', + action: 'Get many processing activities', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing processing activity', + action: 'Update a processing activity', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/processingActivity/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/processingActivity/update.operation.ts new file mode 100644 index 000000000..5f281a741 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/processingActivity/update.operation.ts @@ -0,0 +1,169 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Processing Activity ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the processing activity to update', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['update'], + }, + }, + default: '', + description: 'The name of the processing activity', + }, + { + displayName: 'Purpose', + name: 'purpose', + type: 'string', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['update'], + }, + }, + default: '', + description: 'The purpose of the processing activity', + }, + { + displayName: 'Role', + name: 'role', + type: 'options', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Controller', + value: 'CONTROLLER', + }, + { + name: 'Processor', + value: 'PROCESSOR', + }, + ], + default: '', + description: 'The role for the processing activity', + }, + { + displayName: 'Lawful Basis', + name: 'lawfulBasis', + type: 'options', + displayOptions: { + show: { + resource: ['processingActivity'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Consent', + value: 'CONSENT', + }, + { + name: 'Contractual Necessity', + value: 'CONTRACTUAL_NECESSITY', + }, + { + name: 'Legal Obligation', + value: 'LEGAL_OBLIGATION', + }, + { + name: 'Legitimate Interest', + value: 'LEGITIMATE_INTEREST', + }, + { + name: 'Public Task', + value: 'PUBLIC_TASK', + }, + { + name: 'Vital Interests', + value: 'VITAL_INTERESTS', + }, + ], + default: '', + description: 'The lawful basis for the processing activity', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const id = this.getNodeParameter('id', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex, '') as string; + const purpose = this.getNodeParameter('purpose', itemIndex, '') as string; + const role = this.getNodeParameter('role', itemIndex, '') as string; + const lawfulBasis = this.getNodeParameter('lawfulBasis', itemIndex, '') as string; + + const query = ` + mutation UpdateProcessingActivity($input: UpdateProcessingActivityInput!) { + updateProcessingActivity(input: $input) { + processingActivity { + id + name + purpose + role + lawfulBasis + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id }; + if (name) input.name = name; + if (purpose) input.purpose = purpose; + if (role) input.role = role; + if (lawfulBasis) input.lawfulBasis = lawfulBasis; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/rightsRequest/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/rightsRequest/create.operation.ts new file mode 100644 index 000000000..b15fc5e61 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/rightsRequest/create.operation.ts @@ -0,0 +1,210 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Request Type', + name: 'requestType', + type: 'options', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Access', + value: 'ACCESS', + }, + { + name: 'Deletion', + value: 'DELETION', + }, + { + name: 'Portability', + value: 'PORTABILITY', + }, + ], + default: 'ACCESS', + description: 'The type of rights request', + required: true, + }, + { + displayName: 'Request State', + name: 'requestState', + type: 'options', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + options: [ + { + name: 'To Do', + value: 'TODO', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Done', + value: 'DONE', + }, + ], + default: 'TODO', + description: 'The state of the rights request', + required: true, + }, + { + displayName: 'Data Subject', + name: 'dataSubject', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + default: '', + description: 'The data subject of the rights request', + required: true, + }, + { + displayName: 'Contact', + name: 'contact', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + default: '', + description: 'The contact for the rights request', + }, + { + displayName: 'Details', + name: 'details', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + default: '', + description: 'The details of the rights request', + }, + { + displayName: 'Deadline', + name: 'deadline', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + default: '', + description: 'The deadline for the rights request', + }, + { + displayName: 'Action Taken', + name: 'actionTaken', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['create'], + }, + }, + default: '', + description: 'The action taken for the rights request', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const requestType = this.getNodeParameter('requestType', itemIndex) as string; + const requestState = this.getNodeParameter('requestState', itemIndex) as string; + const dataSubject = this.getNodeParameter('dataSubject', itemIndex) as string; + const contact = this.getNodeParameter('contact', itemIndex, '') as string; + const details = this.getNodeParameter('details', itemIndex, '') as string; + const deadline = this.getNodeParameter('deadline', itemIndex, '') as string; + const actionTaken = this.getNodeParameter('actionTaken', itemIndex, '') as string; + + const query = ` + mutation CreateRightsRequest($input: CreateRightsRequestInput!) { + createRightsRequest(input: $input) { + rightsRequestEdge { + node { + id + requestType + requestState + dataSubject + contact + details + deadline + actionTaken + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + organizationId, + requestType, + requestState, + dataSubject, + ...(contact && { contact }), + ...(details && { details }), + ...(deadline && { deadline }), + ...(actionTaken && { actionTaken }), + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/rightsRequest/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/rightsRequest/delete.operation.ts new file mode 100644 index 000000000..a79f9ec17 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/rightsRequest/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Rights Request ID', + name: 'rightsRequestId', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the rights request to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const rightsRequestId = this.getNodeParameter('rightsRequestId', itemIndex) as string; + + const query = ` + mutation DeleteRightsRequest($input: DeleteRightsRequestInput!) { + deleteRightsRequest(input: $input) { + deletedRightsRequestId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { rightsRequestId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/rightsRequest/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/rightsRequest/get.operation.ts new file mode 100644 index 000000000..8ba6aa0aa --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/rightsRequest/get.operation.ts @@ -0,0 +1,70 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Rights Request ID', + name: 'rightsRequestId', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the rights request', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const rightsRequestId = this.getNodeParameter('rightsRequestId', itemIndex) as string; + + const query = ` + query GetRightsRequest($rightsRequestId: ID!) { + node(id: $rightsRequestId) { + ... on RightsRequest { + id + requestType + requestState + dataSubject + contact + details + deadline + actionTaken + createdAt + updatedAt + } + } + } + `; + + const variables = { + rightsRequestId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/rightsRequest/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/rightsRequest/getAll.operation.ts new file mode 100644 index 000000000..81217951a --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/rightsRequest/getAll.operation.ts @@ -0,0 +1,119 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['rightsRequest'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['getAll'], + 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 GetRightsRequests($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + rightsRequests(first: $first, after: $after) { + edges { + node { + id + requestType + requestState + dataSubject + contact + details + deadline + actionTaken + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const rightsRequests = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.rightsRequests as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { rightsRequests }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/rightsRequest/index.ts b/packages/n8n-node/nodes/Probo/actions/rightsRequest/index.ts new file mode 100644 index 000000000..402708c8d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/rightsRequest/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['rightsRequest'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new rights request', + action: 'Create a rights request', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a rights request', + action: 'Delete a rights request', + }, + { + name: 'Get', + value: 'get', + description: 'Get a rights request', + action: 'Get a rights request', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many rights requests', + action: 'Get many rights requests', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing rights request', + action: 'Update a rights request', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/rightsRequest/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/rightsRequest/update.operation.ts new file mode 100644 index 000000000..4a41dc50d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/rightsRequest/update.operation.ts @@ -0,0 +1,209 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Rights Request ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the rights request to update', + required: true, + }, + { + displayName: 'Request Type', + name: 'requestType', + type: 'options', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Access', + value: 'ACCESS', + }, + { + name: 'Deletion', + value: 'DELETION', + }, + { + name: 'Portability', + value: 'PORTABILITY', + }, + ], + default: '', + description: 'The type of rights request', + }, + { + displayName: 'Request State', + name: 'requestState', + type: 'options', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'To Do', + value: 'TODO', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Done', + value: 'DONE', + }, + ], + default: '', + description: 'The state of the rights request', + }, + { + displayName: 'Data Subject', + name: 'dataSubject', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + default: '', + description: 'The data subject of the rights request', + }, + { + displayName: 'Contact', + name: 'contact', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + default: '', + description: 'The contact for the rights request', + }, + { + displayName: 'Details', + name: 'details', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + default: '', + description: 'The details of the rights request', + }, + { + displayName: 'Deadline', + name: 'deadline', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + default: '', + description: 'The deadline for the rights request', + }, + { + displayName: 'Action Taken', + name: 'actionTaken', + type: 'string', + displayOptions: { + show: { + resource: ['rightsRequest'], + operation: ['update'], + }, + }, + default: '', + description: 'The action taken for the rights request', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const id = this.getNodeParameter('id', itemIndex) as string; + const requestType = this.getNodeParameter('requestType', itemIndex, '') as string; + const requestState = this.getNodeParameter('requestState', itemIndex, '') as string; + const dataSubject = this.getNodeParameter('dataSubject', itemIndex, '') as string; + const contact = this.getNodeParameter('contact', itemIndex, '') as string; + const details = this.getNodeParameter('details', itemIndex, '') as string; + const deadline = this.getNodeParameter('deadline', itemIndex, '') as string; + const actionTaken = this.getNodeParameter('actionTaken', itemIndex, '') as string; + + const query = ` + mutation UpdateRightsRequest($input: UpdateRightsRequestInput!) { + updateRightsRequest(input: $input) { + rightsRequest { + id + requestType + requestState + dataSubject + contact + details + deadline + actionTaken + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id }; + if (requestType) input.requestType = requestType; + if (requestState) input.requestState = requestState; + if (dataSubject) input.dataSubject = dataSubject; + if (contact) input.contact = contact; + if (details) input.details = details; + if (deadline) input.deadline = deadline; + if (actionTaken) input.actionTaken = actionTaken; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/risk/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/risk/update.operation.ts index fbaa72a20..fb71b5198 100644 --- a/packages/n8n-node/nodes/Probo/actions/risk/update.operation.ts +++ b/packages/n8n-node/nodes/Probo/actions/risk/update.operation.ts @@ -67,10 +67,7 @@ export const description: INodeProperties[] = [ }, }, options: [ - { - name: 'Mitigated', - value: 'MITIGATED', - }, + { name: '(Unchanged)', value: '' }, { name: 'Accepted', value: 'ACCEPTED', @@ -79,12 +76,16 @@ export const description: INodeProperties[] = [ name: 'Avoided', value: 'AVOIDED', }, + { + name: 'Mitigated', + value: 'MITIGATED', + }, { name: 'Transferred', value: 'TRANSFERRED', }, ], - default: 'MITIGATED', + default: '', description: 'The treatment strategy for the risk', }, { diff --git a/packages/n8n-node/nodes/Probo/actions/snapshot/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/snapshot/create.operation.ts new file mode 100644 index 000000000..4d014d3be --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/snapshot/create.operation.ts @@ -0,0 +1,146 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['create'], + }, + }, + default: '', + description: 'The name of the snapshot', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['create'], + }, + }, + default: '', + description: 'The description of the snapshot', + }, + { + displayName: 'Type', + name: 'type', + type: 'options', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Assets', + value: 'ASSETS', + }, + { + name: 'Findings', + value: 'FINDINGS', + }, + { + name: 'Obligations', + value: 'OBLIGATIONS', + }, + { + name: 'Processing Activities', + value: 'PROCESSING_ACTIVITIES', + }, + { + name: 'Risks', + value: 'RISKS', + }, + { + name: 'Statements of Applicability', + value: 'STATEMENTS_OF_APPLICABILITY', + }, + { + name: 'Vendors', + value: 'VENDORS', + }, + ], + default: 'RISKS', + description: 'The type of snapshot', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const type = this.getNodeParameter('type', itemIndex) as string; + + const query = ` + mutation CreateSnapshot($input: CreateSnapshotInput!) { + createSnapshot(input: $input) { + snapshotEdge { + node { + id + name + description + type + createdAt + } + } + } + } + `; + + const variables = { + input: { + organizationId, + name, + ...(description && { description }), + type, + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/snapshot/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/snapshot/delete.operation.ts new file mode 100644 index 000000000..e05deaee0 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/snapshot/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Snapshot ID', + name: 'snapshotId', + type: 'string', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the snapshot to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string; + + const query = ` + mutation DeleteSnapshot($input: DeleteSnapshotInput!) { + deleteSnapshot(input: $input) { + deletedSnapshotId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { snapshotId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/snapshot/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/snapshot/get.operation.ts new file mode 100644 index 000000000..47c202d90 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/snapshot/get.operation.ts @@ -0,0 +1,65 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Snapshot ID', + name: 'snapshotId', + type: 'string', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the snapshot', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const snapshotId = this.getNodeParameter('snapshotId', itemIndex) as string; + + const query = ` + query GetSnapshot($snapshotId: ID!) { + node(id: $snapshotId) { + ... on Snapshot { + id + name + description + type + createdAt + } + } + } + `; + + const variables = { + snapshotId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/snapshot/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/snapshot/getAll.operation.ts new file mode 100644 index 000000000..b42320105 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/snapshot/getAll.operation.ts @@ -0,0 +1,114 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['snapshot'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['snapshot'], + operation: ['getAll'], + 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 GetSnapshots($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + snapshots(first: $first, after: $after) { + edges { + node { + id + name + description + type + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const snapshots = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.snapshots as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { snapshots }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/snapshot/index.ts b/packages/n8n-node/nodes/Probo/actions/snapshot/index.ts new file mode 100644 index 000000000..4c5e069bd --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/snapshot/index.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['snapshot'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new snapshot', + action: 'Create a snapshot', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a snapshot', + action: 'Delete a snapshot', + }, + { + name: 'Get', + value: 'get', + description: 'Get a snapshot', + action: 'Get a snapshot', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many snapshots', + action: 'Get many snapshots', + }, + ], + default: 'create', + }, + ...createOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/task/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/task/create.operation.ts new file mode 100644 index 000000000..f1385ea2b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/task/create.operation.ts @@ -0,0 +1,198 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the measure this task belongs to', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The name of the task', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The description of the task', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + options: [ + { + name: 'Urgent', + value: 'URGENT', + }, + { + name: 'High', + value: 'HIGH', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'Low', + value: 'LOW', + }, + ], + default: 'MEDIUM', + description: 'The priority of the task', + }, + { + displayName: 'Time Estimate', + name: 'timeEstimate', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The time estimate for the task', + }, + { + displayName: 'Assigned To ID', + name: 'assignedToId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the user assigned to this task', + }, + { + displayName: 'Deadline', + name: 'deadline', + type: 'dateTime', + displayOptions: { + show: { + resource: ['task'], + operation: ['create'], + }, + }, + default: '', + description: 'The deadline for the task', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + const measureId = this.getNodeParameter('measureId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const priority = this.getNodeParameter('priority', itemIndex, '') as string; + const timeEstimate = this.getNodeParameter('timeEstimate', itemIndex, '') as string; + const assignedToId = this.getNodeParameter('assignedToId', itemIndex, '') as string; + const deadline = this.getNodeParameter('deadline', itemIndex, '') as string; + + const query = ` + mutation CreateTask($input: CreateTaskInput!) { + createTask(input: $input) { + taskEdge { + node { + id + name + description + state + priority + timeEstimate + deadline + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + organizationId, + measureId, + name, + ...(description && { description }), + ...(priority && { priority }), + ...(timeEstimate && { timeEstimate }), + ...(assignedToId && { assignedToId }), + ...(deadline && { deadline }), + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/task/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/task/delete.operation.ts new file mode 100644 index 000000000..0e462b752 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/task/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the task to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const taskId = this.getNodeParameter('taskId', itemIndex) as string; + + const query = ` + mutation DeleteTask($input: DeleteTaskInput!) { + deleteTask(input: $input) { + deletedTaskId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { taskId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/task/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/task/get.operation.ts new file mode 100644 index 000000000..c53ff84dd --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/task/get.operation.ts @@ -0,0 +1,69 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the task', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const taskId = this.getNodeParameter('taskId', itemIndex) as string; + + const query = ` + query GetTask($taskId: ID!) { + node(id: $taskId) { + ... on Task { + id + name + description + state + priority + timeEstimate + deadline + createdAt + updatedAt + } + } + } + `; + + const variables = { + taskId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/task/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/task/getAll.operation.ts new file mode 100644 index 000000000..19457ce14 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/task/getAll.operation.ts @@ -0,0 +1,118 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['task'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['task'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['task'], + operation: ['getAll'], + 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 GetTasks($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + tasks(first: $first, after: $after) { + edges { + node { + id + name + description + state + priority + timeEstimate + deadline + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const tasks = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.tasks as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { tasks }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/task/index.ts b/packages/n8n-node/nodes/Probo/actions/task/index.ts new file mode 100644 index 000000000..ad8cd6978 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/task/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['task'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new task', + action: 'Create a task', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a task', + action: 'Delete a task', + }, + { + name: 'Get', + value: 'get', + description: 'Get a task', + action: 'Get a task', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many tasks', + action: 'Get many tasks', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing task', + action: 'Update a task', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/task/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/task/update.operation.ts new file mode 100644 index 000000000..91f1b135d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/task/update.operation.ts @@ -0,0 +1,242 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Task ID', + name: 'taskId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the task to update', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The name of the task', + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The description of the task', + }, + { + displayName: 'State', + name: 'state', + type: 'options', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Todo', + value: 'TODO', + }, + { + name: 'In Progress', + value: 'IN_PROGRESS', + }, + { + name: 'Done', + value: 'DONE', + }, + ], + default: '', + description: 'The state of the task', + }, + { + displayName: 'Priority', + name: 'priority', + type: 'options', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'High', + value: 'HIGH', + }, + { + name: 'Low', + value: 'LOW', + }, + { + name: 'Medium', + value: 'MEDIUM', + }, + { + name: 'Urgent', + value: 'URGENT', + }, + ], + default: '', + description: 'The priority of the task', + }, + { + displayName: 'Rank', + name: 'rank', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The rank of the task for ordering', + }, + { + displayName: 'Time Estimate', + name: 'timeEstimate', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The time estimate for the task', + }, + { + displayName: 'Deadline', + name: 'deadline', + type: 'dateTime', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The deadline for the task', + }, + { + displayName: 'Assigned To ID', + name: 'assignedToId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the user assigned to this task', + }, + { + displayName: 'Measure ID', + name: 'measureId', + type: 'string', + displayOptions: { + show: { + resource: ['task'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the measure this task belongs to', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const taskId = this.getNodeParameter('taskId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex, '') as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const state = this.getNodeParameter('state', itemIndex, '') as string; + const priority = this.getNodeParameter('priority', itemIndex, '') as string; + const rank = this.getNodeParameter('rank', itemIndex, '') as string; + const timeEstimate = this.getNodeParameter('timeEstimate', itemIndex, '') as string; + const deadline = this.getNodeParameter('deadline', itemIndex, '') as string; + const assignedToId = this.getNodeParameter('assignedToId', itemIndex, '') as string; + const measureId = this.getNodeParameter('measureId', itemIndex, '') as string; + + const query = ` + mutation UpdateTask($input: UpdateTaskInput!) { + updateTask(input: $input) { + task { + id + name + description + state + priority + timeEstimate + deadline + createdAt + updatedAt + } + } + } + `; + + const input: Record = { taskId }; + if (name) input.name = name; + if (description) input.description = description; + if (state) input.state = state; + if (priority) input.priority = priority; + if (rank) input.rank = rank; + if (timeEstimate) input.timeEstimate = timeEstimate; + if (deadline) input.deadline = deadline; + if (assignedToId) input.assignedToId = assignedToId; + if (measureId) input.measureId = measureId; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/tia/create.operation.ts b/packages/n8n-node/nodes/Probo/actions/tia/create.operation.ts new file mode 100644 index 000000000..c620c302b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/tia/create.operation.ts @@ -0,0 +1,152 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Processing Activity ID', + name: 'processingActivityId', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['create'], + }, + }, + default: '', + description: 'The ID of the processing activity', + required: true, + }, + { + displayName: 'Data Subjects', + name: 'dataSubjects', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['create'], + }, + }, + default: '', + description: 'The data subjects involved in the transfer', + required: true, + }, + { + displayName: 'Legal Mechanism', + name: 'legalMechanism', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['create'], + }, + }, + default: '', + description: 'The legal mechanism for the transfer', + required: true, + }, + { + displayName: 'Transfer', + name: 'transfer', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['create'], + }, + }, + default: '', + description: 'The transfer details', + required: true, + }, + { + displayName: 'Local Law Risk', + name: 'localLawRisk', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['create'], + }, + }, + default: '', + description: 'The local law risk assessment', + required: true, + }, + { + displayName: 'Supplementary Measures', + name: 'supplementaryMeasures', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['create'], + }, + }, + default: '', + description: 'The supplementary measures for the transfer', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const processingActivityId = this.getNodeParameter('processingActivityId', itemIndex) as string; + const dataSubjects = this.getNodeParameter('dataSubjects', itemIndex) as string; + const legalMechanism = this.getNodeParameter('legalMechanism', itemIndex) as string; + const transfer = this.getNodeParameter('transfer', itemIndex) as string; + const localLawRisk = this.getNodeParameter('localLawRisk', itemIndex) as string; + const supplementaryMeasures = this.getNodeParameter('supplementaryMeasures', itemIndex) as string; + + const query = ` + mutation CreateTransferImpactAssessment($input: CreateTransferImpactAssessmentInput!) { + createTransferImpactAssessment(input: $input) { + transferImpactAssessmentEdge { + node { + id + dataSubjects + legalMechanism + transfer + localLawRisk + supplementaryMeasures + createdAt + updatedAt + } + } + } + } + `; + + const variables = { + input: { + processingActivityId, + dataSubjects, + legalMechanism, + transfer, + localLawRisk, + supplementaryMeasures, + }, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/tia/delete.operation.ts b/packages/n8n-node/nodes/Probo/actions/tia/delete.operation.ts new file mode 100644 index 000000000..183aa1feb --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/tia/delete.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'TIA ID', + name: 'transferImpactAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['delete'], + }, + }, + default: '', + description: 'The ID of the TIA to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const transferImpactAssessmentId = this.getNodeParameter('transferImpactAssessmentId', itemIndex) as string; + + const query = ` + mutation DeleteTransferImpactAssessment($input: DeleteTransferImpactAssessmentInput!) { + deleteTransferImpactAssessment(input: $input) { + deletedTransferImpactAssessmentId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { transferImpactAssessmentId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/tia/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/tia/get.operation.ts new file mode 100644 index 000000000..592129cc4 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/tia/get.operation.ts @@ -0,0 +1,68 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'TIA ID', + name: 'transferImpactAssessmentId', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the TIA', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const transferImpactAssessmentId = this.getNodeParameter('transferImpactAssessmentId', itemIndex) as string; + + const query = ` + query GetTransferImpactAssessment($transferImpactAssessmentId: ID!) { + node(id: $transferImpactAssessmentId) { + ... on TransferImpactAssessment { + id + dataSubjects + legalMechanism + transfer + localLawRisk + supplementaryMeasures + createdAt + updatedAt + } + } + } + `; + + const variables = { + transferImpactAssessmentId, + }; + + const responseData = await proboApiRequest.call(this, query, variables); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/tia/getAll.operation.ts b/packages/n8n-node/nodes/Probo/actions/tia/getAll.operation.ts new file mode 100644 index 000000000..0d6dea3c2 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/tia/getAll.operation.ts @@ -0,0 +1,117 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['tia'], + operation: ['getAll'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['tia'], + operation: ['getAll'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['tia'], + operation: ['getAll'], + 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 GetTransferImpactAssessments($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + transferImpactAssessments(first: $first, after: $after) { + edges { + node { + id + dataSubjects + legalMechanism + transfer + localLawRisk + supplementaryMeasures + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const transferImpactAssessments = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.transferImpactAssessments as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { transferImpactAssessments }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/tia/index.ts b/packages/n8n-node/nodes/Probo/actions/tia/index.ts new file mode 100644 index 000000000..cbedd2c88 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/tia/index.ts @@ -0,0 +1,74 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as createOp from './create.operation'; +import * as updateOp from './update.operation'; +import * as deleteOp from './delete.operation'; +import * as getOp from './get.operation'; +import * as getAllOp from './getAll.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['tia'], + }, + }, + options: [ + { + name: 'Create', + value: 'create', + description: 'Create a new TIA', + action: 'Create a TIA', + }, + { + name: 'Delete', + value: 'delete', + description: 'Delete a TIA', + action: 'Delete a TIA', + }, + { + name: 'Get', + value: 'get', + description: 'Get a TIA', + action: 'Get a TIA', + }, + { + name: 'Get Many', + value: 'getAll', + description: 'Get many TIAs', + action: 'Get many tias', + }, + { + name: 'Update', + value: 'update', + description: 'Update an existing TIA', + action: 'Update a TIA', + }, + ], + default: 'create', + }, + ...createOp.description, + ...updateOp.description, + ...deleteOp.description, + ...getOp.description, + ...getAllOp.description, +]; + +export { createOp as create, updateOp as update, deleteOp as delete, getOp as get, getAllOp as getAll }; diff --git a/packages/n8n-node/nodes/Probo/actions/tia/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/tia/update.operation.ts new file mode 100644 index 000000000..450f0706d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/tia/update.operation.ts @@ -0,0 +1,141 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'TIA ID', + name: 'id', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['update'], + }, + }, + default: '', + description: 'The ID of the TIA to update', + required: true, + }, + { + displayName: 'Data Subjects', + name: 'dataSubjects', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['update'], + }, + }, + default: '', + description: 'The data subjects involved in the transfer', + }, + { + displayName: 'Legal Mechanism', + name: 'legalMechanism', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['update'], + }, + }, + default: '', + description: 'The legal mechanism for the transfer', + }, + { + displayName: 'Transfer', + name: 'transfer', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['update'], + }, + }, + default: '', + description: 'The transfer details', + }, + { + displayName: 'Local Law Risk', + name: 'localLawRisk', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['update'], + }, + }, + default: '', + description: 'The local law risk assessment', + }, + { + displayName: 'Supplementary Measures', + name: 'supplementaryMeasures', + type: 'string', + displayOptions: { + show: { + resource: ['tia'], + operation: ['update'], + }, + }, + default: '', + description: 'The supplementary measures for the transfer', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const id = this.getNodeParameter('id', itemIndex) as string; + const dataSubjects = this.getNodeParameter('dataSubjects', itemIndex, '') as string; + const legalMechanism = this.getNodeParameter('legalMechanism', itemIndex, '') as string; + const transfer = this.getNodeParameter('transfer', itemIndex, '') as string; + const localLawRisk = this.getNodeParameter('localLawRisk', itemIndex, '') as string; + const supplementaryMeasures = this.getNodeParameter('supplementaryMeasures', itemIndex, '') as string; + + const query = ` + mutation UpdateTransferImpactAssessment($input: UpdateTransferImpactAssessmentInput!) { + updateTransferImpactAssessment(input: $input) { + transferImpactAssessment { + id + dataSubjects + legalMechanism + transfer + localLawRisk + supplementaryMeasures + createdAt + updatedAt + } + } + } + `; + + const input: Record = { id }; + if (dataSubjects) input.dataSubjects = dataSubjects; + if (legalMechanism) input.legalMechanism = legalMechanism; + if (transfer) input.transfer = transfer; + if (localLawRisk) input.localLawRisk = localLawRisk; + if (supplementaryMeasures) input.supplementaryMeasures = supplementaryMeasures; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/createExternalUrl.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/createExternalUrl.operation.ts new file mode 100644 index 000000000..cc4f0ee56 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/createExternalUrl.operation.ts @@ -0,0 +1,96 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['createExternalUrl'], + }, + }, + default: '', + description: 'The ID of the trust center', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createExternalUrl'], + }, + }, + default: '', + description: 'The name of the external URL', + required: true, + }, + { + displayName: 'URL', + name: 'url', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createExternalUrl'], + }, + }, + default: '', + description: 'The external URL', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const trustCenterId = this.getNodeParameter('trustCenterId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const url = this.getNodeParameter('url', itemIndex) as string; + + const query = ` + mutation CreateComplianceExternalURL($input: CreateComplianceExternalURLInput!) { + createComplianceExternalURL(input: $input) { + complianceExternalURLEdge { + node { + id + name + url + rank + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { + input: { trustCenterId, name, url }, + }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/createReference.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/createReference.operation.ts new file mode 100644 index 000000000..a58e9fcc9 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/createReference.operation.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['createReference'], + }, + }, + default: '', + description: 'The ID of the trust center', + required: true, + }, + { + displayName: 'Name', + name: 'name', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createReference'], + }, + }, + default: '', + description: 'The name of the reference', + required: true, + }, + { + displayName: 'Description', + name: 'description', + type: 'string', + typeOptions: { + rows: 4, + }, + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createReference'], + }, + }, + default: '', + description: 'The description of the reference', + }, + { + displayName: 'Website URL', + name: 'websiteUrl', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['createReference'], + }, + }, + default: '', + description: 'The website URL of the reference', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const trustCenterId = this.getNodeParameter('trustCenterId', itemIndex) as string; + const name = this.getNodeParameter('name', itemIndex) as string; + const description = this.getNodeParameter('description', itemIndex, '') as string; + const websiteUrl = this.getNodeParameter('websiteUrl', itemIndex, '') as string; + + const query = ` + mutation CreateTrustCenterReference($input: CreateTrustCenterReferenceInput!) { + createTrustCenterReference(input: $input) { + trustCenterReferenceEdge { + node { + id + name + description + websiteUrl + rank + createdAt + updatedAt + } + } + } + } + `; + + const input: Record = { trustCenterId, name }; + if (description) input.description = description; + if (websiteUrl) input.websiteUrl = websiteUrl; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteExternalUrl.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteExternalUrl.operation.ts new file mode 100644 index 000000000..755c31d3b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteExternalUrl.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Compliance External URL ID', + name: 'complianceExternalUrlId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['deleteExternalUrl'], + }, + }, + default: '', + description: 'The ID of the compliance external URL to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const complianceExternalUrlId = this.getNodeParameter('complianceExternalUrlId', itemIndex) as string; + + const query = ` + mutation DeleteComplianceExternalURL($input: DeleteComplianceExternalURLInput!) { + deleteComplianceExternalURL(input: $input) { + deletedComplianceExternalURLId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { id: complianceExternalUrlId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteFile.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteFile.operation.ts new file mode 100644 index 000000000..1e382b57c --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteFile.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Trust Center File ID', + name: 'trustCenterFileId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['deleteFile'], + }, + }, + default: '', + description: 'The ID of the trust center file to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const trustCenterFileId = this.getNodeParameter('trustCenterFileId', itemIndex) as string; + + const query = ` + mutation DeleteTrustCenterFile($input: DeleteTrustCenterFileInput!) { + deleteTrustCenterFile(input: $input) { + deletedTrustCenterFileId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { id: trustCenterFileId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteReference.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteReference.operation.ts new file mode 100644 index 000000000..535aa353f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/deleteReference.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Trust Center Reference ID', + name: 'trustCenterReferenceId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['deleteReference'], + }, + }, + default: '', + description: 'The ID of the trust center reference to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const trustCenterReferenceId = this.getNodeParameter('trustCenterReferenceId', itemIndex) as string; + + const query = ` + mutation DeleteTrustCenterReference($input: DeleteTrustCenterReferenceInput!) { + deleteTrustCenterReference(input: $input) { + deletedTrustCenterReferenceId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { id: trustCenterReferenceId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/get.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/get.operation.ts new file mode 100644 index 000000000..55d54bcb1 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/get.operation.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Organization ID', + name: 'organizationId', + type: 'string', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['get'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const organizationId = this.getNodeParameter('organizationId', itemIndex) as string; + + const query = ` + query GetTrustCenter($organizationId: ID!) { + node(id: $organizationId) { + ... on Organization { + trustCenter { + id + active + searchEngineIndexing + logoFileUrl + darkLogoFileUrl + ndaFileName + ndaFileUrl + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { organizationId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllFiles.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllFiles.operation.ts new file mode 100644 index 000000000..7abc6947f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllFiles.operation.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['getAllFiles'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllFiles'], + }, + }, + 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: ['getAllFiles'], + 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 GetTrustCenterFiles($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + trustCenterFiles(first: $first, after: $after) { + edges { + node { + id + name + category + trustCenterVisibility + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const trustCenterFiles = await proboApiRequestAllItems.call( + this, + query, + { organizationId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.trustCenterFiles as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { trustCenterFiles }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllReferences.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllReferences.operation.ts new file mode 100644 index 000000000..263bb6f00 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/getAllReferences.operation.ts @@ -0,0 +1,119 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['getAllReferences'], + }, + }, + default: '', + description: 'The ID of the organization', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['getAllReferences'], + }, + }, + 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: ['getAllReferences'], + 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 GetTrustCenterReferences($organizationId: ID!, $first: Int, $after: CursorKey) { + node(id: $organizationId) { + ... on Organization { + trustCenter { + references(first: $first, after: $after) { + edges { + node { + id + name + description + websiteUrl + rank + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + } + `; + + const references = 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?.references as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { references }, + 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 new file mode 100644 index 000000000..0c7aec412 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/index.ts @@ -0,0 +1,116 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties } from 'n8n-workflow'; +import * as getOp from './get.operation'; +import * as updateOp from './update.operation'; +import * as getAllReferencesOp from './getAllReferences.operation'; +import * as createReferenceOp from './createReference.operation'; +import * as deleteReferenceOp from './deleteReference.operation'; +import * as getAllFilesOp from './getAllFiles.operation'; +import * as deleteFileOp from './deleteFile.operation'; +import * as createExternalUrlOp from './createExternalUrl.operation'; +import * as deleteExternalUrlOp from './deleteExternalUrl.operation'; + +export const description: INodeProperties[] = [ + { + displayName: 'Operation', + name: 'operation', + type: 'options', + noDataExpression: true, + displayOptions: { + show: { + resource: ['trustCenter'], + }, + }, + options: [ + { + name: 'Create External URL', + value: 'createExternalUrl', + description: 'Create a new compliance external URL', + action: 'Create a compliance external URL', + }, + { + name: 'Create Reference', + value: 'createReference', + description: 'Create a new trust center reference', + action: 'Create a trust center reference', + }, + { + name: 'Delete External URL', + value: 'deleteExternalUrl', + description: 'Delete a compliance external URL', + action: 'Delete a compliance external URL', + }, + { + name: 'Delete File', + value: 'deleteFile', + description: 'Delete a trust center file', + action: 'Delete a trust center file', + }, + { + name: 'Delete Reference', + value: 'deleteReference', + description: 'Delete a trust center reference', + action: 'Delete a trust center reference', + }, + { + name: 'Get', + value: 'get', + description: 'Get trust center settings', + action: 'Get trust center settings', + }, + { + name: 'Get Many Files', + value: 'getAllFiles', + description: 'Get many trust center files', + action: 'Get many trust center files', + }, + { + name: 'Get Many References', + value: 'getAllReferences', + description: 'Get many trust center references', + action: 'Get many trust center references', + }, + { + name: 'Update', + value: 'update', + description: 'Update trust center settings', + action: 'Update trust center settings', + }, + ], + default: 'get', + }, + ...getOp.description, + ...updateOp.description, + ...getAllReferencesOp.description, + ...createReferenceOp.description, + ...deleteReferenceOp.description, + ...getAllFilesOp.description, + ...deleteFileOp.description, + ...createExternalUrlOp.description, + ...deleteExternalUrlOp.description, +]; + +export { + getOp as get, + updateOp as update, + getAllReferencesOp as getAllReferences, + createReferenceOp as createReference, + deleteReferenceOp as deleteReference, + getAllFilesOp as getAllFiles, + deleteFileOp as deleteFile, + createExternalUrlOp as createExternalUrl, + deleteExternalUrlOp as deleteExternalUrl, +}; diff --git a/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts b/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts new file mode 100644 index 000000000..b55a36def --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/trustCenter/update.operation.ts @@ -0,0 +1,107 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ['update'], + }, + }, + default: '', + description: 'The ID of the trust center to update', + required: true, + }, + { + displayName: 'Active', + name: 'active', + type: 'boolean', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['update'], + }, + }, + default: false, + description: 'Whether the trust center is active', + }, + { + displayName: 'Search Engine Indexing', + name: 'searchEngineIndexing', + type: 'options', + displayOptions: { + show: { + resource: ['trustCenter'], + operation: ['update'], + }, + }, + options: [ + { + name: '(Unchanged)', + value: '', + }, + { + name: 'Indexable', + value: 'INDEXABLE', + }, + { + name: 'Not Indexable', + value: 'NOT_INDEXABLE', + }, + ], + default: '', + description: 'Whether search engines should index the trust center', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const trustCenterId = this.getNodeParameter('trustCenterId', itemIndex) as string; + const active = this.getNodeParameter('active', itemIndex) as boolean | undefined; + const searchEngineIndexing = this.getNodeParameter('searchEngineIndexing', itemIndex, '') as string; + + const query = ` + mutation UpdateTrustCenter($input: UpdateTrustCenterInput!) { + updateTrustCenter(input: $input) { + trustCenter { + id + active + searchEngineIndexing + createdAt + updatedAt + } + } + } + `; + + const input: Record = { trustCenterId }; + if (active !== undefined) input.active = active; + if (searchEngineIndexing) input.searchEngineIndexing = searchEngineIndexing; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/deleteBusinessAssociateAgreement.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/deleteBusinessAssociateAgreement.operation.ts new file mode 100644 index 000000000..e149ffd6f --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/deleteBusinessAssociateAgreement.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['deleteBusinessAssociateAgreement'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + + const query = ` + mutation DeleteVendorBusinessAssociateAgreement($input: DeleteVendorBusinessAssociateAgreementInput!) { + deleteVendorBusinessAssociateAgreement(input: $input) { + deletedVendorBusinessAssociateAgreementId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { vendorId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/deleteComplianceReport.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/deleteComplianceReport.operation.ts new file mode 100644 index 000000000..0e719ec2b --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/deleteComplianceReport.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor Compliance Report ID', + name: 'vendorComplianceReportId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['deleteComplianceReport'], + }, + }, + default: '', + description: 'The ID of the vendor compliance report to delete', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorComplianceReportId = this.getNodeParameter('vendorComplianceReportId', itemIndex) as string; + + const query = ` + mutation DeleteVendorComplianceReport($input: DeleteVendorComplianceReportInput!) { + deleteVendorComplianceReport(input: $input) { + deletedVendorComplianceReportId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { vendorComplianceReportId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/deleteDataPrivacyAgreement.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/deleteDataPrivacyAgreement.operation.ts new file mode 100644 index 000000000..1e28d3f88 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/deleteDataPrivacyAgreement.operation.ts @@ -0,0 +1,55 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['deleteDataPrivacyAgreement'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + + const query = ` + mutation DeleteVendorDataPrivacyAgreement($input: DeleteVendorDataPrivacyAgreementInput!) { + deleteVendorDataPrivacyAgreement(input: $input) { + deletedVendorDataPrivacyAgreementId + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { input: { vendorId } }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/getAllComplianceReports.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/getAllComplianceReports.operation.ts new file mode 100644 index 000000000..ffbd84be4 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/getAllComplianceReports.operation.ts @@ -0,0 +1,115 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData, IDataObject } from 'n8n-workflow'; +import { proboApiRequestAllItems } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['getAllComplianceReports'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['getAllComplianceReports'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['getAllComplianceReports'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetVendorComplianceReports($vendorId: ID!, $first: Int, $after: CursorKey) { + node(id: $vendorId) { + ... on Vendor { + complianceReports(first: $first, after: $after) { + edges { + node { + id + reportDate + validUntil + reportName + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const vendorComplianceReports = await proboApiRequestAllItems.call( + this, + query, + { vendorId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.complianceReports as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { vendorComplianceReports }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/getBusinessAssociateAgreement.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/getBusinessAssociateAgreement.operation.ts new file mode 100644 index 000000000..f5b0c6399 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/getBusinessAssociateAgreement.operation.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['getBusinessAssociateAgreement'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + + const query = ` + query GetVendorBusinessAssociateAgreement($vendorId: ID!) { + node(id: $vendorId) { + ... on Vendor { + businessAssociateAgreement { + id + validFrom + validUntil + fileName + fileUrl + fileSize + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { vendorId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/getDataPrivacyAgreement.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/getDataPrivacyAgreement.operation.ts new file mode 100644 index 000000000..6894aeeb3 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/getDataPrivacyAgreement.operation.ts @@ -0,0 +1,66 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['getDataPrivacyAgreement'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + + const query = ` + query GetVendorDataPrivacyAgreement($vendorId: ID!) { + node(id: $vendorId) { + ... on Vendor { + dataPrivacyAgreement { + id + validFrom + validUntil + fileName + fileUrl + fileSize + createdAt + updatedAt + } + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { vendorId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/index.ts b/packages/n8n-node/nodes/Probo/actions/vendor/index.ts index dc4e615eb..8c6d741aa 100644 --- a/packages/n8n-node/nodes/Probo/actions/vendor/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/vendor/index.ts @@ -31,6 +31,14 @@ import * as getAllServicesOp from './getAllServices.operation'; import * as createRiskAssessmentOp from './createRiskAssessment.operation'; import * as getRiskAssessmentOp from './getRiskAssessment.operation'; import * as getAllRiskAssessmentsOp from './getAllRiskAssessments.operation'; +import * as getAllComplianceReportsOp from './getAllComplianceReports.operation'; +import * as deleteComplianceReportOp from './deleteComplianceReport.operation'; +import * as getBusinessAssociateAgreementOp from './getBusinessAssociateAgreement.operation'; +import * as deleteBusinessAssociateAgreementOp from './deleteBusinessAssociateAgreement.operation'; +import * as updateBusinessAssociateAgreementOp from './updateBusinessAssociateAgreement.operation'; +import * as getDataPrivacyAgreementOp from './getDataPrivacyAgreement.operation'; +import * as deleteDataPrivacyAgreementOp from './deleteDataPrivacyAgreement.operation'; +import * as updateDataPrivacyAgreementOp from './updateDataPrivacyAgreement.operation'; export const description: INodeProperties[] = [ { @@ -74,12 +82,30 @@ export const description: INodeProperties[] = [ description: 'Delete a vendor', action: 'Delete a vendor', }, + { + name: 'Delete Business Associate Agreement', + value: 'deleteBusinessAssociateAgreement', + description: 'Delete a vendor business associate agreement', + action: 'Delete a vendor business associate agreement', + }, + { + name: 'Delete Compliance Report', + value: 'deleteComplianceReport', + description: 'Delete a vendor compliance report', + action: 'Delete a vendor compliance report', + }, { name: 'Delete Contact', value: 'deleteContact', description: 'Delete a vendor contact', action: 'Delete a vendor contact', }, + { + name: 'Delete Data Privacy Agreement', + value: 'deleteDataPrivacyAgreement', + description: 'Delete a vendor data privacy agreement', + action: 'Delete a vendor data privacy agreement', + }, { name: 'Delete Service', value: 'deleteService', @@ -92,18 +118,36 @@ export const description: INodeProperties[] = [ description: 'Get a vendor', action: 'Get a vendor', }, + { + name: 'Get Business Associate Agreement', + value: 'getBusinessAssociateAgreement', + description: 'Get a vendor business associate agreement', + action: 'Get a vendor business associate agreement', + }, { name: 'Get Contact', value: 'getContact', description: 'Get a vendor contact', action: 'Get a vendor contact', }, + { + name: 'Get Data Privacy Agreement', + value: 'getDataPrivacyAgreement', + description: 'Get a vendor data privacy agreement', + action: 'Get a vendor data privacy agreement', + }, { name: 'Get Many', value: 'getAll', description: 'Get many vendors', action: 'Get many vendors', }, + { + name: 'Get Many Compliance Reports', + value: 'getAllComplianceReports', + description: 'Get many vendor compliance reports', + action: 'Get many vendor compliance reports', + }, { name: 'Get Many Contacts', value: 'getAllContacts', @@ -140,12 +184,24 @@ export const description: INodeProperties[] = [ description: 'Update an existing vendor', action: 'Update a vendor', }, + { + name: 'Update Business Associate Agreement', + value: 'updateBusinessAssociateAgreement', + description: 'Update a vendor business associate agreement validity', + action: 'Update a vendor business associate agreement', + }, { name: 'Update Contact', value: 'updateContact', description: 'Update an existing vendor contact', action: 'Update a vendor contact', }, + { + name: 'Update Data Privacy Agreement', + value: 'updateDataPrivacyAgreement', + description: 'Update a vendor data privacy agreement validity', + action: 'Update a vendor data privacy agreement', + }, { name: 'Update Service', value: 'updateService', @@ -173,6 +229,14 @@ export const description: INodeProperties[] = [ ...createRiskAssessmentOp.description, ...getRiskAssessmentOp.description, ...getAllRiskAssessmentsOp.description, + ...getAllComplianceReportsOp.description, + ...deleteComplianceReportOp.description, + ...getBusinessAssociateAgreementOp.description, + ...deleteBusinessAssociateAgreementOp.description, + ...updateBusinessAssociateAgreementOp.description, + ...getDataPrivacyAgreementOp.description, + ...deleteDataPrivacyAgreementOp.description, + ...updateDataPrivacyAgreementOp.description, ]; export { @@ -194,4 +258,12 @@ export { createRiskAssessmentOp as createRiskAssessment, getRiskAssessmentOp as getRiskAssessment, getAllRiskAssessmentsOp as getAllRiskAssessments, + getAllComplianceReportsOp as getAllComplianceReports, + deleteComplianceReportOp as deleteComplianceReport, + getBusinessAssociateAgreementOp as getBusinessAssociateAgreement, + deleteBusinessAssociateAgreementOp as deleteBusinessAssociateAgreement, + updateBusinessAssociateAgreementOp as updateBusinessAssociateAgreement, + getDataPrivacyAgreementOp as getDataPrivacyAgreement, + deleteDataPrivacyAgreementOp as deleteDataPrivacyAgreement, + updateDataPrivacyAgreementOp as updateDataPrivacyAgreement, }; diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/updateBusinessAssociateAgreement.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/updateBusinessAssociateAgreement.operation.ts new file mode 100644 index 000000000..8ca0ff509 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/updateBusinessAssociateAgreement.operation.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['updateBusinessAssociateAgreement'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, + { + displayName: 'Valid From', + name: 'validFrom', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['updateBusinessAssociateAgreement'], + }, + }, + default: '', + description: 'The start date of the agreement validity (ISO 8601)', + }, + { + displayName: 'Valid Until', + name: 'validUntil', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['updateBusinessAssociateAgreement'], + }, + }, + default: '', + description: 'The end date of the agreement validity (ISO 8601)', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + const validFrom = this.getNodeParameter('validFrom', itemIndex, '') as string; + const validUntil = this.getNodeParameter('validUntil', itemIndex, '') as string; + + const query = ` + mutation UpdateVendorBusinessAssociateAgreement($input: UpdateVendorBusinessAssociateAgreementInput!) { + updateVendorBusinessAssociateAgreement(input: $input) { + vendorBusinessAssociateAgreement { + id + validFrom + validUntil + } + } + } + `; + + const input: Record = { vendorId }; + if (validFrom) input.validFrom = validFrom; + if (validUntil) input.validUntil = validUntil; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/vendor/updateDataPrivacyAgreement.operation.ts b/packages/n8n-node/nodes/Probo/actions/vendor/updateDataPrivacyAgreement.operation.ts new file mode 100644 index 000000000..6233042dd --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/vendor/updateDataPrivacyAgreement.operation.ts @@ -0,0 +1,91 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +import type { INodeProperties, IExecuteFunctions, INodeExecutionData } from 'n8n-workflow'; +import { proboApiRequest } from '../../GenericFunctions'; + +export const description: INodeProperties[] = [ + { + displayName: 'Vendor ID', + name: 'vendorId', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['updateDataPrivacyAgreement'], + }, + }, + default: '', + description: 'The ID of the vendor', + required: true, + }, + { + displayName: 'Valid From', + name: 'validFrom', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['updateDataPrivacyAgreement'], + }, + }, + default: '', + description: 'The start date of the agreement validity (ISO 8601)', + }, + { + displayName: 'Valid Until', + name: 'validUntil', + type: 'string', + displayOptions: { + show: { + resource: ['vendor'], + operation: ['updateDataPrivacyAgreement'], + }, + }, + default: '', + description: 'The end date of the agreement validity (ISO 8601)', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const vendorId = this.getNodeParameter('vendorId', itemIndex) as string; + const validFrom = this.getNodeParameter('validFrom', itemIndex, '') as string; + const validUntil = this.getNodeParameter('validUntil', itemIndex, '') as string; + + const query = ` + mutation UpdateVendorDataPrivacyAgreement($input: UpdateVendorDataPrivacyAgreementInput!) { + updateVendorDataPrivacyAgreement(input: $input) { + vendorDataPrivacyAgreement { + id + validFrom + validUntil + } + } + } + `; + + const input: Record = { vendorId }; + if (validFrom) input.validFrom = validFrom; + if (validUntil) input.validUntil = validUntil; + + const responseData = await proboApiRequest.call(this, query, { input }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/pkg/cli/api/client.go b/pkg/cli/api/client.go index e0d03f711..5c759e799 100644 --- a/pkg/cli/api/client.go +++ b/pkg/cli/api/client.go @@ -19,6 +19,7 @@ import ( "encoding/json" "fmt" "io" + "mime/multipart" "net/http" "net/url" "strings" @@ -193,6 +194,134 @@ func (c *Client) doRequest( return respBody, resp.StatusCode, nil } +// DoUpload sends a GraphQL multipart file upload request following the +// graphql-multipart-request-spec. It maps the given file to the variable +// at the provided path (e.g. "variables.input.file"). +func (c *Client) DoUpload( + query string, + variables map[string]any, + varPath string, + filename string, + file io.Reader, +) (json.RawMessage, error) { + raw, err := c.doUploadRequest(query, variables, varPath, filename, file) + if err != nil { + return nil, err + } + + var resp graphQLResponse + if err := json.Unmarshal(raw, &resp); err != nil { + return nil, fmt.Errorf("cannot parse GraphQL response: %w", err) + } + + if len(resp.Errors) > 0 { + var msg strings.Builder + msg.WriteString(resp.Errors[0].Message) + for _, e := range resp.Errors[1:] { + msg.WriteString("; " + e.Message) + } + return nil, fmt.Errorf("GraphQL error: %s", msg.String()) + } + + return resp.Data, nil +} + +func (c *Client) doUploadRequest( + query string, + variables map[string]any, + varPath string, + filename string, + file io.Reader, +) ([]byte, error) { + var buf bytes.Buffer + writer := multipart.NewWriter(&buf) + + // Part 1: operations + operationsJSON, err := json.Marshal(graphQLRequest{ + Query: query, + Variables: variables, + }) + if err != nil { + return nil, fmt.Errorf("cannot marshal operations: %w", err) + } + + if err := writer.WriteField("operations", string(operationsJSON)); err != nil { + return nil, fmt.Errorf("cannot write operations field: %w", err) + } + + // Part 2: map + mapJSON, err := json.Marshal(map[string][]string{ + "0": {varPath}, + }) + if err != nil { + return nil, fmt.Errorf("cannot marshal map: %w", err) + } + + if err := writer.WriteField("map", string(mapJSON)); err != nil { + return nil, fmt.Errorf("cannot write map field: %w", err) + } + + // Part 3: file + part, err := writer.CreateFormFile("0", filename) + if err != nil { + return nil, fmt.Errorf("cannot create form file: %w", err) + } + + if _, err := io.Copy(part, file); err != nil { + return nil, fmt.Errorf("cannot write file content: %w", err) + } + + if err := writer.Close(); err != nil { + return nil, fmt.Errorf("cannot close multipart writer: %w", err) + } + + host := c.host + if !strings.HasPrefix(host, "http://") && !strings.HasPrefix(host, "https://") { + host = "https://" + host + } + + reqURL := host + c.endpoint + req, err := http.NewRequest(http.MethodPost, reqURL, &buf) + if err != nil { + return nil, fmt.Errorf("cannot create HTTP request: %w", err) + } + + req.Header.Set("Content-Type", writer.FormDataContentType()) + req.Header.Set("Authorization", "Bearer "+c.token) + req.Header.Set("User-Agent", version.UserAgent("prb")) + + resp, err := c.httpClient.Do(req) + if err != nil { + return nil, fmt.Errorf("cannot send HTTP request: %w", err) + } + defer func() { _ = resp.Body.Close() }() + + respBody, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("cannot read HTTP response: %w", err) + } + + if resp.StatusCode == http.StatusUnauthorized && c.refresher != nil { + if refreshErr := c.tryRefreshToken(); refreshErr == nil { + // Retry — but we can't re-read the file, so return the original error. + return nil, fmt.Errorf("authentication failed (HTTP 401): token was refreshed, please retry the command") + } + } + + if resp.StatusCode != http.StatusOK { + switch resp.StatusCode { + case http.StatusUnauthorized: + return nil, fmt.Errorf("authentication failed (HTTP 401): token may be invalid or expired, try 'prb auth login'") + case http.StatusForbidden: + return nil, fmt.Errorf("access denied (HTTP 403): you do not have permission to perform this action") + default: + return nil, fmt.Errorf("HTTP %d: %s", resp.StatusCode, string(respBody)) + } + } + + return respBody, nil +} + func (c *Client) tryRefreshToken() error { r := c.refresher diff --git a/pkg/cmd/asset/asset.go b/pkg/cmd/asset/asset.go index b11522a92..c36bbe4fe 100644 --- a/pkg/cmd/asset/asset.go +++ b/pkg/cmd/asset/asset.go @@ -16,7 +16,12 @@ package asset import ( "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/asset/create" + "go.probo.inc/probo/pkg/cmd/asset/delete" + "go.probo.inc/probo/pkg/cmd/asset/list" "go.probo.inc/probo/pkg/cmd/asset/publish" + "go.probo.inc/probo/pkg/cmd/asset/update" + "go.probo.inc/probo/pkg/cmd/asset/view" "go.probo.inc/probo/pkg/cmd/cmdutil" ) @@ -26,6 +31,11 @@ func NewCmdAsset(f *cmdutil.Factory) *cobra.Command { Short: "Manage assets", } + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) cmd.AddCommand(publish.NewCmdPublish(f)) return cmd diff --git a/pkg/cmd/asset/create/create.go b/pkg/cmd/asset/create/create.go new file mode 100644 index 000000000..013a302eb --- /dev/null +++ b/pkg/cmd/asset/create/create.go @@ -0,0 +1,187 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateAssetInput!) { + createAsset(input: $input) { + assetEdge { + node { + id + name + assetType + amount + } + } + } +} +` + +type createResponse struct { + CreateAsset struct { + AssetEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + AssetType string `json:"assetType"` + Amount int `json:"amount"` + } `json:"node"` + } `json:"assetEdge"` + } `json:"createAsset"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagAssetType string + flagAmount int + flagOwner string + flagDataTypesStored string + flagVendorIDs []string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new asset", + Example: ` # Create an asset interactively + prb asset create + + # Create an asset non-interactively + prb asset create --name "Production Database" --asset-type VIRTUAL --amount 50000`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Asset name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagAssetType == "" { + err := huh.NewSelect[string](). + Title("Asset type"). + Options( + huh.NewOption("Physical", "PHYSICAL"), + huh.NewOption("Virtual", "VIRTUAL"), + ). + Value(&flagAssetType). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagAssetType == "" { + return fmt.Errorf("asset type is required; pass --asset-type or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + "assetType": flagAssetType, + } + + if cmd.Flags().Changed("amount") { + input["amount"] = flagAmount + } + if flagOwner != "" { + input["ownerId"] = flagOwner + } + if flagDataTypesStored != "" { + input["dataTypesStored"] = flagDataTypesStored + } + if len(flagVendorIDs) > 0 { + input["vendorIds"] = flagVendorIDs + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + a := resp.CreateAsset.AssetEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created asset %s (%s)\n", + a.ID, + a.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Asset name (required)") + cmd.Flags().StringVar(&flagAssetType, "asset-type", "", "Asset type: PHYSICAL, VIRTUAL (required)") + cmd.Flags().IntVar(&flagAmount, "amount", 0, "Asset amount") + cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID") + cmd.Flags().StringVar(&flagDataTypesStored, "data-types-stored", "", "Data types stored") + cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)") + + return cmd +} diff --git a/pkg/cmd/asset/delete/delete.go b/pkg/cmd/asset/delete/delete.go new file mode 100644 index 000000000..1826b680c --- /dev/null +++ b/pkg/cmd/asset/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteAssetInput!) { + deleteAsset(input: $input) { + deletedAssetId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete an asset", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete asset: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete asset %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{ + "assetId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted asset %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/asset/list/list.go b/pkg/cmd/asset/list/list.go new file mode 100644 index 000000000..5d9c0eabf --- /dev/null +++ b/pkg/cmd/asset/list/list.go @@ -0,0 +1,193 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: AssetOrder, $filter: AssetFilter) { + node(id: $id) { + __typename + ... on Organization { + assets(first: $first, after: $after, orderBy: $orderBy, filter: $filter) { + totalCount + edges { + node { + id + name + assetType + amount + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type asset struct { + ID string `json:"id"` + Name string `json:"name"` + AssetType string `json:"assetType"` + Amount int `json:"amount"` +} + +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 assets in an organization", + Aliases: []string{"ls"}, + Example: ` # List assets in the default organization + prb asset list + + # List assets sorted by amount + prb asset ls --order-by AMOUNT --json`, + 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{"CREATED_AT", "AMOUNT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + assets, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[asset], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Assets api.Connection[asset] `json:"assets"` + } `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) + } + return &resp.Node.Assets, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, assets) + } + + if len(assets) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No assets found.") + return nil + } + + rows := make([][]string, 0, len(assets)) + for _, a := range assets { + rows = append(rows, []string{ + a.ID, + a.Name, + a.AssetType, + fmt.Sprintf("%d", a.Amount), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "TYPE", "AMOUNT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(assets) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d assets\n", + len(assets), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of assets to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, AMOUNT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/asset/update/update.go b/pkg/cmd/asset/update/update.go new file mode 100644 index 000000000..2edc6141d --- /dev/null +++ b/pkg/cmd/asset/update/update.go @@ -0,0 +1,147 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateAssetInput!) { + updateAsset(input: $input) { + asset { + id + name + assetType + amount + } + } +} +` + +type updateResponse struct { + UpdateAsset struct { + Asset struct { + ID string `json:"id"` + Name string `json:"name"` + AssetType string `json:"assetType"` + Amount int `json:"amount"` + } `json:"asset"` + } `json:"updateAsset"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagAssetType string + flagAmount int + flagOwner string + flagDataTypesStored string + flagVendorIDs []string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update an asset", + 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("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("asset-type") { + input["assetType"] = flagAssetType + } + if cmd.Flags().Changed("amount") { + input["amount"] = flagAmount + } + if cmd.Flags().Changed("owner") { + if flagOwner == "" { + input["ownerId"] = nil + } else { + input["ownerId"] = flagOwner + } + } + if cmd.Flags().Changed("data-types-stored") { + input["dataTypesStored"] = flagDataTypesStored + } + if cmd.Flags().Changed("vendor-ids") { + input["vendorIds"] = flagVendorIDs + } + + 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) + } + + a := resp.UpdateAsset.Asset + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated asset %s (%s)\n", + a.ID, + a.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Asset name") + cmd.Flags().StringVar(&flagAssetType, "asset-type", "", "Asset type: PHYSICAL, VIRTUAL") + cmd.Flags().IntVar(&flagAmount, "amount", 0, "Asset amount") + cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID") + cmd.Flags().StringVar(&flagDataTypesStored, "data-types-stored", "", "Data types stored") + cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)") + + return cmd +} diff --git a/pkg/cmd/asset/view/view.go b/pkg/cmd/asset/view/view.go new file mode 100644 index 000000000..d64f537cd --- /dev/null +++ b/pkg/cmd/asset/view/view.go @@ -0,0 +1,139 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Asset { + id + name + assetType + amount + dataTypesStored + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + AssetType string `json:"assetType"` + Amount int `json:"amount"` + DataTypesStored string `json:"dataTypesStored"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View an asset", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("asset %s not found", args[0]) + } + + if resp.Node.Typename != "Asset" { + return fmt.Errorf("expected Asset node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + a := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(a.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), a.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), a.AssetType) + _, _ = fmt.Fprintf(out, "%s%d\n", label.Render("Amount:"), a.Amount) + + if a.DataTypesStored != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Data Types Stored:"), a.DataTypesStored) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(a.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(a.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/audit/audit.go b/pkg/cmd/audit/audit.go new file mode 100644 index 000000000..4b817a96a --- /dev/null +++ b/pkg/cmd/audit/audit.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package audit + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/audit/create" + "go.probo.inc/probo/pkg/cmd/audit/delete" + "go.probo.inc/probo/pkg/cmd/audit/list" + "go.probo.inc/probo/pkg/cmd/audit/update" + "go.probo.inc/probo/pkg/cmd/audit/view" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +func NewCmdAudit(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "audit ", + Short: "Manage audits", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/audit/create/create.go b/pkg/cmd/audit/create/create.go new file mode 100644 index 000000000..e073ad498 --- /dev/null +++ b/pkg/cmd/audit/create/create.go @@ -0,0 +1,191 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateAuditInput!) { + createAudit(input: $input) { + auditEdge { + node { + id + name + state + validFrom + validUntil + } + } + } +} +` + +type createResponse struct { + CreateAudit struct { + AuditEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + ValidFrom *string `json:"validFrom"` + ValidUntil *string `json:"validUntil"` + } `json:"node"` + } `json:"auditEdge"` + } `json:"createAudit"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagFramework string + flagName string + flagState string + flagValidFrom string + flagValidUntil string + flagTrustCenterVisibility string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new audit", + Example: ` # Create an audit interactively + prb audit create + + # Create an audit non-interactively + prb audit create --name "SOC 2 Type II 2026" --state IN_PROGRESS --valid-from 2026-01-01 --valid-until 2026-12-31`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Audit name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagState == "" { + err := huh.NewSelect[string](). + Title("Audit state"). + Options( + huh.NewOption("Not Started", "NOT_STARTED"), + huh.NewOption("In Progress", "IN_PROGRESS"), + huh.NewOption("Completed", "COMPLETED"), + huh.NewOption("Rejected", "REJECTED"), + huh.NewOption("Outdated", "OUTDATED"), + ). + Value(&flagState). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + } + + if flagFramework != "" { + input["frameworkId"] = flagFramework + } + if flagState != "" { + input["state"] = flagState + } + if flagValidFrom != "" { + input["validFrom"] = flagValidFrom + } + if flagValidUntil != "" { + input["validUntil"] = flagValidUntil + } + if flagTrustCenterVisibility != "" { + input["trustCenterVisibility"] = flagTrustCenterVisibility + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + a := resp.CreateAudit.AuditEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created audit %s (%s)\n", + a.ID, + a.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagFramework, "framework", "", "Framework ID") + cmd.Flags().StringVar(&flagName, "name", "", "Audit name (required)") + cmd.Flags().StringVar(&flagState, "state", "", "Audit state: NOT_STARTED, IN_PROGRESS, COMPLETED, REJECTED, OUTDATED") + cmd.Flags().StringVar(&flagValidFrom, "valid-from", "", "Valid from date (e.g. 2026-01-01)") + cmd.Flags().StringVar(&flagValidUntil, "valid-until", "", "Valid until date (e.g. 2026-12-31)") + cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC") + + return cmd +} diff --git a/pkg/cmd/audit/delete/delete.go b/pkg/cmd/audit/delete/delete.go new file mode 100644 index 000000000..b0492440b --- /dev/null +++ b/pkg/cmd/audit/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteAuditInput!) { + deleteAudit(input: $input) { + deletedAuditId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete an audit", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete audit: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete audit %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{ + "auditId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted audit %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/audit/list/list.go b/pkg/cmd/audit/list/list.go new file mode 100644 index 000000000..ffaf8db7c --- /dev/null +++ b/pkg/cmd/audit/list/list.go @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: AuditOrder) { + node(id: $id) { + __typename + ... on Organization { + audits(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + state + validFrom + validUntil + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type audit struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + ValidFrom *string `json:"validFrom"` + ValidUntil *string `json:"validUntil"` +} + +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 audits in an organization", + Aliases: []string{"ls"}, + Example: ` # List audits in the default organization + prb audit list + + # List audits sorted by state + prb audit ls --order-by STATE --json`, + 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{"CREATED_AT", "VALID_FROM", "VALID_UNTIL", "STATE"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + audits, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[audit], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Audits api.Connection[audit] `json:"audits"` + } `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) + } + return &resp.Node.Audits, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, audits) + } + + if len(audits) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No audits found.") + return nil + } + + rows := make([][]string, 0, len(audits)) + for _, a := range audits { + validFrom := "" + if a.ValidFrom != nil { + validFrom = *a.ValidFrom + } + validUntil := "" + if a.ValidUntil != nil { + validUntil = *a.ValidUntil + } + rows = append(rows, []string{ + a.ID, + a.Name, + a.State, + validFrom, + validUntil, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "STATE", "VALID FROM", "VALID UNTIL").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(audits) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d audits\n", + len(audits), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of audits to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, VALID_FROM, VALID_UNTIL, STATE)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/audit/update/update.go b/pkg/cmd/audit/update/update.go new file mode 100644 index 000000000..e24efc271 --- /dev/null +++ b/pkg/cmd/audit/update/update.go @@ -0,0 +1,140 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateAuditInput!) { + updateAudit(input: $input) { + audit { + id + name + state + validFrom + validUntil + } + } +} +` + +type updateResponse struct { + UpdateAudit struct { + Audit struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + ValidFrom *string `json:"validFrom"` + ValidUntil *string `json:"validUntil"` + } `json:"audit"` + } `json:"updateAudit"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagState string + flagValidFrom string + flagValidUntil string + flagTrustCenterVisibility string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update an audit", + 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("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("state") { + input["state"] = flagState + } + if cmd.Flags().Changed("valid-from") { + input["validFrom"] = flagValidFrom + } + if cmd.Flags().Changed("valid-until") { + input["validUntil"] = flagValidUntil + } + if cmd.Flags().Changed("trust-center-visibility") { + input["trustCenterVisibility"] = flagTrustCenterVisibility + } + + 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) + } + + a := resp.UpdateAudit.Audit + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated audit %s (%s)\n", + a.ID, + a.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Audit name") + cmd.Flags().StringVar(&flagState, "state", "", "Audit state: NOT_STARTED, IN_PROGRESS, COMPLETED, REJECTED, OUTDATED") + cmd.Flags().StringVar(&flagValidFrom, "valid-from", "", "Valid from date (e.g. 2026-01-01)") + cmd.Flags().StringVar(&flagValidUntil, "valid-until", "", "Valid until date (e.g. 2026-12-31)") + cmd.Flags().StringVar(&flagTrustCenterVisibility, "trust-center-visibility", "", "Trust center visibility: NONE, PRIVATE, PUBLIC") + + return cmd +} diff --git a/pkg/cmd/audit/view/view.go b/pkg/cmd/audit/view/view.go new file mode 100644 index 000000000..da1f098c0 --- /dev/null +++ b/pkg/cmd/audit/view/view.go @@ -0,0 +1,142 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Audit { + id + name + state + validFrom + validUntil + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + ValidFrom *string `json:"validFrom"` + ValidUntil *string `json:"validUntil"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View an audit", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("audit %s not found", args[0]) + } + + if resp.Node.Typename != "Audit" { + return fmt.Errorf("expected Audit node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + a := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(a.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), a.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), a.State) + + if a.ValidFrom != nil && *a.ValidFrom != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Valid From:"), *a.ValidFrom) + } + + if a.ValidUntil != nil && *a.ValidUntil != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Valid Until:"), *a.ValidUntil) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(a.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(a.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/datum/create/create.go b/pkg/cmd/datum/create/create.go new file mode 100644 index 000000000..c5f674354 --- /dev/null +++ b/pkg/cmd/datum/create/create.go @@ -0,0 +1,177 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateDatumInput!) { + createDatum(input: $input) { + datumEdge { + node { + id + name + dataClassification + } + } + } +} +` + +type createResponse struct { + CreateDatum struct { + DatumEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + DataClassification string `json:"dataClassification"` + } `json:"node"` + } `json:"datumEdge"` + } `json:"createDatum"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagClassification string + flagOwner string + flagVendorIDs []string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new datum", + Example: ` # Create a datum interactively + prb datum create + + # Create a datum non-interactively + prb datum create --name "Customer PII" --data-classification CONFIDENTIAL`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Datum name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagClassification == "" { + err := huh.NewSelect[string](). + Title("Data classification"). + Options( + huh.NewOption("Public", "PUBLIC"), + huh.NewOption("Internal", "INTERNAL"), + huh.NewOption("Confidential", "CONFIDENTIAL"), + huh.NewOption("Secret", "SECRET"), + ). + Value(&flagClassification). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagClassification == "" { + return fmt.Errorf("data classification is required; pass --data-classification or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + "dataClassification": flagClassification, + } + + if flagOwner != "" { + input["ownerId"] = flagOwner + } + if len(flagVendorIDs) > 0 { + input["vendorIds"] = flagVendorIDs + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + d := resp.CreateDatum.DatumEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created datum %s (%s)\n", + d.ID, + d.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Datum name (required)") + cmd.Flags().StringVar(&flagClassification, "data-classification", "", "Data classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET (required)") + cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID") + cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)") + + return cmd +} diff --git a/pkg/cmd/datum/datum.go b/pkg/cmd/datum/datum.go index 9b6ec8f33..518ae3ec0 100644 --- a/pkg/cmd/datum/datum.go +++ b/pkg/cmd/datum/datum.go @@ -17,7 +17,12 @@ package datum import ( "github.com/spf13/cobra" "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/datum/create" + "go.probo.inc/probo/pkg/cmd/datum/delete" + "go.probo.inc/probo/pkg/cmd/datum/list" "go.probo.inc/probo/pkg/cmd/datum/publish" + "go.probo.inc/probo/pkg/cmd/datum/update" + "go.probo.inc/probo/pkg/cmd/datum/view" ) func NewCmdDatum(f *cmdutil.Factory) *cobra.Command { @@ -26,6 +31,11 @@ func NewCmdDatum(f *cmdutil.Factory) *cobra.Command { Short: "Manage data", } + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) cmd.AddCommand(publish.NewCmdPublish(f)) return cmd diff --git a/pkg/cmd/datum/delete/delete.go b/pkg/cmd/datum/delete/delete.go new file mode 100644 index 000000000..47d194dbf --- /dev/null +++ b/pkg/cmd/datum/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteDatumInput!) { + deleteDatum(input: $input) { + deletedDatumId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a datum", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete datum: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete datum %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{ + "datumId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted datum %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/datum/list/list.go b/pkg/cmd/datum/list/list.go new file mode 100644 index 000000000..852a35b8c --- /dev/null +++ b/pkg/cmd/datum/list/list.go @@ -0,0 +1,190 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DatumOrder) { + node(id: $id) { + __typename + ... on Organization { + data(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + dataClassification + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type datum struct { + ID string `json:"id"` + Name string `json:"name"` + DataClassification string `json:"dataClassification"` +} + +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 data in an organization", + Aliases: []string{"ls"}, + Example: ` # List data in the default organization + prb datum list + + # List data sorted by name + prb datum ls --order-by NAME --json`, + 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{"CREATED_AT", "NAME", "DATA_CLASSIFICATION"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + data, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(raw json.RawMessage) (*api.Connection[datum], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Data api.Connection[datum] `json:"data"` + } `json:"node"` + } + if err := json.Unmarshal(raw, &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) + } + return &resp.Node.Data, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, data) + } + + if len(data) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No data found.") + return nil + } + + rows := make([][]string, 0, len(data)) + for _, d := range data { + rows = append(rows, []string{ + d.ID, + d.Name, + d.DataClassification, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "CLASSIFICATION").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(data) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d data\n", + len(data), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of data to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME, DATA_CLASSIFICATION)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/datum/update/update.go b/pkg/cmd/datum/update/update.go new file mode 100644 index 000000000..5a8ea1d9a --- /dev/null +++ b/pkg/cmd/datum/update/update.go @@ -0,0 +1,135 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateDatumInput!) { + updateDatum(input: $input) { + datum { + id + name + dataClassification + } + } +} +` + +type updateResponse struct { + UpdateDatum struct { + Datum struct { + ID string `json:"id"` + Name string `json:"name"` + DataClassification string `json:"dataClassification"` + } `json:"datum"` + } `json:"updateDatum"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagClassification string + flagOwner string + flagVendorIDs []string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a datum", + 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("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("data-classification") { + input["dataClassification"] = flagClassification + } + if cmd.Flags().Changed("owner") { + if flagOwner == "" { + input["ownerId"] = nil + } else { + input["ownerId"] = flagOwner + } + } + if cmd.Flags().Changed("vendor-ids") { + input["vendorIds"] = flagVendorIDs + } + + 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) + } + + d := resp.UpdateDatum.Datum + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated datum %s (%s)\n", + d.ID, + d.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Datum name") + cmd.Flags().StringVar(&flagClassification, "data-classification", "", "Data classification: PUBLIC, INTERNAL, CONFIDENTIAL, SECRET") + cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID") + cmd.Flags().StringSliceVar(&flagVendorIDs, "vendor-ids", nil, "Vendor IDs (comma-separated)") + + return cmd +} diff --git a/pkg/cmd/datum/view/view.go b/pkg/cmd/datum/view/view.go new file mode 100644 index 000000000..5a764567a --- /dev/null +++ b/pkg/cmd/datum/view/view.go @@ -0,0 +1,130 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Datum { + id + name + dataClassification + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + DataClassification string `json:"dataClassification"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a datum", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("datum %s not found", args[0]) + } + + if resp.Node.Typename != "Datum" { + return fmt.Errorf("expected Datum node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + d := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(d.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), d.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Classification:"), d.DataClassification) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(d.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(d.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/dpia/create/create.go b/pkg/cmd/dpia/create/create.go new file mode 100644 index 000000000..b1f8472b3 --- /dev/null +++ b/pkg/cmd/dpia/create/create.go @@ -0,0 +1,163 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateDataProtectionImpactAssessmentInput!) { + createDataProtectionImpactAssessment(input: $input) { + dataProtectionImpactAssessmentEdge { + node { + id + description + residualRisk + } + } + } +} +` + +type createResponse struct { + CreateDataProtectionImpactAssessment struct { + DataProtectionImpactAssessmentEdge struct { + Node struct { + ID string `json:"id"` + Description string `json:"description"` + ResidualRisk string `json:"residualRisk"` + } `json:"node"` + } `json:"dataProtectionImpactAssessmentEdge"` + } `json:"createDataProtectionImpactAssessment"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagProcessingActivity string + flagDescription string + flagNecessityAndProportionality string + flagPotentialRisk string + flagMitigations string + flagResidualRisk string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new data protection impact assessment", + Example: ` # Create a DPIA + prb dpia create --processing-activity --description "Assessment for HR processing" + + # Create a DPIA with all fields + prb dpia create --processing-activity --description "Assessment" --necessity "Required by law" --potential-risk "Data leak" --mitigations "Encryption" --residual-risk LOW`, + 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 flagProcessingActivity == "" { + return fmt.Errorf("processing activity is required; pass --processing-activity") + } + + if f.IOStreams.IsInteractive() && flagResidualRisk == "" { + err := huh.NewSelect[string](). + Title("Residual risk"). + Options( + huh.NewOption("Low", "LOW"), + huh.NewOption("Medium", "MEDIUM"), + huh.NewOption("High", "HIGH"), + ). + Value(&flagResidualRisk). + Run() + if err != nil { + return err + } + } + + input := map[string]any{ + "processingActivityId": flagProcessingActivity, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + if flagNecessityAndProportionality != "" { + input["necessityAndProportionality"] = flagNecessityAndProportionality + } + if flagPotentialRisk != "" { + input["potentialRisk"] = flagPotentialRisk + } + if flagMitigations != "" { + input["mitigations"] = flagMitigations + } + if flagResidualRisk != "" { + input["residualRisk"] = flagResidualRisk + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateDataProtectionImpactAssessment.DataProtectionImpactAssessmentEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created data protection impact assessment %s\n", + r.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagProcessingActivity, "processing-activity", "", "Processing activity ID (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Description") + cmd.Flags().StringVar(&flagNecessityAndProportionality, "necessity", "", "Necessity and proportionality") + cmd.Flags().StringVar(&flagPotentialRisk, "potential-risk", "", "Potential risk") + cmd.Flags().StringVar(&flagMitigations, "mitigations", "", "Mitigations") + cmd.Flags().StringVar(&flagResidualRisk, "residual-risk", "", "Residual risk: LOW, MEDIUM, HIGH") + + _ = cmd.MarkFlagRequired("processing-activity") + + return cmd +} diff --git a/pkg/cmd/dpia/delete/delete.go b/pkg/cmd/dpia/delete/delete.go new file mode 100644 index 000000000..c909ba6ea --- /dev/null +++ b/pkg/cmd/dpia/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteDataProtectionImpactAssessmentInput!) { + deleteDataProtectionImpactAssessment(input: $input) { + deletedDataProtectionImpactAssessmentId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a data protection impact assessment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete data protection impact assessment: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete data protection impact assessment %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{ + "dataProtectionImpactAssessmentId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted data protection impact assessment %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/dpia/dpia.go b/pkg/cmd/dpia/dpia.go new file mode 100644 index 000000000..f22e57073 --- /dev/null +++ b/pkg/cmd/dpia/dpia.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package dpia + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/dpia/create" + "go.probo.inc/probo/pkg/cmd/dpia/delete" + "go.probo.inc/probo/pkg/cmd/dpia/list" + "go.probo.inc/probo/pkg/cmd/dpia/update" + "go.probo.inc/probo/pkg/cmd/dpia/view" +) + +func NewCmdDPIA(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "dpia ", + Short: "Manage data protection impact assessments", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/dpia/list/list.go b/pkg/cmd/dpia/list/list.go new file mode 100644 index 000000000..4d3d0f549 --- /dev/null +++ b/pkg/cmd/dpia/list/list.go @@ -0,0 +1,195 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DataProtectionImpactAssessmentOrder) { + node(id: $id) { + __typename + ... on Organization { + dataProtectionImpactAssessments(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + description + residualRisk + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type dataProtectionImpactAssessment struct { + ID string `json:"id"` + Description string `json:"description"` + ResidualRisk string `json:"residualRisk"` + CreatedAt string `json:"createdAt"` +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max-3] + "..." +} + +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 data protection impact assessments in an organization", + Aliases: []string{"ls"}, + 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{"CREATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + dpias, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[dataProtectionImpactAssessment], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + DataProtectionImpactAssessments api.Connection[dataProtectionImpactAssessment] `json:"dataProtectionImpactAssessments"` + } `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) + } + return &resp.Node.DataProtectionImpactAssessments, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, dpias) + } + + if len(dpias) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No data protection impact assessments found.") + return nil + } + + rows := make([][]string, 0, len(dpias)) + for _, d := range dpias { + rows = append(rows, []string{ + d.ID, + truncate(d.Description, 50), + d.ResidualRisk, + cmdutil.FormatTime(d.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "DESCRIPTION", "RESIDUAL RISK", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(dpias) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d data protection impact assessments\n", + len(dpias), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of data protection impact assessments to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/dpia/update/update.go b/pkg/cmd/dpia/update/update.go new file mode 100644 index 000000000..596544261 --- /dev/null +++ b/pkg/cmd/dpia/update/update.go @@ -0,0 +1,135 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateDataProtectionImpactAssessmentInput!) { + updateDataProtectionImpactAssessment(input: $input) { + dataProtectionImpactAssessment { + id + description + residualRisk + } + } +} +` + +type updateResponse struct { + UpdateDataProtectionImpactAssessment struct { + DataProtectionImpactAssessment struct { + ID string `json:"id"` + Description string `json:"description"` + ResidualRisk string `json:"residualRisk"` + } `json:"dataProtectionImpactAssessment"` + } `json:"updateDataProtectionImpactAssessment"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagDescription string + flagNecessityAndProportionality string + flagPotentialRisk string + flagMitigations string + flagResidualRisk string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a data protection impact assessment", + 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("description") { + input["description"] = flagDescription + } + if cmd.Flags().Changed("necessity") { + input["necessityAndProportionality"] = flagNecessityAndProportionality + } + if cmd.Flags().Changed("potential-risk") { + input["potentialRisk"] = flagPotentialRisk + } + if cmd.Flags().Changed("mitigations") { + input["mitigations"] = flagMitigations + } + if cmd.Flags().Changed("residual-risk") { + input["residualRisk"] = flagResidualRisk + } + + 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) + } + + r := resp.UpdateDataProtectionImpactAssessment.DataProtectionImpactAssessment + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated data protection impact assessment %s\n", + r.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagDescription, "description", "", "Description") + cmd.Flags().StringVar(&flagNecessityAndProportionality, "necessity", "", "Necessity and proportionality") + cmd.Flags().StringVar(&flagPotentialRisk, "potential-risk", "", "Potential risk") + cmd.Flags().StringVar(&flagMitigations, "mitigations", "", "Mitigations") + cmd.Flags().StringVar(&flagResidualRisk, "residual-risk", "", "Residual risk: LOW, MEDIUM, HIGH") + + return cmd +} diff --git a/pkg/cmd/dpia/view/view.go b/pkg/cmd/dpia/view/view.go new file mode 100644 index 000000000..cbbbb73d2 --- /dev/null +++ b/pkg/cmd/dpia/view/view.go @@ -0,0 +1,155 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on DataProtectionImpactAssessment { + id + description + necessityAndProportionality + potentialRisk + mitigations + residualRisk + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Description string `json:"description"` + NecessityAndProportionality string `json:"necessityAndProportionality"` + PotentialRisk string `json:"potentialRisk"` + Mitigations string `json:"mitigations"` + ResidualRisk string `json:"residualRisk"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a data protection impact assessment", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("data protection impact assessment %s not found", args[0]) + } + + if resp.Node.Typename != "DataProtectionImpactAssessment" { + return fmt.Errorf("expected DataProtectionImpactAssessment node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(30) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Data Protection Impact Assessment")) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + + if r.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), r.Description) + } + + if r.NecessityAndProportionality != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Necessity & Proportionality:"), r.NecessityAndProportionality) + } + + if r.PotentialRisk != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Potential Risk:"), r.PotentialRisk) + } + + if r.Mitigations != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Mitigations:"), r.Mitigations) + } + + if r.ResidualRisk != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Residual Risk:"), r.ResidualRisk) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/evidence/evidence.go b/pkg/cmd/evidence/evidence.go index e696d874d..4e7f534c5 100644 --- a/pkg/cmd/evidence/evidence.go +++ b/pkg/cmd/evidence/evidence.go @@ -19,6 +19,7 @@ import ( "go.probo.inc/probo/pkg/cmd/cmdutil" "go.probo.inc/probo/pkg/cmd/evidence/delete" "go.probo.inc/probo/pkg/cmd/evidence/list" + "go.probo.inc/probo/pkg/cmd/evidence/upload" "go.probo.inc/probo/pkg/cmd/evidence/view" ) @@ -31,6 +32,7 @@ func NewCmdEvidence(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(list.NewCmdList(f)) cmd.AddCommand(view.NewCmdView(f)) cmd.AddCommand(delete.NewCmdDelete(f)) + cmd.AddCommand(upload.NewCmdUpload(f)) return cmd } diff --git a/pkg/cmd/evidence/upload/upload.go b/pkg/cmd/evidence/upload/upload.go new file mode 100644 index 000000000..f48295c8a --- /dev/null +++ b/pkg/cmd/evidence/upload/upload.go @@ -0,0 +1,119 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package upload + +import ( + "encoding/json" + "fmt" + "os" + "path/filepath" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const uploadMutation = ` +mutation($input: UploadMeasureEvidenceInput!) { + uploadMeasureEvidence(input: $input) { + evidence { + id + state + type + } + } +} +` + +type uploadResponse struct { + UploadMeasureEvidence struct { + Evidence struct { + ID string `json:"id"` + State string `json:"state"` + Type string `json:"type"` + } `json:"evidence"` + } `json:"uploadMeasureEvidence"` +} + +func NewCmdUpload(f *cmdutil.Factory) *cobra.Command { + var flagMeasure string + + cmd := &cobra.Command{ + Use: "upload ", + Short: "Upload evidence for a measure", + Example: ` # Upload a file as evidence for a measure + prb evidence upload ./report.pdf --measure `, + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + filePath := args[0] + + file, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("cannot open file: %w", err) + } + defer func() { _ = file.Close() }() + + 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{ + "input": map[string]any{ + "measureId": flagMeasure, + "file": nil, + }, + } + + data, err := client.DoUpload( + uploadMutation, + variables, + "variables.input.file", + filepath.Base(filePath), + file, + ) + if err != nil { + return err + } + + var resp uploadResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + _, _ = fmt.Fprintf(f.IOStreams.Out, "Uploaded evidence %s\n", resp.UploadMeasureEvidence.Evidence.ID) + + return nil + }, + } + + cmd.Flags().StringVar(&flagMeasure, "measure", "", "Measure ID (required)") + _ = cmd.MarkFlagRequired("measure") + + return cmd +} diff --git a/pkg/cmd/measure/create/create.go b/pkg/cmd/measure/create/create.go new file mode 100644 index 000000000..20ee94526 --- /dev/null +++ b/pkg/cmd/measure/create/create.go @@ -0,0 +1,168 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateMeasureInput!) { + createMeasure(input: $input) { + measureEdge { + node { + id + name + category + state + } + } + } +} +` + +type createResponse struct { + CreateMeasure struct { + MeasureEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + State string `json:"state"` + } `json:"node"` + } `json:"measureEdge"` + } `json:"createMeasure"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagCategory string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new measure", + Example: ` # Create a measure interactively + prb measure create + + # Create a measure non-interactively + prb measure create --name "Enable encryption at rest" --category "Security"`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Measure name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagCategory == "" { + err := huh.NewInput(). + Title("Measure category"). + Value(&flagCategory). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagCategory == "" { + return fmt.Errorf("category is required; pass --category or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + "category": flagCategory, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + m := resp.CreateMeasure.MeasureEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created measure %s (%s)\n", + m.ID, + m.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Measure name (required)") + cmd.Flags().StringVar(&flagCategory, "category", "", "Measure category (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Measure description") + + return cmd +} diff --git a/pkg/cmd/measure/delete/delete.go b/pkg/cmd/measure/delete/delete.go new file mode 100644 index 000000000..5a63ae6b6 --- /dev/null +++ b/pkg/cmd/measure/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteMeasureInput!) { + deleteMeasure(input: $input) { + deletedMeasureId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a measure", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete measure: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete measure %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{ + "measureId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted measure %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/measure/list/list.go b/pkg/cmd/measure/list/list.go new file mode 100644 index 000000000..1dd17525f --- /dev/null +++ b/pkg/cmd/measure/list/list.go @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: MeasureOrder, $filter: MeasureFilter) { + node(id: $id) { + __typename + ... on Organization { + measures(first: $first, after: $after, orderBy: $orderBy, filter: $filter) { + totalCount + edges { + node { + id + name + category + state + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type measure struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + State string `json:"state"` +} + +func NewCmdList(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagLimit int + flagOrderBy string + flagOrderDir string + flagFilter string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list", + Short: "List measures in an organization", + Aliases: []string{"ls"}, + Example: ` # List measures in the default organization + prb measure list + + # Filter measures by name + prb measure list --filter "encryption" + + # List measures sorted by name + prb measure ls --order-by NAME --json`, + 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{"CREATED_AT", "NAME"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + if flagFilter != "" { + variables["filter"] = map[string]any{ + "query": flagFilter, + } + } + + measures, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[measure], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Measures api.Connection[measure] `json:"measures"` + } `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) + } + return &resp.Node.Measures, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, measures) + } + + if len(measures) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No measures found.") + return nil + } + + rows := make([][]string, 0, len(measures)) + for _, m := range measures { + rows = append(rows, []string{ + m.ID, + m.Name, + m.Category, + m.State, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "CATEGORY", "STATE").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(measures) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d measures\n", + len(measures), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of measures to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + cmd.Flags().StringVarP(&flagFilter, "filter", "q", "", "Filter measures by search query") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/measure/measure.go b/pkg/cmd/measure/measure.go new file mode 100644 index 000000000..0da652204 --- /dev/null +++ b/pkg/cmd/measure/measure.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package measure + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/measure/create" + "go.probo.inc/probo/pkg/cmd/measure/delete" + "go.probo.inc/probo/pkg/cmd/measure/list" + "go.probo.inc/probo/pkg/cmd/measure/update" + "go.probo.inc/probo/pkg/cmd/measure/view" +) + +func NewCmdMeasure(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "measure ", + Short: "Manage measures", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/measure/update/update.go b/pkg/cmd/measure/update/update.go new file mode 100644 index 000000000..6256c99ae --- /dev/null +++ b/pkg/cmd/measure/update/update.go @@ -0,0 +1,136 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateMeasureInput!) { + updateMeasure(input: $input) { + measure { + id + name + category + state + } + } +} +` + +type updateResponse struct { + UpdateMeasure struct { + Measure struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + State string `json:"state"` + } `json:"measure"` + } `json:"updateMeasure"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagDescription string + flagCategory string + flagState string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a measure", + 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("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + if cmd.Flags().Changed("category") { + input["category"] = flagCategory + } + if cmd.Flags().Changed("state") { + if err := cmdutil.ValidateEnum("state", flagState, []string{"NOT_STARTED", "IN_PROGRESS", "NOT_APPLICABLE", "IMPLEMENTED"}); err != nil { + return err + } + input["state"] = flagState + } + + 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) + } + + m := resp.UpdateMeasure.Measure + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated measure %s (%s)\n", + m.ID, + m.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Measure name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Measure description") + cmd.Flags().StringVar(&flagCategory, "category", "", "Measure category") + cmd.Flags().StringVar(&flagState, "state", "", "Measure state: NOT_STARTED, IN_PROGRESS, NOT_APPLICABLE, IMPLEMENTED") + + return cmd +} diff --git a/pkg/cmd/measure/view/view.go b/pkg/cmd/measure/view/view.go new file mode 100644 index 000000000..dc97449b2 --- /dev/null +++ b/pkg/cmd/measure/view/view.go @@ -0,0 +1,139 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Measure { + id + name + description + category + state + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Category string `json:"category"` + State string `json:"state"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a measure", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("measure %s not found", args[0]) + } + + if resp.Node.Typename != "Measure" { + return fmt.Errorf("expected Measure node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + m := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(m.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), m.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Category:"), m.Category) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), m.State) + + if m.Description != nil && *m.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *m.Description) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(m.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(m.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/obligation/create/create.go b/pkg/cmd/obligation/create/create.go new file mode 100644 index 000000000..1d546b8db --- /dev/null +++ b/pkg/cmd/obligation/create/create.go @@ -0,0 +1,235 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateObligationInput!) { + createObligation(input: $input) { + obligationEdge { + node { + id + area + source + status + type + } + } + } +} +` + +type createResponse struct { + CreateObligation struct { + ObligationEdge struct { + Node struct { + ID string `json:"id"` + Area string `json:"area"` + Source string `json:"source"` + Status string `json:"status"` + Type string `json:"type"` + } `json:"node"` + } `json:"obligationEdge"` + } `json:"createObligation"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagArea string + flagSource string + flagStatus string + flagType string + flagRequirement string + flagActionsToBeImplemented string + flagRegulator string + flagOwner string + flagLastReviewDate string + flagDueDate string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new obligation", + Example: ` # Create an obligation interactively + prb obligation create + + # Create an obligation non-interactively + prb obligation create --area "Data Protection" --source "GDPR Article 5" --status NON_COMPLIANT --type LEGAL`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagArea == "" { + err := huh.NewInput(). + Title("Obligation area"). + Value(&flagArea). + Run() + if err != nil { + return err + } + } + + if flagSource == "" { + err := huh.NewInput(). + Title("Obligation source"). + Value(&flagSource). + Run() + if err != nil { + return err + } + } + + if flagStatus == "" { + err := huh.NewSelect[string](). + Title("Obligation status"). + Options( + huh.NewOption("Non-Compliant", "NON_COMPLIANT"), + huh.NewOption("Partially Compliant", "PARTIALLY_COMPLIANT"), + huh.NewOption("Compliant", "COMPLIANT"), + ). + Value(&flagStatus). + Run() + if err != nil { + return err + } + } + + if flagType == "" { + err := huh.NewSelect[string](). + Title("Obligation type"). + Options( + huh.NewOption("Legal", "LEGAL"), + huh.NewOption("Contractual", "CONTRACTUAL"), + ). + Value(&flagType). + Run() + if err != nil { + return err + } + } + } + + if flagArea == "" { + return fmt.Errorf("area is required; pass --area or run interactively") + } + if flagSource == "" { + return fmt.Errorf("source is required; pass --source or run interactively") + } + if flagStatus == "" { + return fmt.Errorf("status is required; pass --status or run interactively") + } + if flagType == "" { + return fmt.Errorf("type is required; pass --type or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "area": flagArea, + "source": flagSource, + "status": flagStatus, + "type": flagType, + } + + if flagRequirement != "" { + input["requirement"] = flagRequirement + } + if flagActionsToBeImplemented != "" { + input["actionsToBeImplemented"] = flagActionsToBeImplemented + } + if flagRegulator != "" { + input["regulator"] = flagRegulator + } + if flagOwner != "" { + input["ownerId"] = flagOwner + } + if flagLastReviewDate != "" { + input["lastReviewDate"] = flagLastReviewDate + } + if flagDueDate != "" { + input["dueDate"] = flagDueDate + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + o := resp.CreateObligation.ObligationEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created obligation %s\n", + o.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagArea, "area", "", "Obligation area (required)") + cmd.Flags().StringVar(&flagSource, "source", "", "Obligation source (required)") + cmd.Flags().StringVar(&flagStatus, "status", "", "Obligation status: NON_COMPLIANT, PARTIALLY_COMPLIANT, COMPLIANT (required)") + cmd.Flags().StringVar(&flagType, "type", "", "Obligation type: LEGAL, CONTRACTUAL (required)") + cmd.Flags().StringVar(&flagRequirement, "requirement", "", "Obligation requirement") + cmd.Flags().StringVar(&flagActionsToBeImplemented, "actions-to-be-implemented", "", "Actions to be implemented") + cmd.Flags().StringVar(&flagRegulator, "regulator", "", "Regulator") + cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID") + cmd.Flags().StringVar(&flagLastReviewDate, "last-review-date", "", "Last review date (ISO 8601)") + cmd.Flags().StringVar(&flagDueDate, "due-date", "", "Due date (ISO 8601)") + + return cmd +} diff --git a/pkg/cmd/obligation/delete/delete.go b/pkg/cmd/obligation/delete/delete.go new file mode 100644 index 000000000..d16607a39 --- /dev/null +++ b/pkg/cmd/obligation/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteObligationInput!) { + deleteObligation(input: $input) { + deletedObligationId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete an obligation", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete obligation: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete obligation %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{ + "obligationId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted obligation %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/obligation/list/list.go b/pkg/cmd/obligation/list/list.go new file mode 100644 index 000000000..fc0b73324 --- /dev/null +++ b/pkg/cmd/obligation/list/list.go @@ -0,0 +1,203 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ObligationOrder, $filter: ObligationFilter) { + node(id: $id) { + __typename + ... on Organization { + obligations(first: $first, after: $after, orderBy: $orderBy, filter: $filter) { + totalCount + edges { + node { + id + area + source + status + type + dueDate + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type obligation struct { + ID string `json:"id"` + Area string `json:"area"` + Source string `json:"source"` + Status string `json:"status"` + Type string `json:"type"` + DueDate *string `json:"dueDate"` +} + +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 obligations in an organization", + Aliases: []string{"ls"}, + Example: ` # List obligations in the default organization + prb obligation list + + # List obligations sorted by due date + prb obligation ls --order-by DUE_DATE --json`, + 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{"CREATED_AT", "LAST_REVIEW_DATE", "DUE_DATE", "STATUS"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + obligations, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[obligation], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Obligations api.Connection[obligation] `json:"obligations"` + } `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) + } + return &resp.Node.Obligations, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, obligations) + } + + if len(obligations) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No obligations found.") + return nil + } + + rows := make([][]string, 0, len(obligations)) + for _, o := range obligations { + dueDate := "" + if o.DueDate != nil { + dueDate = *o.DueDate + } + rows = append(rows, []string{ + o.ID, + o.Area, + o.Source, + o.Status, + o.Type, + dueDate, + }) + } + + t := cmdutil.NewTable("ID", "AREA", "SOURCE", "STATUS", "TYPE", "DUE DATE").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(obligations) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d obligations\n", + len(obligations), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of obligations to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, LAST_REVIEW_DATE, DUE_DATE, STATUS)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/obligation/obligation.go b/pkg/cmd/obligation/obligation.go new file mode 100644 index 000000000..b8b0c36f9 --- /dev/null +++ b/pkg/cmd/obligation/obligation.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package obligation + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/obligation/create" + "go.probo.inc/probo/pkg/cmd/obligation/delete" + "go.probo.inc/probo/pkg/cmd/obligation/list" + "go.probo.inc/probo/pkg/cmd/obligation/update" + "go.probo.inc/probo/pkg/cmd/obligation/view" +) + +func NewCmdObligation(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "obligation ", + Short: "Manage obligations", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/obligation/update/update.go b/pkg/cmd/obligation/update/update.go new file mode 100644 index 000000000..e984257a1 --- /dev/null +++ b/pkg/cmd/obligation/update/update.go @@ -0,0 +1,168 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateObligationInput!) { + updateObligation(input: $input) { + obligation { + id + area + source + status + type + } + } +} +` + +type updateResponse struct { + UpdateObligation struct { + Obligation struct { + ID string `json:"id"` + Area string `json:"area"` + Source string `json:"source"` + Status string `json:"status"` + Type string `json:"type"` + } `json:"obligation"` + } `json:"updateObligation"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagArea string + flagSource string + flagStatus string + flagType string + flagRequirement string + flagActionsToBeImplemented string + flagRegulator string + flagOwner string + flagLastReviewDate string + flagDueDate string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update an obligation", + 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("area") { + input["area"] = flagArea + } + if cmd.Flags().Changed("source") { + input["source"] = flagSource + } + if cmd.Flags().Changed("status") { + input["status"] = flagStatus + } + if cmd.Flags().Changed("type") { + input["type"] = flagType + } + if cmd.Flags().Changed("requirement") { + input["requirement"] = flagRequirement + } + if cmd.Flags().Changed("actions-to-be-implemented") { + input["actionsToBeImplemented"] = flagActionsToBeImplemented + } + if cmd.Flags().Changed("regulator") { + input["regulator"] = flagRegulator + } + if cmd.Flags().Changed("owner") { + if flagOwner == "" { + input["ownerId"] = nil + } else { + input["ownerId"] = flagOwner + } + } + if cmd.Flags().Changed("last-review-date") { + input["lastReviewDate"] = flagLastReviewDate + } + if cmd.Flags().Changed("due-date") { + input["dueDate"] = flagDueDate + } + + 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) + } + + o := resp.UpdateObligation.Obligation + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated obligation %s\n", + o.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagArea, "area", "", "Obligation area") + cmd.Flags().StringVar(&flagSource, "source", "", "Obligation source") + cmd.Flags().StringVar(&flagStatus, "status", "", "Obligation status: NON_COMPLIANT, PARTIALLY_COMPLIANT, COMPLIANT") + cmd.Flags().StringVar(&flagType, "type", "", "Obligation type: LEGAL, CONTRACTUAL") + cmd.Flags().StringVar(&flagRequirement, "requirement", "", "Obligation requirement") + cmd.Flags().StringVar(&flagActionsToBeImplemented, "actions-to-be-implemented", "", "Actions to be implemented") + cmd.Flags().StringVar(&flagRegulator, "regulator", "", "Regulator") + cmd.Flags().StringVar(&flagOwner, "owner", "", "Owner profile ID") + cmd.Flags().StringVar(&flagLastReviewDate, "last-review-date", "", "Last review date (ISO 8601)") + cmd.Flags().StringVar(&flagDueDate, "due-date", "", "Due date (ISO 8601)") + + return cmd +} diff --git a/pkg/cmd/obligation/view/view.go b/pkg/cmd/obligation/view/view.go new file mode 100644 index 000000000..4a63ee5e5 --- /dev/null +++ b/pkg/cmd/obligation/view/view.go @@ -0,0 +1,168 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Obligation { + id + area + source + requirement + actionsToBeImplemented + regulator + lastReviewDate + dueDate + status + type + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Area string `json:"area"` + Source string `json:"source"` + Requirement *string `json:"requirement"` + ActionsToBeImplemented *string `json:"actionsToBeImplemented"` + Regulator *string `json:"regulator"` + LastReviewDate *string `json:"lastReviewDate"` + DueDate *string `json:"dueDate"` + Status string `json:"status"` + Type string `json:"type"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View an obligation", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("obligation %s not found", args[0]) + } + + if resp.Node.Typename != "Obligation" { + return fmt.Errorf("expected Obligation node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + o := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(o.Area)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), o.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Source:"), o.Source) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Status:"), o.Status) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), o.Type) + + if o.Requirement != nil && *o.Requirement != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Requirement:"), *o.Requirement) + } + + if o.ActionsToBeImplemented != nil && *o.ActionsToBeImplemented != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Actions:"), *o.ActionsToBeImplemented) + } + + if o.Regulator != nil && *o.Regulator != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Regulator:"), *o.Regulator) + } + + _, _ = fmt.Fprintln(out) + + if o.LastReviewDate != nil && *o.LastReviewDate != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Last Review Date:"), *o.LastReviewDate) + } + + if o.DueDate != nil && *o.DueDate != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Due Date:"), *o.DueDate) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(o.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(o.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/processing-activity/create/create.go b/pkg/cmd/processing-activity/create/create.go new file mode 100644 index 000000000..478b44702 --- /dev/null +++ b/pkg/cmd/processing-activity/create/create.go @@ -0,0 +1,204 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateProcessingActivityInput!) { + createProcessingActivity(input: $input) { + processingActivityEdge { + node { + id + name + role + lawfulBasis + } + } + } +} +` + +type createResponse struct { + CreateProcessingActivity struct { + ProcessingActivityEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + LawfulBasis string `json:"lawfulBasis"` + } `json:"node"` + } `json:"processingActivityEdge"` + } `json:"createProcessingActivity"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagPurpose string + flagRole string + flagLawfulBasis string + flagDataSubjectCategory string + flagPersonalDataCategory string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new processing activity", + Example: ` # Create a processing activity interactively + prb processing-activity create + + # Create a processing activity non-interactively + prb pa create --name "Customer onboarding" --role CONTROLLER --lawful-basis CONSENT`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Processing activity name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagRole == "" { + err := huh.NewSelect[string](). + Title("Role"). + Options( + huh.NewOption("Controller", "CONTROLLER"), + huh.NewOption("Processor", "PROCESSOR"), + ). + Value(&flagRole). + Run() + if err != nil { + return err + } + } + + if flagLawfulBasis == "" { + err := huh.NewSelect[string](). + Title("Lawful basis"). + Options( + huh.NewOption("Legitimate interest", "LEGITIMATE_INTEREST"), + huh.NewOption("Consent", "CONSENT"), + huh.NewOption("Contractual necessity", "CONTRACTUAL_NECESSITY"), + huh.NewOption("Legal obligation", "LEGAL_OBLIGATION"), + huh.NewOption("Vital interests", "VITAL_INTERESTS"), + huh.NewOption("Public task", "PUBLIC_TASK"), + ). + Value(&flagLawfulBasis). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + } + + if flagPurpose != "" { + input["purpose"] = flagPurpose + } + if flagRole != "" { + input["role"] = flagRole + } + if flagLawfulBasis != "" { + input["lawfulBasis"] = flagLawfulBasis + } + if flagDataSubjectCategory != "" { + input["dataSubjectCategory"] = flagDataSubjectCategory + } + if flagPersonalDataCategory != "" { + input["personalDataCategory"] = flagPersonalDataCategory + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + a := resp.CreateProcessingActivity.ProcessingActivityEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created processing activity %s (%s)\n", + a.ID, + a.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Processing activity name (required)") + cmd.Flags().StringVar(&flagPurpose, "purpose", "", "Purpose of processing") + cmd.Flags().StringVar(&flagRole, "role", "", "Role: CONTROLLER, PROCESSOR") + cmd.Flags().StringVar(&flagLawfulBasis, "lawful-basis", "", "Lawful basis: LEGITIMATE_INTEREST, CONSENT, CONTRACTUAL_NECESSITY, LEGAL_OBLIGATION, VITAL_INTERESTS, PUBLIC_TASK") + cmd.Flags().StringVar(&flagDataSubjectCategory, "data-subject-category", "", "Data subject category") + cmd.Flags().StringVar(&flagPersonalDataCategory, "personal-data-category", "", "Personal data category") + + return cmd +} diff --git a/pkg/cmd/processing-activity/delete/delete.go b/pkg/cmd/processing-activity/delete/delete.go new file mode 100644 index 000000000..cf320f94d --- /dev/null +++ b/pkg/cmd/processing-activity/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteProcessingActivityInput!) { + deleteProcessingActivity(input: $input) { + deletedProcessingActivityId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a processing activity", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete processing activity: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete processing activity %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{ + "processingActivityId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted processing activity %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/processing-activity/list/list.go b/pkg/cmd/processing-activity/list/list.go new file mode 100644 index 000000000..06587b60f --- /dev/null +++ b/pkg/cmd/processing-activity/list/list.go @@ -0,0 +1,193 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: ProcessingActivityOrder) { + node(id: $id) { + __typename + ... on Organization { + processingActivities(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + role + lawfulBasis + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type processingActivity struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + LawfulBasis string `json:"lawfulBasis"` +} + +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 processing activities in an organization", + Aliases: []string{"ls"}, + Example: ` # List processing activities in the default organization + prb processing-activity list + + # List processing activities sorted by name + prb pa ls --order-by NAME --json`, + 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{"CREATED_AT", "NAME"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + activities, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[processingActivity], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + ProcessingActivities api.Connection[processingActivity] `json:"processingActivities"` + } `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) + } + return &resp.Node.ProcessingActivities, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, activities) + } + + if len(activities) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No processing activities found.") + return nil + } + + rows := make([][]string, 0, len(activities)) + for _, a := range activities { + rows = append(rows, []string{ + a.ID, + a.Name, + a.Role, + a.LawfulBasis, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "ROLE", "LAWFUL BASIS").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(activities) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d processing activities\n", + len(activities), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of processing activities to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/processing-activity/processing_activity.go b/pkg/cmd/processing-activity/processing_activity.go new file mode 100644 index 000000000..29d6d95ce --- /dev/null +++ b/pkg/cmd/processing-activity/processing_activity.go @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package processingactivity + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/processing-activity/create" + "go.probo.inc/probo/pkg/cmd/processing-activity/delete" + "go.probo.inc/probo/pkg/cmd/processing-activity/list" + "go.probo.inc/probo/pkg/cmd/processing-activity/update" + "go.probo.inc/probo/pkg/cmd/processing-activity/view" +) + +func NewCmdProcessingActivity(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "processing-activity ", + Short: "Manage processing activities", + Aliases: []string{"pa"}, + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/processing-activity/update/update.go b/pkg/cmd/processing-activity/update/update.go new file mode 100644 index 000000000..6165e3f4a --- /dev/null +++ b/pkg/cmd/processing-activity/update/update.go @@ -0,0 +1,133 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateProcessingActivityInput!) { + updateProcessingActivity(input: $input) { + processingActivity { + id + name + role + lawfulBasis + } + } +} +` + +type updateResponse struct { + UpdateProcessingActivity struct { + ProcessingActivity struct { + ID string `json:"id"` + Name string `json:"name"` + Role string `json:"role"` + LawfulBasis string `json:"lawfulBasis"` + } `json:"processingActivity"` + } `json:"updateProcessingActivity"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagPurpose string + flagRole string + flagLawfulBasis string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a processing activity", + 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("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("purpose") { + input["purpose"] = flagPurpose + } + if cmd.Flags().Changed("role") { + input["role"] = flagRole + } + if cmd.Flags().Changed("lawful-basis") { + input["lawfulBasis"] = flagLawfulBasis + } + + 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) + } + + a := resp.UpdateProcessingActivity.ProcessingActivity + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated processing activity %s (%s)\n", + a.ID, + a.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Processing activity name") + cmd.Flags().StringVar(&flagPurpose, "purpose", "", "Purpose of processing") + cmd.Flags().StringVar(&flagRole, "role", "", "Role: CONTROLLER, PROCESSOR") + cmd.Flags().StringVar(&flagLawfulBasis, "lawful-basis", "", "Lawful basis: LEGITIMATE_INTEREST, CONSENT, CONTRACTUAL_NECESSITY, LEGAL_OBLIGATION, VITAL_INTERESTS, PUBLIC_TASK") + + return cmd +} diff --git a/pkg/cmd/processing-activity/view/view.go b/pkg/cmd/processing-activity/view/view.go new file mode 100644 index 000000000..aeeb2256d --- /dev/null +++ b/pkg/cmd/processing-activity/view/view.go @@ -0,0 +1,139 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on ProcessingActivity { + id + name + purpose + role + lawfulBasis + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Purpose *string `json:"purpose"` + Role string `json:"role"` + LawfulBasis string `json:"lawfulBasis"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a processing activity", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("processing activity %s not found", args[0]) + } + + if resp.Node.Typename != "ProcessingActivity" { + return fmt.Errorf("expected ProcessingActivity node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + a := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(a.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), a.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Role:"), a.Role) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Lawful Basis:"), a.LawfulBasis) + + if a.Purpose != nil && *a.Purpose != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Purpose:"), *a.Purpose) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(a.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(a.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/rights-request/create/create.go b/pkg/cmd/rights-request/create/create.go new file mode 100644 index 000000000..a66826b24 --- /dev/null +++ b/pkg/cmd/rights-request/create/create.go @@ -0,0 +1,208 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateRightsRequestInput!) { + createRightsRequest(input: $input) { + rightsRequestEdge { + node { + id + requestType + requestState + dataSubject + } + } + } +} +` + +type createResponse struct { + CreateRightsRequest struct { + RightsRequestEdge struct { + Node struct { + ID string `json:"id"` + RequestType string `json:"requestType"` + RequestState string `json:"requestState"` + DataSubject string `json:"dataSubject"` + } `json:"node"` + } `json:"rightsRequestEdge"` + } `json:"createRightsRequest"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagDataSubject string + flagType string + flagState string + flagContact string + flagDetails string + flagDeadline string + flagActionTaken string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new rights request", + Example: ` # Create a rights request interactively + prb rights-request create + + # Create a rights request non-interactively + prb rights-request create --data-subject "John Doe" --type ACCESS --state TODO`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagDataSubject == "" { + err := huh.NewInput(). + Title("Data subject"). + Value(&flagDataSubject). + Run() + if err != nil { + return err + } + } + + if flagType == "" { + err := huh.NewSelect[string](). + Title("Request type"). + Options( + huh.NewOption("Access", "ACCESS"), + huh.NewOption("Deletion", "DELETION"), + huh.NewOption("Portability", "PORTABILITY"), + ). + Value(&flagType). + Run() + if err != nil { + return err + } + } + + if flagState == "" { + err := huh.NewSelect[string](). + Title("Request state"). + Options( + huh.NewOption("To Do", "TODO"), + huh.NewOption("In Progress", "IN_PROGRESS"), + huh.NewOption("Done", "DONE"), + ). + Value(&flagState). + Run() + if err != nil { + return err + } + } + } + + if flagDataSubject == "" { + return fmt.Errorf("data subject is required; pass --data-subject or run interactively") + } + if flagType == "" { + return fmt.Errorf("request type is required; pass --type or run interactively") + } + if flagState == "" { + return fmt.Errorf("request state is required; pass --state or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "requestType": flagType, + "requestState": flagState, + "dataSubject": flagDataSubject, + } + + if flagContact != "" { + input["contact"] = flagContact + } + if flagDetails != "" { + input["details"] = flagDetails + } + if flagDeadline != "" { + input["deadline"] = flagDeadline + } + if flagActionTaken != "" { + input["actionTaken"] = flagActionTaken + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateRightsRequest.RightsRequestEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created rights request %s\n", + r.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagDataSubject, "data-subject", "", "Data subject name (required)") + cmd.Flags().StringVar(&flagType, "type", "", "Request type: ACCESS, DELETION, PORTABILITY (required)") + cmd.Flags().StringVar(&flagState, "state", "", "Request state: TODO, IN_PROGRESS, DONE (required)") + cmd.Flags().StringVar(&flagContact, "contact", "", "Contact information") + cmd.Flags().StringVar(&flagDetails, "details", "", "Request details") + cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline") + cmd.Flags().StringVar(&flagActionTaken, "action-taken", "", "Action taken") + + return cmd +} diff --git a/pkg/cmd/rights-request/delete/delete.go b/pkg/cmd/rights-request/delete/delete.go new file mode 100644 index 000000000..f31df7cbe --- /dev/null +++ b/pkg/cmd/rights-request/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteRightsRequestInput!) { + deleteRightsRequest(input: $input) { + deletedRightsRequestId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a rights request", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete rights request: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete rights request %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{ + "rightsRequestId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted rights request %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/rights-request/list/list.go b/pkg/cmd/rights-request/list/list.go new file mode 100644 index 000000000..445f583fa --- /dev/null +++ b/pkg/cmd/rights-request/list/list.go @@ -0,0 +1,200 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: RightsRequestOrder) { + node(id: $id) { + __typename + ... on Organization { + rightsRequests(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + dataSubject + requestType + requestState + deadline + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type rightsRequest struct { + ID string `json:"id"` + DataSubject string `json:"dataSubject"` + RequestType string `json:"requestType"` + RequestState string `json:"requestState"` + Deadline *string `json:"deadline"` +} + +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 rights requests in an organization", + Aliases: []string{"ls"}, + Example: ` # List rights requests in the default organization + prb rights-request list + + # List rights requests sorted by deadline + prb rights-request ls --order-by DEADLINE --json`, + 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{"CREATED_AT", "DEADLINE", "STATE", "TYPE"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + rightsRequests, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[rightsRequest], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + RightsRequests api.Connection[rightsRequest] `json:"rightsRequests"` + } `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) + } + return &resp.Node.RightsRequests, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, rightsRequests) + } + + if len(rightsRequests) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No rights requests found.") + return nil + } + + rows := make([][]string, 0, len(rightsRequests)) + for _, r := range rightsRequests { + deadline := "" + if r.Deadline != nil { + deadline = *r.Deadline + } + rows = append(rows, []string{ + r.ID, + r.DataSubject, + r.RequestType, + r.RequestState, + deadline, + }) + } + + t := cmdutil.NewTable("ID", "DATA SUBJECT", "TYPE", "STATE", "DEADLINE").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(rightsRequests) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d rights requests\n", + len(rightsRequests), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of rights requests to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, DEADLINE, STATE, TYPE)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/rights-request/rights_request.go b/pkg/cmd/rights-request/rights_request.go new file mode 100644 index 000000000..42cd64893 --- /dev/null +++ b/pkg/cmd/rights-request/rights_request.go @@ -0,0 +1,41 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package rightsrequest + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/rights-request/create" + "go.probo.inc/probo/pkg/cmd/rights-request/delete" + "go.probo.inc/probo/pkg/cmd/rights-request/list" + "go.probo.inc/probo/pkg/cmd/rights-request/update" + "go.probo.inc/probo/pkg/cmd/rights-request/view" +) + +func NewCmdRightsRequest(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "rights-request ", + Short: "Manage rights requests", + Aliases: []string{"rr"}, + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/rights-request/update/update.go b/pkg/cmd/rights-request/update/update.go new file mode 100644 index 000000000..54b4e17b2 --- /dev/null +++ b/pkg/cmd/rights-request/update/update.go @@ -0,0 +1,147 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateRightsRequestInput!) { + updateRightsRequest(input: $input) { + rightsRequest { + id + requestType + requestState + dataSubject + } + } +} +` + +type updateResponse struct { + UpdateRightsRequest struct { + RightsRequest struct { + ID string `json:"id"` + RequestType string `json:"requestType"` + RequestState string `json:"requestState"` + DataSubject string `json:"dataSubject"` + } `json:"rightsRequest"` + } `json:"updateRightsRequest"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagType string + flagState string + flagDataSubject string + flagContact string + flagDetails string + flagDeadline string + flagActionTaken string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a rights request", + 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("type") { + input["requestType"] = flagType + } + if cmd.Flags().Changed("state") { + input["requestState"] = flagState + } + if cmd.Flags().Changed("data-subject") { + input["dataSubject"] = flagDataSubject + } + if cmd.Flags().Changed("contact") { + input["contact"] = flagContact + } + if cmd.Flags().Changed("details") { + input["details"] = flagDetails + } + if cmd.Flags().Changed("deadline") { + input["deadline"] = flagDeadline + } + if cmd.Flags().Changed("action-taken") { + input["actionTaken"] = flagActionTaken + } + + 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) + } + + r := resp.UpdateRightsRequest.RightsRequest + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated rights request %s\n", + r.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagType, "type", "", "Request type: ACCESS, DELETION, PORTABILITY") + cmd.Flags().StringVar(&flagState, "state", "", "Request state: TODO, IN_PROGRESS, DONE") + cmd.Flags().StringVar(&flagDataSubject, "data-subject", "", "Data subject name") + cmd.Flags().StringVar(&flagContact, "contact", "", "Contact information") + cmd.Flags().StringVar(&flagDetails, "details", "", "Request details") + cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline") + cmd.Flags().StringVar(&flagActionTaken, "action-taken", "", "Action taken") + + return cmd +} diff --git a/pkg/cmd/rights-request/view/view.go b/pkg/cmd/rights-request/view/view.go new file mode 100644 index 000000000..c5f17950a --- /dev/null +++ b/pkg/cmd/rights-request/view/view.go @@ -0,0 +1,157 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on RightsRequest { + id + requestType + requestState + dataSubject + contact + details + deadline + actionTaken + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + RequestType string `json:"requestType"` + RequestState string `json:"requestState"` + DataSubject string `json:"dataSubject"` + Contact *string `json:"contact"` + Details *string `json:"details"` + Deadline *string `json:"deadline"` + ActionTaken *string `json:"actionTaken"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a rights request", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("rights request %s not found", args[0]) + } + + if resp.Node.Typename != "RightsRequest" { + return fmt.Errorf("expected RightsRequest node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(r.DataSubject)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), r.RequestType) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), r.RequestState) + + if r.Contact != nil && *r.Contact != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Contact:"), *r.Contact) + } + + if r.Details != nil && *r.Details != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Details:"), *r.Details) + } + + if r.Deadline != nil && *r.Deadline != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Deadline:"), *r.Deadline) + } + + if r.ActionTaken != nil && *r.ActionTaken != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Action Taken:"), *r.ActionTaken) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/root/root.go b/pkg/cmd/root/root.go index d1d71204e..3cc7f0e35 100644 --- a/pkg/cmd/root/root.go +++ b/pkg/cmd/root/root.go @@ -19,6 +19,7 @@ import ( accessreview "go.probo.inc/probo/pkg/cmd/access-review" cmdapi "go.probo.inc/probo/pkg/cmd/api" "go.probo.inc/probo/pkg/cmd/asset" + "go.probo.inc/probo/pkg/cmd/audit" "go.probo.inc/probo/pkg/cmd/auditlog" "go.probo.inc/probo/pkg/cmd/auth" "go.probo.inc/probo/pkg/cmd/browse" @@ -29,13 +30,23 @@ import ( "go.probo.inc/probo/pkg/cmd/control" "go.probo.inc/probo/pkg/cmd/datum" "go.probo.inc/probo/pkg/cmd/document" + "go.probo.inc/probo/pkg/cmd/dpia" "go.probo.inc/probo/pkg/cmd/evidence" "go.probo.inc/probo/pkg/cmd/finding" "go.probo.inc/probo/pkg/cmd/framework" + "go.probo.inc/probo/pkg/cmd/measure" + "go.probo.inc/probo/pkg/cmd/obligation" "go.probo.inc/probo/pkg/cmd/org" + processingactivity "go.probo.inc/probo/pkg/cmd/processing-activity" + rightsrequest "go.probo.inc/probo/pkg/cmd/rights-request" "go.probo.inc/probo/pkg/cmd/risk" + "go.probo.inc/probo/pkg/cmd/snapshot" "go.probo.inc/probo/pkg/cmd/soa" + "go.probo.inc/probo/pkg/cmd/task" + "go.probo.inc/probo/pkg/cmd/tia" + trustcenter "go.probo.inc/probo/pkg/cmd/trust-center" "go.probo.inc/probo/pkg/cmd/user" + "go.probo.inc/probo/pkg/cmd/vendormgmt" "go.probo.inc/probo/pkg/cmd/version" "go.probo.inc/probo/pkg/cmd/webhook" ) @@ -73,6 +84,7 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(accessreview.NewCmdAccessReview(f)) cmd.AddCommand(cmdapi.NewCmdAPI(f)) cmd.AddCommand(asset.NewCmdAsset(f)) + cmd.AddCommand(audit.NewCmdAudit(f)) cmd.AddCommand(auditlog.NewCmdAuditLog(f)) cmd.AddCommand(auth.NewCmdAuth(f)) cmd.AddCommand(browse.NewCmdBrowse(f)) @@ -82,13 +94,23 @@ func NewCmdRoot(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(control.NewCmdControl(f)) cmd.AddCommand(datum.NewCmdDatum(f)) cmd.AddCommand(document.NewCmdDocument(f)) + cmd.AddCommand(dpia.NewCmdDPIA(f)) cmd.AddCommand(evidence.NewCmdEvidence(f)) cmd.AddCommand(finding.NewCmdFinding(f)) cmd.AddCommand(framework.NewCmdFramework(f)) + cmd.AddCommand(measure.NewCmdMeasure(f)) + cmd.AddCommand(obligation.NewCmdObligation(f)) cmd.AddCommand(org.NewCmdOrg(f)) + cmd.AddCommand(processingactivity.NewCmdProcessingActivity(f)) + cmd.AddCommand(rightsrequest.NewCmdRightsRequest(f)) cmd.AddCommand(risk.NewCmdRisk(f)) + cmd.AddCommand(snapshot.NewCmdSnapshot(f)) cmd.AddCommand(soa.NewCmdSoa(f)) + cmd.AddCommand(task.NewCmdTask(f)) + cmd.AddCommand(tia.NewCmdTIA(f)) + cmd.AddCommand(trustcenter.NewCmdTrustCenter(f)) cmd.AddCommand(user.NewCmdUser(f)) + cmd.AddCommand(vendormgmt.NewCmdVendor(f)) cmd.AddCommand(version.NewCmdVersion(f)) cmd.AddCommand(webhook.NewCmdWebhook(f)) diff --git a/pkg/cmd/snapshot/create/create.go b/pkg/cmd/snapshot/create/create.go new file mode 100644 index 000000000..eae9dad53 --- /dev/null +++ b/pkg/cmd/snapshot/create/create.go @@ -0,0 +1,175 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateSnapshotInput!) { + createSnapshot(input: $input) { + snapshotEdge { + node { + id + name + type + } + } + } +} +` + +type createResponse struct { + CreateSnapshot struct { + SnapshotEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + } `json:"node"` + } `json:"snapshotEdge"` + } `json:"createSnapshot"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagType string + flagDescription string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new snapshot", + Example: ` # Create a snapshot interactively + prb snapshot create + + # Create a snapshot non-interactively + prb snapshot create --name "Q1 2026 Risks" --type RISKS`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Snapshot name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagType == "" { + err := huh.NewSelect[string](). + Title("Snapshot type"). + Options( + huh.NewOption("Risks", "RISKS"), + huh.NewOption("Vendors", "VENDORS"), + huh.NewOption("Assets", "ASSETS"), + huh.NewOption("Findings", "FINDINGS"), + huh.NewOption("Obligations", "OBLIGATIONS"), + huh.NewOption("Processing Activities", "PROCESSING_ACTIVITIES"), + huh.NewOption("Statements of Applicability", "STATEMENTS_OF_APPLICABILITY"), + ). + Value(&flagType). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagType == "" { + return fmt.Errorf("type is required; pass --type or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + "type": flagType, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + s := resp.CreateSnapshot.SnapshotEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created snapshot %s (%s)\n", + s.ID, + s.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Snapshot name (required)") + cmd.Flags().StringVar(&flagType, "type", "", "Snapshot type: RISKS, VENDORS, ASSETS, FINDINGS, OBLIGATIONS, PROCESSING_ACTIVITIES, STATEMENTS_OF_APPLICABILITY (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Snapshot description") + + return cmd +} diff --git a/pkg/cmd/snapshot/delete/delete.go b/pkg/cmd/snapshot/delete/delete.go new file mode 100644 index 000000000..25d673978 --- /dev/null +++ b/pkg/cmd/snapshot/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteSnapshotInput!) { + deleteSnapshot(input: $input) { + deletedSnapshotId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a snapshot", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete snapshot: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete snapshot %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{ + "snapshotId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted snapshot %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/snapshot/list/list.go b/pkg/cmd/snapshot/list/list.go new file mode 100644 index 000000000..4f5664dde --- /dev/null +++ b/pkg/cmd/snapshot/list/list.go @@ -0,0 +1,193 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: SnapshotOrder) { + node(id: $id) { + __typename + ... on Organization { + snapshots(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + type + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type snapshot struct { + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + 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 snapshots in an organization", + Aliases: []string{"ls"}, + Example: ` # List snapshots in the default organization + prb snapshot list + + # List snapshots sorted by name + prb snapshot ls --order-by NAME --json`, + 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{"CREATED_AT", "NAME", "TYPE"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + snapshots, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[snapshot], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Snapshots api.Connection[snapshot] `json:"snapshots"` + } `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) + } + return &resp.Node.Snapshots, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, snapshots) + } + + if len(snapshots) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No snapshots found.") + return nil + } + + rows := make([][]string, 0, len(snapshots)) + for _, s := range snapshots { + rows = append(rows, []string{ + s.ID, + s.Name, + s.Type, + cmdutil.FormatTime(s.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "TYPE", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(snapshots) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d snapshots\n", + len(snapshots), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of snapshots to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT, NAME, TYPE)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/snapshot/snapshot.go b/pkg/cmd/snapshot/snapshot.go new file mode 100644 index 000000000..cb3ebacc7 --- /dev/null +++ b/pkg/cmd/snapshot/snapshot.go @@ -0,0 +1,38 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package snapshot + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/snapshot/create" + "go.probo.inc/probo/pkg/cmd/snapshot/delete" + "go.probo.inc/probo/pkg/cmd/snapshot/list" + "go.probo.inc/probo/pkg/cmd/snapshot/view" +) + +func NewCmdSnapshot(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "snapshot ", + Short: "Manage snapshots", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/snapshot/view/view.go b/pkg/cmd/snapshot/view/view.go new file mode 100644 index 000000000..919d096bd --- /dev/null +++ b/pkg/cmd/snapshot/view/view.go @@ -0,0 +1,133 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Snapshot { + id + name + description + type + createdAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Type string `json:"type"` + CreatedAt string `json:"createdAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a snapshot", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("snapshot %s not found", args[0]) + } + + if resp.Node.Typename != "Snapshot" { + return fmt.Errorf("expected Snapshot node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + s := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(s.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), s.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Type:"), s.Type) + + if s.Description != nil && *s.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *s.Description) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(s.CreatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/task/create/create.go b/pkg/cmd/task/create/create.go new file mode 100644 index 000000000..61749039b --- /dev/null +++ b/pkg/cmd/task/create/create.go @@ -0,0 +1,193 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateTaskInput!) { + createTask(input: $input) { + taskEdge { + node { + id + name + state + priority + } + } + } +} +` + +type createResponse struct { + CreateTask struct { + TaskEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + Priority string `json:"priority"` + } `json:"node"` + } `json:"taskEdge"` + } `json:"createTask"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagDescription string + flagPriority string + flagMeasure string + flagTimeEstimate string + flagAssignedTo string + flagDeadline string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new task", + Example: ` # Create a task interactively + prb task create + + # Create a task non-interactively + prb task create --name "Review access controls" --priority HIGH`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Task name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagPriority == "" { + err := huh.NewSelect[string](). + Title("Task priority"). + Options( + huh.NewOption("Urgent", "URGENT"), + huh.NewOption("High", "HIGH"), + huh.NewOption("Medium", "MEDIUM"), + huh.NewOption("Low", "LOW"), + ). + Value(&flagPriority). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + if flagPriority != "" { + input["priority"] = flagPriority + } + if flagMeasure != "" { + input["measureId"] = flagMeasure + } + if flagTimeEstimate != "" { + input["timeEstimate"] = flagTimeEstimate + } + if flagAssignedTo != "" { + input["assignedToId"] = flagAssignedTo + } + if flagDeadline != "" { + input["deadline"] = flagDeadline + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + t := resp.CreateTask.TaskEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created task %s (%s)\n", + t.ID, + t.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Task name (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Task description") + cmd.Flags().StringVar(&flagPriority, "priority", "", "Task priority: URGENT, HIGH, MEDIUM, LOW") + cmd.Flags().StringVar(&flagMeasure, "measure", "", "Measure ID") + cmd.Flags().StringVar(&flagTimeEstimate, "time-estimate", "", "Time estimate") + cmd.Flags().StringVar(&flagAssignedTo, "assigned-to", "", "Assigned profile ID") + cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline") + + return cmd +} diff --git a/pkg/cmd/task/delete/delete.go b/pkg/cmd/task/delete/delete.go new file mode 100644 index 000000000..7300457db --- /dev/null +++ b/pkg/cmd/task/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteTaskInput!) { + deleteTask(input: $input) { + deletedTaskId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a task", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete task: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete task %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{ + "taskId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted task %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/task/list/list.go b/pkg/cmd/task/list/list.go new file mode 100644 index 000000000..de7c81d46 --- /dev/null +++ b/pkg/cmd/task/list/list.go @@ -0,0 +1,200 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: TaskOrder) { + node(id: $id) { + __typename + ... on Organization { + tasks(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + state + priority + deadline + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type task struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + Priority string `json:"priority"` + Deadline *string `json:"deadline"` +} + +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 tasks in an organization", + Aliases: []string{"ls"}, + Example: ` # List tasks in the default organization + prb task list + + # List tasks sorted by priority + prb task ls --order-by PRIORITY_RANK --json`, + 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{"PRIORITY_RANK", "CREATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + tasks, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[task], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Tasks api.Connection[task] `json:"tasks"` + } `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) + } + return &resp.Node.Tasks, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, tasks) + } + + if len(tasks) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No tasks found.") + return nil + } + + rows := make([][]string, 0, len(tasks)) + for _, t := range tasks { + deadline := "" + if t.Deadline != nil { + deadline = *t.Deadline + } + rows = append(rows, []string{ + t.ID, + t.Name, + t.State, + t.Priority, + deadline, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "STATE", "PRIORITY", "DEADLINE").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(tasks) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d tasks\n", + len(tasks), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of tasks to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (PRIORITY_RANK, CREATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/task/task.go b/pkg/cmd/task/task.go new file mode 100644 index 000000000..8e264256f --- /dev/null +++ b/pkg/cmd/task/task.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package task + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/task/create" + "go.probo.inc/probo/pkg/cmd/task/delete" + "go.probo.inc/probo/pkg/cmd/task/list" + "go.probo.inc/probo/pkg/cmd/task/update" + "go.probo.inc/probo/pkg/cmd/task/view" +) + +func NewCmdTask(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "task ", + Short: "Manage tasks", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/task/update/update.go b/pkg/cmd/task/update/update.go new file mode 100644 index 000000000..c3f2d773b --- /dev/null +++ b/pkg/cmd/task/update/update.go @@ -0,0 +1,161 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateTaskInput!) { + updateTask(input: $input) { + task { + id + name + state + priority + } + } +} +` + +type updateResponse struct { + UpdateTask struct { + Task struct { + ID string `json:"id"` + Name string `json:"name"` + State string `json:"state"` + Priority string `json:"priority"` + } `json:"task"` + } `json:"updateTask"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagDescription string + flagState string + flagPriority string + flagTimeEstimate string + flagDeadline string + flagAssignedTo string + flagMeasure string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a task", + 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{ + "taskId": args[0], + } + + if cmd.Flags().Changed("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + if cmd.Flags().Changed("state") { + input["state"] = flagState + } + if cmd.Flags().Changed("priority") { + input["priority"] = flagPriority + } + if cmd.Flags().Changed("time-estimate") { + input["timeEstimate"] = flagTimeEstimate + } + if cmd.Flags().Changed("deadline") { + input["deadline"] = flagDeadline + } + if cmd.Flags().Changed("assigned-to") { + if flagAssignedTo == "" { + input["assignedToId"] = nil + } else { + input["assignedToId"] = flagAssignedTo + } + } + if cmd.Flags().Changed("measure") { + if flagMeasure == "" { + input["measureId"] = nil + } else { + input["measureId"] = flagMeasure + } + } + + 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) + } + + t := resp.UpdateTask.Task + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated task %s (%s)\n", + t.ID, + t.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Task name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Task description") + cmd.Flags().StringVar(&flagState, "state", "", "Task state: TODO, IN_PROGRESS, DONE") + cmd.Flags().StringVar(&flagPriority, "priority", "", "Task priority: URGENT, HIGH, MEDIUM, LOW") + cmd.Flags().StringVar(&flagTimeEstimate, "time-estimate", "", "Time estimate") + cmd.Flags().StringVar(&flagDeadline, "deadline", "", "Deadline") + cmd.Flags().StringVar(&flagAssignedTo, "assigned-to", "", "Assigned profile ID") + cmd.Flags().StringVar(&flagMeasure, "measure", "", "Measure ID") + + return cmd +} diff --git a/pkg/cmd/task/view/view.go b/pkg/cmd/task/view/view.go new file mode 100644 index 000000000..8e5047e88 --- /dev/null +++ b/pkg/cmd/task/view/view.go @@ -0,0 +1,151 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Task { + id + name + description + state + priority + timeEstimate + deadline + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + State string `json:"state"` + Priority string `json:"priority"` + TimeEstimate *string `json:"timeEstimate"` + Deadline *string `json:"deadline"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a task", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("task %s not found", args[0]) + } + + if resp.Node.Typename != "Task" { + return fmt.Errorf("expected Task node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + t := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(t.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), t.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), t.State) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Priority:"), t.Priority) + + if t.Description != nil && *t.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *t.Description) + } + + if t.TimeEstimate != nil && *t.TimeEstimate != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Time Estimate:"), *t.TimeEstimate) + } + + if t.Deadline != nil && *t.Deadline != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Deadline:"), *t.Deadline) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(t.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(t.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/tia/create/create.go b/pkg/cmd/tia/create/create.go new file mode 100644 index 000000000..be2487f85 --- /dev/null +++ b/pkg/cmd/tia/create/create.go @@ -0,0 +1,147 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package create + +import ( + "encoding/json" + "fmt" + + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const createMutation = ` +mutation($input: CreateTransferImpactAssessmentInput!) { + createTransferImpactAssessment(input: $input) { + transferImpactAssessmentEdge { + node { + id + dataSubjects + legalMechanism + } + } + } +} +` + +type createResponse struct { + CreateTransferImpactAssessment struct { + TransferImpactAssessmentEdge struct { + Node struct { + ID string `json:"id"` + DataSubjects string `json:"dataSubjects"` + LegalMechanism string `json:"legalMechanism"` + } `json:"node"` + } `json:"transferImpactAssessmentEdge"` + } `json:"createTransferImpactAssessment"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagProcessingActivity string + flagDataSubjects string + flagLegalMechanism string + flagTransfer string + flagLocalLawRisk string + flagSupplementaryMeasures string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new transfer impact assessment", + Example: ` # Create a TIA + prb tia create --processing-activity --data-subjects "EU residents" + + # Create a TIA with all fields + prb tia create --processing-activity --data-subjects "EU residents" --legal-mechanism "SCCs" --transfer "US" --local-law-risk "FISA 702" --supplementary-measures "Encryption"`, + 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 flagProcessingActivity == "" { + return fmt.Errorf("processing activity is required; pass --processing-activity") + } + + input := map[string]any{ + "processingActivityId": flagProcessingActivity, + } + + if flagDataSubjects != "" { + input["dataSubjects"] = flagDataSubjects + } + if flagLegalMechanism != "" { + input["legalMechanism"] = flagLegalMechanism + } + if flagTransfer != "" { + input["transfer"] = flagTransfer + } + if flagLocalLawRisk != "" { + input["localLawRisk"] = flagLocalLawRisk + } + if flagSupplementaryMeasures != "" { + input["supplementaryMeasures"] = flagSupplementaryMeasures + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateTransferImpactAssessment.TransferImpactAssessmentEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created transfer impact assessment %s\n", + r.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagProcessingActivity, "processing-activity", "", "Processing activity ID (required)") + cmd.Flags().StringVar(&flagDataSubjects, "data-subjects", "", "Data subjects") + cmd.Flags().StringVar(&flagLegalMechanism, "legal-mechanism", "", "Legal mechanism") + cmd.Flags().StringVar(&flagTransfer, "transfer", "", "Transfer") + cmd.Flags().StringVar(&flagLocalLawRisk, "local-law-risk", "", "Local law risk") + cmd.Flags().StringVar(&flagSupplementaryMeasures, "supplementary-measures", "", "Supplementary measures") + + _ = cmd.MarkFlagRequired("processing-activity") + + return cmd +} diff --git a/pkg/cmd/tia/delete/delete.go b/pkg/cmd/tia/delete/delete.go new file mode 100644 index 000000000..c34fb7f62 --- /dev/null +++ b/pkg/cmd/tia/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteTransferImpactAssessmentInput!) { + deleteTransferImpactAssessment(input: $input) { + deletedTransferImpactAssessmentId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a transfer impact assessment", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete transfer impact assessment: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete transfer impact assessment %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{ + "transferImpactAssessmentId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted transfer impact assessment %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/tia/list/list.go b/pkg/cmd/tia/list/list.go new file mode 100644 index 000000000..8b7da6050 --- /dev/null +++ b/pkg/cmd/tia/list/list.go @@ -0,0 +1,195 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: TransferImpactAssessmentOrder) { + node(id: $id) { + __typename + ... on Organization { + transferImpactAssessments(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + dataSubjects + legalMechanism + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type transferImpactAssessment struct { + ID string `json:"id"` + DataSubjects string `json:"dataSubjects"` + LegalMechanism string `json:"legalMechanism"` + CreatedAt string `json:"createdAt"` +} + +func truncate(s string, max int) string { + if len(s) <= max { + return s + } + return s[:max-3] + "..." +} + +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 transfer impact assessments in an organization", + Aliases: []string{"ls"}, + 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{"CREATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + tias, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[transferImpactAssessment], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + TransferImpactAssessments api.Connection[transferImpactAssessment] `json:"transferImpactAssessments"` + } `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) + } + return &resp.Node.TransferImpactAssessments, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, tias) + } + + if len(tias) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No transfer impact assessments found.") + return nil + } + + rows := make([][]string, 0, len(tias)) + for _, t := range tias { + rows = append(rows, []string{ + t.ID, + truncate(t.DataSubjects, 50), + truncate(t.LegalMechanism, 50), + cmdutil.FormatTime(t.CreatedAt), + }) + } + + tbl := cmdutil.NewTable("ID", "DATA SUBJECTS", "LEGAL MECHANISM", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, tbl) + + if totalCount > len(tias) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d transfer impact assessments\n", + len(tias), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of transfer impact assessments to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (CREATED_AT)") + cmd.Flags().StringVar(&flagOrderDir, "order-direction", "DESC", "Sort direction (ASC, DESC)") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/tia/tia.go b/pkg/cmd/tia/tia.go new file mode 100644 index 000000000..ddb6e5d9f --- /dev/null +++ b/pkg/cmd/tia/tia.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package tia + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/tia/create" + "go.probo.inc/probo/pkg/cmd/tia/delete" + "go.probo.inc/probo/pkg/cmd/tia/list" + "go.probo.inc/probo/pkg/cmd/tia/update" + "go.probo.inc/probo/pkg/cmd/tia/view" +) + +func NewCmdTIA(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "tia ", + Short: "Manage transfer impact assessments", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/tia/update/update.go b/pkg/cmd/tia/update/update.go new file mode 100644 index 000000000..5d617a002 --- /dev/null +++ b/pkg/cmd/tia/update/update.go @@ -0,0 +1,135 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateTransferImpactAssessmentInput!) { + updateTransferImpactAssessment(input: $input) { + transferImpactAssessment { + id + dataSubjects + legalMechanism + } + } +} +` + +type updateResponse struct { + UpdateTransferImpactAssessment struct { + TransferImpactAssessment struct { + ID string `json:"id"` + DataSubjects string `json:"dataSubjects"` + LegalMechanism string `json:"legalMechanism"` + } `json:"transferImpactAssessment"` + } `json:"updateTransferImpactAssessment"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagDataSubjects string + flagLegalMechanism string + flagTransfer string + flagLocalLawRisk string + flagSupplementaryMeasures string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a transfer impact assessment", + 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("data-subjects") { + input["dataSubjects"] = flagDataSubjects + } + if cmd.Flags().Changed("legal-mechanism") { + input["legalMechanism"] = flagLegalMechanism + } + if cmd.Flags().Changed("transfer") { + input["transfer"] = flagTransfer + } + if cmd.Flags().Changed("local-law-risk") { + input["localLawRisk"] = flagLocalLawRisk + } + if cmd.Flags().Changed("supplementary-measures") { + input["supplementaryMeasures"] = flagSupplementaryMeasures + } + + 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) + } + + r := resp.UpdateTransferImpactAssessment.TransferImpactAssessment + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated transfer impact assessment %s\n", + r.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagDataSubjects, "data-subjects", "", "Data subjects") + cmd.Flags().StringVar(&flagLegalMechanism, "legal-mechanism", "", "Legal mechanism") + cmd.Flags().StringVar(&flagTransfer, "transfer", "", "Transfer") + cmd.Flags().StringVar(&flagLocalLawRisk, "local-law-risk", "", "Local law risk") + cmd.Flags().StringVar(&flagSupplementaryMeasures, "supplementary-measures", "", "Supplementary measures") + + return cmd +} diff --git a/pkg/cmd/tia/view/view.go b/pkg/cmd/tia/view/view.go new file mode 100644 index 000000000..4ed40a7e0 --- /dev/null +++ b/pkg/cmd/tia/view/view.go @@ -0,0 +1,155 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on TransferImpactAssessment { + id + dataSubjects + legalMechanism + transfer + localLawRisk + supplementaryMeasures + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + DataSubjects string `json:"dataSubjects"` + LegalMechanism string `json:"legalMechanism"` + Transfer string `json:"transfer"` + LocalLawRisk string `json:"localLawRisk"` + SupplementaryMeasures string `json:"supplementaryMeasures"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a transfer impact assessment", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("transfer impact assessment %s not found", args[0]) + } + + if resp.Node.Typename != "TransferImpactAssessment" { + return fmt.Errorf("expected TransferImpactAssessment node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + r := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(26) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Transfer Impact Assessment")) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), r.ID) + + if r.DataSubjects != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Data Subjects:"), r.DataSubjects) + } + + if r.LegalMechanism != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Legal Mechanism:"), r.LegalMechanism) + } + + if r.Transfer != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Transfer:"), r.Transfer) + } + + if r.LocalLawRisk != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Local Law Risk:"), r.LocalLawRisk) + } + + if r.SupplementaryMeasures != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Supplementary Measures:"), r.SupplementaryMeasures) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(r.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(r.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/trust-center/file/delete/delete.go b/pkg/cmd/trust-center/file/delete/delete.go new file mode 100644 index 000000000..39f6c4607 --- /dev/null +++ b/pkg/cmd/trust-center/file/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteTrustCenterFileInput!) { + deleteTrustCenterFile(input: $input) { + deletedTrustCenterFileId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a trust center file", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete file: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete trust center file %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 trust center file %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/file/file.go b/pkg/cmd/trust-center/file/file.go new file mode 100644 index 000000000..857a171cc --- /dev/null +++ b/pkg/cmd/trust-center/file/file.go @@ -0,0 +1,34 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package file + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/trust-center/file/delete" + "go.probo.inc/probo/pkg/cmd/trust-center/file/list" +) + +func NewCmdFile(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "file ", + Short: "Manage trust center files", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/trust-center/file/list/list.go b/pkg/cmd/trust-center/file/list/list.go new file mode 100644 index 000000000..55ecdc5ff --- /dev/null +++ b/pkg/cmd/trust-center/file/list/list.go @@ -0,0 +1,195 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: TrustCenterFileOrder) { + node(id: $id) { + __typename + ... on Organization { + trustCenterFiles(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + category + trustCenterVisibility + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type trustCenterFile struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + TrustCenterVisibility string `json:"trustCenterVisibility"` + 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 trust center files", + Aliases: []string{"ls"}, + Example: ` # List files in the default organization + prb trust-center file list + + # List files sorted by name + prb trust-center file ls --order-by NAME`, + 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{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + files, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[trustCenterFile], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenterFiles api.Connection[trustCenterFile] `json:"trustCenterFiles"` + } `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) + } + return &resp.Node.TrustCenterFiles, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, files) + } + + if len(files) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No files found.") + return nil + } + + rows := make([][]string, 0, len(files)) + for _, file := range files { + rows = append(rows, []string{ + file.ID, + file.Name, + file.Category, + file.TrustCenterVisibility, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "CATEGORY", "VISIBILITY").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(files) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d files\n", + len(files), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of files to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (NAME, 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/reference/create/create.go b/pkg/cmd/trust-center/reference/create/create.go new file mode 100644 index 000000000..314b79849 --- /dev/null +++ b/pkg/cmd/trust-center/reference/create/create.go @@ -0,0 +1,227 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: CreateTrustCenterReferenceInput!) { + createTrustCenterReference(input: $input) { + trustCenterReferenceEdge { + node { + id + name + description + websiteUrl + rank + } + } + } +} +` + +type trustCenterQueryResponse struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenter *struct { + ID string `json:"id"` + } `json:"trustCenter"` + } `json:"node"` +} + +type createResponse struct { + CreateTrustCenterReference struct { + TrustCenterReferenceEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + WebsiteUrl *string `json:"websiteUrl"` + Rank int `json:"rank"` + } `json:"node"` + } `json:"trustCenterReferenceEdge"` + } `json:"createTrustCenterReference"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagDescription string + flagWebsite string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a trust center reference", + Example: ` # Create a reference interactively + prb trust-center reference create + + # Create a reference non-interactively + prb trust-center ref create --name "Acme Corp" --description "Enterprise customer" --website "https://acme.com"`, + 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'") + } + + // Fetch trust center ID from organization. + 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 flagName == "" { + err := huh.NewInput(). + Title("Reference name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagDescription == "" { + err := huh.NewInput(). + Title("Description (optional)"). + Value(&flagDescription). + Run() + if err != nil { + return err + } + } + + if flagWebsite == "" { + err := huh.NewInput(). + Title("Website URL (optional)"). + Value(&flagWebsite). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + + input := map[string]any{ + "trustCenterId": tcResp.Node.TrustCenter.ID, + "name": flagName, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + if flagWebsite != "" { + input["websiteUrl"] = flagWebsite + } + + data, err = client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + r := resp.CreateTrustCenterReference.TrustCenterReferenceEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created reference %s (%s)\n", + r.ID, + r.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Reference name (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Reference description") + cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL") + + return cmd +} diff --git a/pkg/cmd/trust-center/reference/delete/delete.go b/pkg/cmd/trust-center/reference/delete/delete.go new file mode 100644 index 000000000..5c77fcf65 --- /dev/null +++ b/pkg/cmd/trust-center/reference/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteTrustCenterReferenceInput!) { + deleteTrustCenterReference(input: $input) { + deletedTrustCenterReferenceId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a trust center reference", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete reference: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete reference %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 reference %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/reference/list/list.go b/pkg/cmd/trust-center/reference/list/list.go new file mode 100644 index 000000000..2b915e483 --- /dev/null +++ b/pkg/cmd/trust-center/reference/list/list.go @@ -0,0 +1,208 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: TrustCenterReferenceOrder) { + node(id: $id) { + __typename + ... on Organization { + trustCenter { + references(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + description + websiteUrl + rank + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } +} +` + +type trustCenterReference struct { + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + WebsiteUrl *string `json:"websiteUrl"` + 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 trust center references", + Aliases: []string{"ls"}, + Example: ` # List references in the default organization + prb trust-center reference list + + # List references sorted by rank + prb trust-center ref 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", "NAME", "CREATED_AT", "UPDATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + refs, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[trustCenterReference], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenter *struct { + References api.Connection[trustCenterReference] `json:"references"` + } `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.References, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, refs) + } + + if len(refs) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No references found.") + return nil + } + + rows := make([][]string, 0, len(refs)) + for _, r := range refs { + website := "" + if r.WebsiteUrl != nil { + website = *r.WebsiteUrl + } + rows = append(rows, []string{ + r.ID, + r.Name, + website, + fmt.Sprintf("%d", r.Rank), + }) + } + + t := cmdutil.NewTable("ID", "NAME", "WEBSITE", "RANK").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(refs) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d references\n", + len(refs), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of references to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (RANK, NAME, 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/reference/reference.go b/pkg/cmd/trust-center/reference/reference.go new file mode 100644 index 000000000..25c7337b4 --- /dev/null +++ b/pkg/cmd/trust-center/reference/reference.go @@ -0,0 +1,37 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package reference + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/trust-center/reference/create" + "go.probo.inc/probo/pkg/cmd/trust-center/reference/delete" + "go.probo.inc/probo/pkg/cmd/trust-center/reference/list" +) + +func NewCmdReference(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "reference ", + Short: "Manage trust center references", + Aliases: []string{"ref"}, + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/trust-center/trust_center.go b/pkg/cmd/trust-center/trust_center.go new file mode 100644 index 000000000..91c7981ff --- /dev/null +++ b/pkg/cmd/trust-center/trust_center.go @@ -0,0 +1,39 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package trustcenter + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "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" + "go.probo.inc/probo/pkg/cmd/trust-center/view" +) + +func NewCmdTrustCenter(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "trust-center ", + Short: "Manage trust center", + Aliases: []string{"tc"}, + } + + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(reference.NewCmdReference(f)) + cmd.AddCommand(file.NewCmdFile(f)) + + return cmd +} diff --git a/pkg/cmd/trust-center/update/update.go b/pkg/cmd/trust-center/update/update.go new file mode 100644 index 000000000..77ec26117 --- /dev/null +++ b/pkg/cmd/trust-center/update/update.go @@ -0,0 +1,186 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 trustCenterQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Organization { + trustCenter { + id + } + } + } +} +` + +const updateMutation = ` +mutation($input: UpdateTrustCenterInput!) { + updateTrustCenter(input: $input) { + trustCenter { + id + active + searchEngineIndexing + } + } +} +` + +type trustCenterQueryResponse struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenter *struct { + ID string `json:"id"` + } `json:"trustCenter"` + } `json:"node"` +} + +type updateResponse struct { + UpdateTrustCenter struct { + TrustCenter struct { + ID string `json:"id"` + Active bool `json:"active"` + SearchEngineIndexing string `json:"searchEngineIndexing"` + } `json:"trustCenter"` + } `json:"updateTrustCenter"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagActive bool + flagSearchEngineIndexing string + ) + + cmd := &cobra.Command{ + Use: "update", + Short: "Update trust center settings", + Example: ` # Enable the trust center + prb trust-center update --active + + # Disable search engine indexing + prb trust-center update --search-engine-indexing NOT_INDEXABLE`, + Args: cobra.NoArgs, + 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'") + } + + // Fetch trust center ID from organization. + 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) + } + + input := map[string]any{ + "trustCenterId": tcResp.Node.TrustCenter.ID, + } + + if cmd.Flags().Changed("active") { + input["active"] = flagActive + } + if cmd.Flags().Changed("search-engine-indexing") { + if err := cmdutil.ValidateEnum("search-engine-indexing", flagSearchEngineIndexing, []string{"INDEXABLE", "NOT_INDEXABLE"}); err != nil { + return err + } + input["searchEngineIndexing"] = flagSearchEngineIndexing + } + + 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) + } + + tc := resp.UpdateTrustCenter.TrustCenter + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated trust center %s\n", + tc.ID, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().BoolVar(&flagActive, "active", false, "Enable or disable the trust center") + cmd.Flags().StringVar(&flagSearchEngineIndexing, "search-engine-indexing", "", "Search engine indexing: INDEXABLE, NOT_INDEXABLE") + + return cmd +} diff --git a/pkg/cmd/trust-center/view/view.go b/pkg/cmd/trust-center/view/view.go new file mode 100644 index 000000000..7a739fe6f --- /dev/null +++ b/pkg/cmd/trust-center/view/view.go @@ -0,0 +1,172 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Organization { + trustCenter { + id + active + searchEngineIndexing + logoFileUrl + darkLogoFileUrl + ndaFileName + ndaFileUrl + createdAt + updatedAt + } + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + TrustCenter *struct { + ID string `json:"id"` + Active bool `json:"active"` + SearchEngineIndexing string `json:"searchEngineIndexing"` + LogoFileUrl *string `json:"logoFileUrl"` + DarkLogoFileUrl *string `json:"darkLogoFileUrl"` + NdaFileName *string `json:"ndaFileName"` + NdaFileUrl *string `json:"ndaFileUrl"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"trustCenter"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "view", + Short: "View trust center settings", + 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'") + } + + data, err := client.Do( + viewQuery, + map[string]any{"id": flagOrg}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("organization %s not found", flagOrg) + } + + if resp.Node.Typename != "Organization" { + return fmt.Errorf("expected Organization node, got %s", resp.Node.Typename) + } + + if resp.Node.TrustCenter == nil { + return fmt.Errorf("trust center not found for organization %s", flagOrg) + } + + tc := resp.Node.TrustCenter + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, tc) + } + + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(28) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render("Trust Center")) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), tc.ID) + _, _ = fmt.Fprintf(out, "%s%v\n", label.Render("Active:"), tc.Active) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Search Engine Indexing:"), tc.SearchEngineIndexing) + + if tc.NdaFileName != nil && *tc.NdaFileName != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("NDA File:"), *tc.NdaFileName) + } + + if tc.LogoFileUrl != nil && *tc.LogoFileUrl != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Logo URL:"), *tc.LogoFileUrl) + } + + if tc.DarkLogoFileUrl != nil && *tc.DarkLogoFileUrl != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Dark Logo URL:"), *tc.DarkLogoFileUrl) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(tc.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(tc.UpdatedAt)) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/cmd/vendormgmt/create/create.go b/pkg/cmd/vendormgmt/create/create.go new file mode 100644 index 000000000..0e702e1e2 --- /dev/null +++ b/pkg/cmd/vendormgmt/create/create.go @@ -0,0 +1,205 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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 createMutation = ` +mutation($input: CreateVendorInput!) { + createVendor(input: $input) { + vendorEdge { + node { + id + name + category + } + } + } +} +` + +type createResponse struct { + CreateVendor struct { + VendorEdge struct { + Node struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + } `json:"node"` + } `json:"vendorEdge"` + } `json:"createVendor"` +} + +func NewCmdCreate(f *cmdutil.Factory) *cobra.Command { + var ( + flagOrg string + flagName string + flagCategory string + flagDescription string + flagLegalName string + flagAddress string + flagWebsite string + ) + + cmd := &cobra.Command{ + Use: "create", + Short: "Create a new vendor", + Example: ` # Create a vendor interactively + prb vendor create + + # Create a vendor non-interactively + prb vendor create --name "Acme Corp" --category CLOUD_PROVIDER`, + 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'") + } + + if f.IOStreams.IsInteractive() { + if flagName == "" { + err := huh.NewInput(). + Title("Vendor name"). + Value(&flagName). + Run() + if err != nil { + return err + } + } + + if flagCategory == "" { + err := huh.NewSelect[string](). + Title("Vendor category"). + Options( + huh.NewOption("Analytics", "ANALYTICS"), + huh.NewOption("Cloud Monitoring", "CLOUD_MONITORING"), + huh.NewOption("Cloud Provider", "CLOUD_PROVIDER"), + huh.NewOption("Collaboration", "COLLABORATION"), + huh.NewOption("Customer Support", "CUSTOMER_SUPPORT"), + huh.NewOption("Data Storage and Processing", "DATA_STORAGE_AND_PROCESSING"), + huh.NewOption("Document Management", "DOCUMENT_MANAGEMENT"), + huh.NewOption("Employee Management", "EMPLOYEE_MANAGEMENT"), + huh.NewOption("Engineering", "ENGINEERING"), + huh.NewOption("Finance", "FINANCE"), + huh.NewOption("Identity Provider", "IDENTITY_PROVIDER"), + huh.NewOption("IT", "IT"), + huh.NewOption("Marketing", "MARKETING"), + huh.NewOption("Office Operations", "OFFICE_OPERATIONS"), + huh.NewOption("Other", "OTHER"), + huh.NewOption("Password Management", "PASSWORD_MANAGEMENT"), + huh.NewOption("Product and Design", "PRODUCT_AND_DESIGN"), + huh.NewOption("Professional Services", "PROFESSIONAL_SERVICES"), + huh.NewOption("Recruiting", "RECRUITING"), + huh.NewOption("Sales", "SALES"), + huh.NewOption("Security", "SECURITY"), + huh.NewOption("Version Control", "VERSION_CONTROL"), + ). + Value(&flagCategory). + Run() + if err != nil { + return err + } + } + } + + if flagName == "" { + return fmt.Errorf("name is required; pass --name or run interactively") + } + if flagCategory == "" { + return fmt.Errorf("category is required; pass --category or run interactively") + } + + input := map[string]any{ + "organizationId": flagOrg, + "name": flagName, + "category": flagCategory, + } + + if flagDescription != "" { + input["description"] = flagDescription + } + if flagLegalName != "" { + input["legalName"] = flagLegalName + } + if flagAddress != "" { + input["headquarterAddress"] = flagAddress + } + if flagWebsite != "" { + input["websiteUrl"] = flagWebsite + } + + data, err := client.Do( + createMutation, + map[string]any{"input": input}, + ) + if err != nil { + return err + } + + var resp createResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + v := resp.CreateVendor.VendorEdge.Node + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Created vendor %s (%s)\n", + v.ID, + v.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().StringVar(&flagName, "name", "", "Vendor name (required)") + cmd.Flags().StringVar(&flagCategory, "category", "", "Vendor category (required)") + cmd.Flags().StringVar(&flagDescription, "description", "", "Vendor description") + cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name") + cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address") + cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL") + + return cmd +} diff --git a/pkg/cmd/vendormgmt/delete/delete.go b/pkg/cmd/vendormgmt/delete/delete.go new file mode 100644 index 000000000..1137039ce --- /dev/null +++ b/pkg/cmd/vendormgmt/delete/delete.go @@ -0,0 +1,103 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: DeleteVendorInput!) { + deleteVendor(input: $input) { + deletedVendorId + } +} +` + +func NewCmdDelete(f *cmdutil.Factory) *cobra.Command { + var flagYes bool + + cmd := &cobra.Command{ + Use: "delete ", + Short: "Delete a vendor", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + if !flagYes { + if !f.IOStreams.IsInteractive() { + return fmt.Errorf("cannot delete vendor: confirmation required, use --yes to confirm") + } + + var confirmed bool + err := huh.NewConfirm(). + Title(fmt.Sprintf("Delete vendor %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{ + "vendorId": args[0], + }, + }, + ) + if err != nil { + return err + } + + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Deleted vendor %s\n", + args[0], + ) + + return nil + }, + } + + cmd.Flags().BoolVarP(&flagYes, "yes", "y", false, "Skip confirmation prompt") + + return cmd +} diff --git a/pkg/cmd/vendormgmt/list/list.go b/pkg/cmd/vendormgmt/list/list.go new file mode 100644 index 000000000..10ab27cac --- /dev/null +++ b/pkg/cmd/vendormgmt/list/list.go @@ -0,0 +1,190 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: VendorOrder) { + node(id: $id) { + __typename + ... on Organization { + vendors(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + name + category + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type vendor struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` +} + +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 vendors in an organization", + Aliases: []string{"ls"}, + Example: ` # List vendors in the default organization + prb vendor list + + # List vendors sorted by name + prb vendor ls --order-by NAME --json`, + 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{"NAME", "CREATED_AT", "UPDATED_AT"}); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + vendors, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[vendor], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Vendors api.Connection[vendor] `json:"vendors"` + } `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) + } + return &resp.Node.Vendors, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, vendors) + } + + if len(vendors) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No vendors found.") + return nil + } + + rows := make([][]string, 0, len(vendors)) + for _, v := range vendors { + rows = append(rows, []string{ + v.ID, + v.Name, + v.Category, + }) + } + + t := cmdutil.NewTable("ID", "NAME", "CATEGORY").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(vendors) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d vendors\n", + len(vendors), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().StringVar(&flagOrg, "org", "", "Organization ID") + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of vendors to list") + cmd.Flags().StringVar(&flagOrderBy, "order-by", "", "Order by field (NAME, 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/vendormgmt/update/update.go b/pkg/cmd/vendormgmt/update/update.go new file mode 100644 index 000000000..9be5da251 --- /dev/null +++ b/pkg/cmd/vendormgmt/update/update.go @@ -0,0 +1,141 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS 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: UpdateVendorInput!) { + updateVendor(input: $input) { + vendor { + id + name + category + } + } +} +` + +type updateResponse struct { + UpdateVendor struct { + Vendor struct { + ID string `json:"id"` + Name string `json:"name"` + Category string `json:"category"` + } `json:"vendor"` + } `json:"updateVendor"` +} + +func NewCmdUpdate(f *cmdutil.Factory) *cobra.Command { + var ( + flagName string + flagDescription string + flagCategory string + flagLegalName string + flagAddress string + flagWebsite string + ) + + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a vendor", + 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("name") { + input["name"] = flagName + } + if cmd.Flags().Changed("description") { + input["description"] = flagDescription + } + if cmd.Flags().Changed("category") { + input["category"] = flagCategory + } + if cmd.Flags().Changed("legal-name") { + input["legalName"] = flagLegalName + } + if cmd.Flags().Changed("address") { + input["headquarterAddress"] = flagAddress + } + if cmd.Flags().Changed("website") { + input["websiteUrl"] = flagWebsite + } + + 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) + } + + v := resp.UpdateVendor.Vendor + _, _ = fmt.Fprintf( + f.IOStreams.Out, + "Updated vendor %s (%s)\n", + v.ID, + v.Name, + ) + + return nil + }, + } + + cmd.Flags().StringVar(&flagName, "name", "", "Vendor name") + cmd.Flags().StringVar(&flagDescription, "description", "", "Vendor description") + cmd.Flags().StringVar(&flagCategory, "category", "", "Vendor category") + cmd.Flags().StringVar(&flagLegalName, "legal-name", "", "Legal name") + cmd.Flags().StringVar(&flagAddress, "address", "", "Headquarter address") + cmd.Flags().StringVar(&flagWebsite, "website", "", "Website URL") + + return cmd +} diff --git a/pkg/cmd/vendormgmt/vendormgmt.go b/pkg/cmd/vendormgmt/vendormgmt.go new file mode 100644 index 000000000..a21ab1716 --- /dev/null +++ b/pkg/cmd/vendormgmt/vendormgmt.go @@ -0,0 +1,40 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package vendormgmt + +import ( + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cmd/cmdutil" + "go.probo.inc/probo/pkg/cmd/vendormgmt/create" + "go.probo.inc/probo/pkg/cmd/vendormgmt/delete" + "go.probo.inc/probo/pkg/cmd/vendormgmt/list" + "go.probo.inc/probo/pkg/cmd/vendormgmt/update" + "go.probo.inc/probo/pkg/cmd/vendormgmt/view" +) + +func NewCmdVendor(f *cmdutil.Factory) *cobra.Command { + cmd := &cobra.Command{ + Use: "vendor ", + Short: "Manage vendors", + } + + cmd.AddCommand(list.NewCmdList(f)) + cmd.AddCommand(create.NewCmdCreate(f)) + cmd.AddCommand(view.NewCmdView(f)) + cmd.AddCommand(update.NewCmdUpdate(f)) + cmd.AddCommand(delete.NewCmdDelete(f)) + + return cmd +} diff --git a/pkg/cmd/vendormgmt/view/view.go b/pkg/cmd/vendormgmt/view/view.go new file mode 100644 index 000000000..c2371e41c --- /dev/null +++ b/pkg/cmd/vendormgmt/view/view.go @@ -0,0 +1,154 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package view + +import ( + "encoding/json" + "fmt" + + "github.com/charmbracelet/lipgloss" + "github.com/spf13/cobra" + "go.probo.inc/probo/pkg/cli/api" + "go.probo.inc/probo/pkg/cmd/cmdutil" +) + +const viewQuery = ` +query($id: ID!) { + node(id: $id) { + __typename + ... on Vendor { + id + name + description + category + legalName + headquarterAddress + websiteUrl + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Name string `json:"name"` + Description *string `json:"description"` + Category string `json:"category"` + LegalName *string `json:"legalName"` + HeadquarterAddress *string `json:"headquarterAddress"` + WebsiteUrl *string `json:"websiteUrl"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdView(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view ", + Short: "View a vendor", + Args: cobra.ExactArgs(1), + 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), + ) + + data, err := client.Do( + viewQuery, + map[string]any{"id": args[0]}, + ) + if err != nil { + return err + } + + var resp viewResponse + if err := json.Unmarshal(data, &resp); err != nil { + return fmt.Errorf("cannot parse response: %w", err) + } + + if resp.Node == nil { + return fmt.Errorf("vendor %s not found", args[0]) + } + + if resp.Node.Typename != "Vendor" { + return fmt.Errorf("expected Vendor node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + v := resp.Node + out := f.IOStreams.Out + + bold := lipgloss.NewStyle().Bold(true) + label := lipgloss.NewStyle().Foreground(lipgloss.Color("242")).Width(22) + + _, _ = fmt.Fprintf(out, "%s\n\n", bold.Render(v.Name)) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), v.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Category:"), v.Category) + + if v.Description != nil && *v.Description != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Description:"), *v.Description) + } + + if v.LegalName != nil && *v.LegalName != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Legal Name:"), *v.LegalName) + } + + if v.HeadquarterAddress != nil && *v.HeadquarterAddress != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Address:"), *v.HeadquarterAddress) + } + + if v.WebsiteUrl != nil && *v.WebsiteUrl != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Website:"), *v.WebsiteUrl) + } + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(v.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(v.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index 96855578e..c1e2fa3f9 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -4032,3 +4032,734 @@ func (r *Resolver) PublishAssetListTool(ctx context.Context, req *mcp.CallToolRe DocumentVersionID: documentVersion.ID, }, nil } + +// ListVendorContactsTool handles the listVendorContacts tool +// List all contacts for a vendor +func (r *Resolver) ListVendorContactsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListVendorContactsInput) (*mcp.CallToolResult, types.ListVendorContactsOutput, error) { + r.MustAuthorize(ctx, input.VendorID, probo.ActionVendorContactList) + + prb := r.ProboService(ctx, input.VendorID) + + pageOrderBy := page.OrderBy[coredata.VendorContactOrderField]{ + Field: coredata.VendorContactOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.VendorContactOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := prb.VendorContacts.List(ctx, input.VendorID, cursor) + if err != nil { + return nil, types.ListVendorContactsOutput{}, fmt.Errorf("cannot list vendor contacts: %w", err) + } + + return nil, types.NewListVendorContactsOutput(p), nil +} + +// AddVendorContactTool handles the addVendorContact tool +// Add a new contact to a vendor +func (r *Resolver) AddVendorContactTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddVendorContactInput) (*mcp.CallToolResult, types.AddVendorContactOutput, error) { + r.MustAuthorize(ctx, input.VendorID, probo.ActionVendorContactCreate) + + prb := r.ProboService(ctx, input.VendorID) + + emailAddr, err := mail.ParseAddr(input.Email) + if err != nil { + return nil, types.AddVendorContactOutput{}, fmt.Errorf("invalid email address: %w", err) + } + + vendorContact, err := prb.VendorContacts.Create(ctx, probo.CreateVendorContactRequest{ + VendorID: input.VendorID, + FullName: &input.FullName, + Email: &emailAddr, + Phone: &input.Phone, + Role: &input.Role, + }) + if err != nil { + return nil, types.AddVendorContactOutput{}, fmt.Errorf("cannot create vendor contact: %w", err) + } + + return nil, types.AddVendorContactOutput{ + VendorContact: types.NewVendorContact(vendorContact), + }, nil +} + +// UpdateVendorContactTool handles the updateVendorContact tool +// Update an existing vendor contact +func (r *Resolver) UpdateVendorContactTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateVendorContactInput) (*mcp.CallToolResult, types.UpdateVendorContactOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionVendorContactUpdate) + + prb := r.ProboService(ctx, input.ID) + + updateReq := probo.UpdateVendorContactRequest{ + ID: input.ID, + } + + if input.FullName != nil { + updateReq.FullName = &input.FullName + } + + if input.Email != nil { + emailAddr, err := mail.ParseAddr(*input.Email) + if err != nil { + return nil, types.UpdateVendorContactOutput{}, fmt.Errorf("invalid email address: %w", err) + } + emailPtr := &emailAddr + updateReq.Email = &emailPtr + } + + if input.Phone != nil { + updateReq.Phone = &input.Phone + } + + if input.Role != nil { + updateReq.Role = &input.Role + } + + vendorContact, err := prb.VendorContacts.Update(ctx, updateReq) + if err != nil { + return nil, types.UpdateVendorContactOutput{}, fmt.Errorf("cannot update vendor contact: %w", err) + } + + return nil, types.UpdateVendorContactOutput{ + VendorContact: types.NewVendorContact(vendorContact), + }, nil +} + +// DeleteVendorContactTool handles the deleteVendorContact tool +// Delete a vendor contact +func (r *Resolver) DeleteVendorContactTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteVendorContactInput) (*mcp.CallToolResult, types.DeleteVendorContactOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionVendorContactDelete) + + prb := r.ProboService(ctx, input.ID) + + err := prb.VendorContacts.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteVendorContactOutput{}, fmt.Errorf("cannot delete vendor contact: %w", err) + } + + return nil, types.DeleteVendorContactOutput{ + DeletedVendorContactID: input.ID, + }, nil +} + +// ListVendorServicesTool handles the listVendorServices tool +// List all services for a vendor +func (r *Resolver) ListVendorServicesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListVendorServicesInput) (*mcp.CallToolResult, types.ListVendorServicesOutput, error) { + r.MustAuthorize(ctx, input.VendorID, probo.ActionVendorServiceList) + + prb := r.ProboService(ctx, input.VendorID) + + pageOrderBy := page.OrderBy[coredata.VendorServiceOrderField]{ + Field: coredata.VendorServiceOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.VendorServiceOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := prb.VendorServices.List(ctx, input.VendorID, cursor) + if err != nil { + return nil, types.ListVendorServicesOutput{}, fmt.Errorf("cannot list vendor services: %w", err) + } + + return nil, types.NewListVendorServicesOutput(p), nil +} + +// AddVendorServiceTool handles the addVendorService tool +// Add a new service to a vendor +func (r *Resolver) AddVendorServiceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddVendorServiceInput) (*mcp.CallToolResult, types.AddVendorServiceOutput, error) { + r.MustAuthorize(ctx, input.VendorID, probo.ActionVendorServiceCreate) + + prb := r.ProboService(ctx, input.VendorID) + + vendorService, err := prb.VendorServices.Create(ctx, probo.CreateVendorServiceRequest{ + VendorID: input.VendorID, + Name: input.Name, + Description: input.Description, + }) + if err != nil { + return nil, types.AddVendorServiceOutput{}, fmt.Errorf("cannot create vendor service: %w", err) + } + + return nil, types.AddVendorServiceOutput{ + VendorService: types.NewVendorService(vendorService), + }, nil +} + +// UpdateVendorServiceTool handles the updateVendorService tool +// Update an existing vendor service +func (r *Resolver) UpdateVendorServiceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateVendorServiceInput) (*mcp.CallToolResult, types.UpdateVendorServiceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionVendorServiceUpdate) + + prb := r.ProboService(ctx, input.ID) + + updateReq := probo.UpdateVendorServiceRequest{ + ID: input.ID, + } + + if input.Name != nil { + updateReq.Name = input.Name + } + + if input.Description != nil { + updateReq.Description = &input.Description + } + + vendorService, err := prb.VendorServices.Update(ctx, updateReq) + if err != nil { + return nil, types.UpdateVendorServiceOutput{}, fmt.Errorf("cannot update vendor service: %w", err) + } + + return nil, types.UpdateVendorServiceOutput{ + VendorService: types.NewVendorService(vendorService), + }, nil +} + +// DeleteVendorServiceTool handles the deleteVendorService tool +// Delete a vendor service +func (r *Resolver) DeleteVendorServiceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteVendorServiceInput) (*mcp.CallToolResult, types.DeleteVendorServiceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionVendorServiceDelete) + + prb := r.ProboService(ctx, input.ID) + + err := prb.VendorServices.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteVendorServiceOutput{}, fmt.Errorf("cannot delete vendor service: %w", err) + } + + return nil, types.DeleteVendorServiceOutput{ + DeletedVendorServiceID: input.ID, + }, nil +} +func (r *Resolver) DeleteAssetTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteAssetInput) (*mcp.CallToolResult, types.DeleteAssetOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionAssetDelete) + + svc := r.ProboService(ctx, input.ID) + + err := svc.Assets.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteAssetOutput{}, fmt.Errorf("failed to delete asset: %w", err) + } + + return nil, types.DeleteAssetOutput{ + DeletedAssetID: input.ID, + }, nil +} +func (r *Resolver) DeleteDatumTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteDatumInput) (*mcp.CallToolResult, types.DeleteDatumOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionDatumDelete) + + svc := r.ProboService(ctx, input.ID) + + err := svc.Data.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteDatumOutput{}, fmt.Errorf("failed to delete datum: %w", err) + } + + return nil, types.DeleteDatumOutput{ + DeletedDatumID: input.ID, + }, nil +} +func (r *Resolver) DeleteObligationTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteObligationInput) (*mcp.CallToolResult, types.DeleteObligationOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionObligationDelete) + + svc := r.ProboService(ctx, input.ID) + + err := svc.Obligations.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteObligationOutput{}, fmt.Errorf("failed to delete obligation: %w", err) + } + + return nil, types.DeleteObligationOutput{ + DeletedObligationID: input.ID, + }, nil +} +func (r *Resolver) DeleteAuditTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteAuditInput) (*mcp.CallToolResult, types.DeleteAuditOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionAuditDelete) + + svc := r.ProboService(ctx, input.ID) + + err := svc.Audits.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteAuditOutput{}, fmt.Errorf("failed to delete audit: %w", err) + } + + return nil, types.DeleteAuditOutput{ + DeletedAuditID: input.ID, + }, nil +} +func (r *Resolver) ListRightsRequestsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListRightsRequestsInput) (*mcp.CallToolResult, types.ListRightsRequestsOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionRightsRequestList) + + prb := r.ProboService(ctx, input.OrganizationID) + + pageOrderBy := page.OrderBy[coredata.RightsRequestOrderField]{ + Field: coredata.RightsRequestOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.RightsRequestOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + page, err := prb.RightsRequests.ListForOrganizationID(ctx, input.OrganizationID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list organization rights requests: %w", err)) + } + + return nil, types.NewListRightsRequestsOutput(page), nil +} +func (r *Resolver) GetRightsRequestTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetRightsRequestInput) (*mcp.CallToolResult, types.GetRightsRequestOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRightsRequestGet) + + prb := r.ProboService(ctx, input.ID) + + rightsRequest, err := prb.RightsRequests.Get(ctx, input.ID) + if err != nil { + return nil, types.GetRightsRequestOutput{}, fmt.Errorf("failed to get rights request: %w", err) + } + + return nil, types.GetRightsRequestOutput{ + RightsRequest: types.NewRightsRequest(rightsRequest), + }, nil +} +func (r *Resolver) AddRightsRequestTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddRightsRequestInput) (*mcp.CallToolResult, types.AddRightsRequestOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionRightsRequestCreate) + + svc := r.ProboService(ctx, input.OrganizationID) + + rightsRequest, err := svc.RightsRequests.Create( + ctx, + &probo.CreateRightsRequestRequest{ + OrganizationID: input.OrganizationID, + RequestType: &input.RequestType, + RequestState: &input.RequestState, + DataSubject: &input.DataSubject, + Contact: input.Contact, + Details: input.Details, + Deadline: input.Deadline, + ActionTaken: input.ActionTaken, + }, + ) + if err != nil { + return nil, types.AddRightsRequestOutput{}, fmt.Errorf("failed to create rights request: %w", err) + } + + return nil, types.AddRightsRequestOutput{ + RightsRequest: types.NewRightsRequest(rightsRequest), + }, nil +} +func (r *Resolver) UpdateRightsRequestTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateRightsRequestInput) (*mcp.CallToolResult, types.UpdateRightsRequestOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRightsRequestUpdate) + + svc := r.ProboService(ctx, input.ID) + + var dataSubject **string + if input.DataSubject != nil { + dataSubject = &input.DataSubject + } + + rightsRequest, err := svc.RightsRequests.Update( + ctx, + &probo.UpdateRightsRequestRequest{ + ID: input.ID, + RequestType: input.RequestType, + RequestState: input.RequestState, + DataSubject: dataSubject, + Contact: UnwrapOmittable(input.Contact), + Details: UnwrapOmittable(input.Details), + Deadline: UnwrapOmittable(input.Deadline), + ActionTaken: UnwrapOmittable(input.ActionTaken), + }, + ) + if err != nil { + return nil, types.UpdateRightsRequestOutput{}, fmt.Errorf("failed to update rights request: %w", err) + } + + return nil, types.UpdateRightsRequestOutput{ + RightsRequest: types.NewRightsRequest(rightsRequest), + }, nil +} +func (r *Resolver) DeleteRightsRequestTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteRightsRequestInput) (*mcp.CallToolResult, types.DeleteRightsRequestOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionRightsRequestDelete) + + svc := r.ProboService(ctx, input.ID) + + err := svc.RightsRequests.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteRightsRequestOutput{}, fmt.Errorf("failed to delete rights request: %w", err) + } + + return nil, types.DeleteRightsRequestOutput{ + DeletedRightsRequestID: input.ID, + }, nil +} + +// GetTrustCenterTool handles the getTrustCenter tool +// Get the trust center for an organization +func (r *Resolver) GetTrustCenterTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetTrustCenterInput) (*mcp.CallToolResult, types.GetTrustCenterOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionTrustCenterGet) + + prb := r.ProboService(ctx, input.OrganizationID) + + trustCenter, err := prb.TrustCenters.GetByOrganizationID(ctx, input.OrganizationID) + if err != nil { + return nil, types.GetTrustCenterOutput{}, fmt.Errorf("cannot get trust center: %w", err) + } + + tc := types.NewTrustCenter(trustCenter) + + logoURL, err := prb.TrustCenters.GenerateLogoURL(ctx, trustCenter.ID, 1*time.Hour) + if err == nil { + tc.LogoFileURL = logoURL + } + + darkLogoURL, err := prb.TrustCenters.GenerateDarkLogoURL(ctx, trustCenter.ID, 1*time.Hour) + if err == nil { + tc.DarkLogoFileURL = darkLogoURL + } + + ndaFileURL, err := prb.TrustCenters.GenerateNDAFileURL(ctx, trustCenter.ID, 15*time.Minute) + if err == nil { + tc.NdaFileURL = ndaFileURL + } + + return nil, types.GetTrustCenterOutput{TrustCenter: tc}, nil +} + +// UpdateTrustCenterTool handles the updateTrustCenter tool +// Update the trust center settings +func (r *Resolver) UpdateTrustCenterTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateTrustCenterInput) (*mcp.CallToolResult, types.UpdateTrustCenterOutput, error) { + r.MustAuthorize(ctx, input.TrustCenterID, probo.ActionTrustCenterUpdate) + + prb := r.ProboService(ctx, input.TrustCenterID) + + updateReq := &probo.UpdateTrustCenterRequest{ + ID: input.TrustCenterID, + } + if active := UnwrapOmittable(input.Active); active != nil { + updateReq.Active = *active + } + if sei := UnwrapOmittable(input.SearchEngineIndexing); sei != nil { + updateReq.SearchEngineIndexing = *sei + } + + trustCenter, _, err := prb.TrustCenters.Update(ctx, updateReq) + if err != nil { + return nil, types.UpdateTrustCenterOutput{}, fmt.Errorf("cannot update trust center: %w", err) + } + + return nil, types.UpdateTrustCenterOutput{TrustCenter: types.NewTrustCenter(trustCenter)}, nil +} + +// ListTrustCenterReferencesTool handles the listTrustCenterReferences tool +// List all references for a trust center +func (r *Resolver) ListTrustCenterReferencesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListTrustCenterReferencesInput) (*mcp.CallToolResult, types.ListTrustCenterReferencesOutput, error) { + r.MustAuthorize(ctx, input.TrustCenterID, probo.ActionTrustCenterReferenceList) + + prb := r.ProboService(ctx, input.TrustCenterID) + + pageOrderBy := page.OrderBy[coredata.TrustCenterReferenceOrderField]{ + Field: coredata.TrustCenterReferenceOrderFieldRank, + Direction: page.OrderDirectionAsc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.TrustCenterReferenceOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := prb.TrustCenterReferences.ListForTrustCenterID(ctx, input.TrustCenterID, cursor) + if err != nil { + return nil, types.ListTrustCenterReferencesOutput{}, fmt.Errorf("cannot list trust center references: %w", err) + } + + return nil, types.NewListTrustCenterReferencesOutput(p), nil +} + +// AddTrustCenterReferenceTool handles the addTrustCenterReference tool +// Add a new reference to the trust center +func (r *Resolver) AddTrustCenterReferenceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddTrustCenterReferenceInput) (*mcp.CallToolResult, types.AddTrustCenterReferenceOutput, error) { + r.MustAuthorize(ctx, input.TrustCenterID, probo.ActionTrustCenterReferenceCreate) + + prb := r.ProboService(ctx, input.TrustCenterID) + + var websiteURL string + if input.WebsiteURL != nil { + websiteURL = *input.WebsiteURL + } + + reference, err := prb.TrustCenterReferences.Create( + ctx, + &probo.CreateTrustCenterReferenceRequest{ + TrustCenterID: input.TrustCenterID, + Name: input.Name, + Description: input.Description, + WebsiteURL: websiteURL, + }, + ) + if err != nil { + return nil, types.AddTrustCenterReferenceOutput{}, fmt.Errorf("cannot add trust center reference: %w", err) + } + + return nil, types.AddTrustCenterReferenceOutput{TrustCenterReference: types.NewTrustCenterReference(reference)}, nil +} + +// UpdateTrustCenterReferenceTool handles the updateTrustCenterReference tool +// Update a trust center reference +func (r *Resolver) UpdateTrustCenterReferenceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateTrustCenterReferenceInput) (*mcp.CallToolResult, types.UpdateTrustCenterReferenceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionTrustCenterReferenceUpdate) + + prb := r.ProboService(ctx, input.ID) + + updateRefReq := &probo.UpdateTrustCenterReferenceRequest{ + ID: input.ID, + Description: UnwrapOmittable(input.Description), + } + if name := UnwrapOmittable(input.Name); name != nil { + updateRefReq.Name = *name + } + if websiteURL := UnwrapOmittable(input.WebsiteURL); websiteURL != nil { + updateRefReq.WebsiteURL = *websiteURL + } + if rank := UnwrapOmittable(input.Rank); rank != nil { + updateRefReq.Rank = *rank + } + + reference, err := prb.TrustCenterReferences.Update(ctx, updateRefReq) + if err != nil { + return nil, types.UpdateTrustCenterReferenceOutput{}, fmt.Errorf("cannot update trust center reference: %w", err) + } + + return nil, types.UpdateTrustCenterReferenceOutput{TrustCenterReference: types.NewTrustCenterReference(reference)}, nil +} + +// DeleteTrustCenterReferenceTool handles the deleteTrustCenterReference tool +// Delete a trust center reference +func (r *Resolver) DeleteTrustCenterReferenceTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteTrustCenterReferenceInput) (*mcp.CallToolResult, types.DeleteTrustCenterReferenceOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionTrustCenterReferenceDelete) + + prb := r.ProboService(ctx, input.ID) + + err := prb.TrustCenterReferences.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteTrustCenterReferenceOutput{}, fmt.Errorf("cannot delete trust center reference: %w", err) + } + + return nil, types.DeleteTrustCenterReferenceOutput{DeletedTrustCenterReferenceID: input.ID}, nil +} + +// ListTrustCenterFilesTool handles the listTrustCenterFiles tool +// List all files for the trust center +func (r *Resolver) ListTrustCenterFilesTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListTrustCenterFilesInput) (*mcp.CallToolResult, types.ListTrustCenterFilesOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionTrustCenterFileList) + + prb := r.ProboService(ctx, input.OrganizationID) + + pageOrderBy := page.OrderBy[coredata.TrustCenterFileOrderField]{ + Field: coredata.TrustCenterFileOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.TrustCenterFileOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + filter := coredata.NewTrustCenterFileFilter() + + p, err := prb.TrustCenterFiles.ListForOrganizationID(ctx, input.OrganizationID, cursor, filter) + if err != nil { + return nil, types.ListTrustCenterFilesOutput{}, fmt.Errorf("cannot list trust center files: %w", err) + } + + files := make([]*types.TrustCenterFile, 0, len(p.Data)) + for _, f := range p.Data { + fileURL, err := prb.TrustCenterFiles.GenerateFileURL(ctx, f.ID, 1*time.Hour) + if err != nil { + return nil, types.ListTrustCenterFilesOutput{}, fmt.Errorf("cannot generate file URL: %w", err) + } + files = append(files, types.NewTrustCenterFile(f, fileURL)) + } + + return nil, types.NewListTrustCenterFilesOutput(files, p), nil +} + +// DeleteTrustCenterFileTool handles the deleteTrustCenterFile tool +// Delete a trust center file +func (r *Resolver) DeleteTrustCenterFileTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteTrustCenterFileInput) (*mcp.CallToolResult, types.DeleteTrustCenterFileOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionTrustCenterFileDelete) + + prb := r.ProboService(ctx, input.ID) + + err := prb.TrustCenterFiles.Delete(ctx, input.ID) + if err != nil { + return nil, types.DeleteTrustCenterFileOutput{}, fmt.Errorf("cannot delete trust center file: %w", err) + } + + return nil, types.DeleteTrustCenterFileOutput{DeletedTrustCenterFileID: input.ID}, nil +} + +// ListComplianceExternalURLsTool handles the listComplianceExternalURLs tool +// List all external URLs for a trust center +func (r *Resolver) ListComplianceExternalURLsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListComplianceExternalURLsInput) (*mcp.CallToolResult, types.ListComplianceExternalURLsOutput, error) { + r.MustAuthorize(ctx, input.TrustCenterID, probo.ActionComplianceExternalURLList) + + prb := r.ProboService(ctx, input.TrustCenterID) + + pageOrderBy := page.OrderBy[coredata.ComplianceExternalURLOrderField]{ + Field: coredata.ComplianceExternalURLOrderFieldRank, + Direction: page.OrderDirectionAsc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.ComplianceExternalURLOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := prb.ComplianceExternalURLs.List(ctx, input.TrustCenterID, cursor) + if err != nil { + return nil, types.ListComplianceExternalURLsOutput{}, fmt.Errorf("cannot list compliance external URLs: %w", err) + } + + return nil, types.NewListComplianceExternalURLsOutput(p), nil +} + +// AddComplianceExternalURLTool handles the addComplianceExternalURL tool +// Add a new external URL to the trust center +func (r *Resolver) AddComplianceExternalURLTool(ctx context.Context, req *mcp.CallToolRequest, input *types.AddComplianceExternalURLInput) (*mcp.CallToolResult, types.AddComplianceExternalURLOutput, error) { + r.MustAuthorize(ctx, input.TrustCenterID, probo.ActionComplianceExternalURLCreate) + + prb := r.ProboService(ctx, input.TrustCenterID) + + item, err := prb.ComplianceExternalURLs.Create( + ctx, + &probo.CreateComplianceExternalURLRequest{ + TrustCenterID: input.TrustCenterID, + Name: input.Name, + URL: input.URL, + }, + ) + if err != nil { + return nil, types.AddComplianceExternalURLOutput{}, fmt.Errorf("cannot add compliance external URL: %w", err) + } + + return nil, types.AddComplianceExternalURLOutput{ComplianceExternalURL: types.NewComplianceExternalURL(item)}, nil +} + +// UpdateComplianceExternalURLTool handles the updateComplianceExternalURL tool +// Update a compliance external URL +func (r *Resolver) UpdateComplianceExternalURLTool(ctx context.Context, req *mcp.CallToolRequest, input *types.UpdateComplianceExternalURLInput) (*mcp.CallToolResult, types.UpdateComplianceExternalURLOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionComplianceExternalURLUpdate) + + prb := r.ProboService(ctx, input.ID) + + updateURLReq := &probo.UpdateComplianceExternalURLRequest{ + ID: input.ID, + } + + if name := UnwrapOmittable(input.Name); name != nil && *name != nil { + updateURLReq.Name = **name + } + if u := UnwrapOmittable(input.URL); u != nil && *u != nil { + updateURLReq.URL = **u + } + if rank := UnwrapOmittable(input.Rank); rank != nil { + updateURLReq.Rank = *rank + } + + item, err := prb.ComplianceExternalURLs.Update(ctx, updateURLReq) + if err != nil { + return nil, types.UpdateComplianceExternalURLOutput{}, fmt.Errorf("cannot update compliance external URL: %w", err) + } + + return nil, types.UpdateComplianceExternalURLOutput{ComplianceExternalURL: types.NewComplianceExternalURL(item)}, nil +} + +// DeleteComplianceExternalURLTool handles the deleteComplianceExternalURL tool +// Delete a compliance external URL +func (r *Resolver) DeleteComplianceExternalURLTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteComplianceExternalURLInput) (*mcp.CallToolResult, types.DeleteComplianceExternalURLOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionComplianceExternalURLDelete) + + prb := r.ProboService(ctx, input.ID) + + err := prb.ComplianceExternalURLs.Delete( + ctx, + &probo.DeleteComplianceExternalURLRequest{ + ID: input.ID, + }, + ) + if err != nil { + return nil, types.DeleteComplianceExternalURLOutput{}, fmt.Errorf("cannot delete compliance external URL: %w", err) + } + + return nil, types.DeleteComplianceExternalURLOutput{DeletedComplianceExternalURLID: input.ID}, nil +} + +// CreateCustomDomainTool handles the createCustomDomain tool +// Create a custom domain for the organization +func (r *Resolver) CreateCustomDomainTool(ctx context.Context, req *mcp.CallToolRequest, input *types.CreateCustomDomainInput) (*mcp.CallToolResult, types.CreateCustomDomainOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionCustomDomainCreate) + + prb := r.ProboService(ctx, input.OrganizationID) + + domain, err := prb.CustomDomains.CreateCustomDomain( + ctx, + probo.CreateCustomDomainRequest{ + OrganizationID: input.OrganizationID, + Domain: input.Domain, + }, + ) + if err != nil { + return nil, types.CreateCustomDomainOutput{}, fmt.Errorf("cannot create custom domain: %w", err) + } + + return nil, types.CreateCustomDomainOutput{CustomDomain: types.NewCustomDomain(domain)}, nil +} + +// DeleteCustomDomainTool handles the deleteCustomDomain tool +// Delete the custom domain for the organization +func (r *Resolver) DeleteCustomDomainTool(ctx context.Context, req *mcp.CallToolRequest, input *types.DeleteCustomDomainInput) (*mcp.CallToolResult, types.DeleteCustomDomainOutput, error) { + r.MustAuthorize(ctx, input.OrganizationID, probo.ActionCustomDomainDelete) + + prb := r.ProboService(ctx, input.OrganizationID) + + domain, err := prb.CustomDomains.GetOrganizationCustomDomain(ctx, input.OrganizationID) + if err != nil { + return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("cannot get custom domain: %w", err) + } + + if domain == nil { + return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("organization has no custom domain") + } + + deletedDomain := types.NewCustomDomain(domain) + + if err := prb.CustomDomains.DeleteCustomDomain(ctx, input.OrganizationID); err != nil { + return nil, types.DeleteCustomDomainOutput{}, fmt.Errorf("cannot delete custom domain: %w", err) + } + + return nil, types.DeleteCustomDomainOutput{DeletedCustomDomain: deletedDomain}, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index 5377601da..b85da2d31 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -30,6 +30,47 @@ components: - NAME go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.VendorOrderField + VendorContactOrderField: + type: string + enum: + - CREATED_AT + - FULL_NAME + - EMAIL + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.VendorContactOrderField + + VendorContactOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/VendorContactOrderField" + description: Vendor contact order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Vendor contact order direction + + VendorServiceOrderField: + type: string + enum: + - CREATED_AT + - NAME + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.VendorServiceOrderField + + VendorServiceOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/VendorServiceOrderField" + description: Vendor service order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Vendor service order direction + VendorRiskAssessmentOrderField: type: string enum: @@ -804,6 +845,283 @@ components: $ref: "#/components/schemas/GID" description: Deleted vendor ID + VendorContact: + type: object + required: + - id + - vendor_id + - full_name + - email + - phone + - role + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Vendor contact ID + vendor_id: + $ref: "#/components/schemas/GID" + description: Vendor ID + full_name: + type: string + description: Full name + email: + type: string + description: Email address + phone: + type: string + description: Phone number + role: + type: string + description: Role + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + + ListVendorContactsInput: + type: object + required: + - vendor_id + properties: + vendor_id: + $ref: "#/components/schemas/GID" + description: Vendor ID + order_by: + $ref: "#/components/schemas/VendorContactOrderBy" + description: Vendor contact order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListVendorContactsOutput: + type: object + required: + - vendor_contacts + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + vendor_contacts: + type: array + items: + $ref: "#/components/schemas/VendorContact" + + AddVendorContactInput: + type: object + required: + - vendor_id + - full_name + - email + - phone + - role + properties: + vendor_id: + $ref: "#/components/schemas/GID" + description: Vendor ID + full_name: + type: string + description: Full name + email: + type: string + description: Email address + phone: + type: string + description: Phone number + role: + type: string + description: Role + + AddVendorContactOutput: + type: object + required: + - vendor_contact + properties: + vendor_contact: + $ref: "#/components/schemas/VendorContact" + + UpdateVendorContactInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Vendor contact ID + full_name: + type: string + description: Full name + email: + type: string + description: Email address + phone: + type: string + description: Phone number + role: + type: string + description: Role + + UpdateVendorContactOutput: + type: object + required: + - vendor_contact + properties: + vendor_contact: + $ref: "#/components/schemas/VendorContact" + + DeleteVendorContactInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Vendor contact ID + + DeleteVendorContactOutput: + type: object + required: + - deleted_vendor_contact_id + properties: + deleted_vendor_contact_id: + $ref: "#/components/schemas/GID" + description: Deleted vendor contact ID + + VendorService: + type: object + required: + - id + - vendor_id + - name + - description + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Vendor service ID + vendor_id: + $ref: "#/components/schemas/GID" + description: Vendor ID + name: + type: string + description: Service name + description: + type: string + description: Service description + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + + ListVendorServicesInput: + type: object + required: + - vendor_id + properties: + vendor_id: + $ref: "#/components/schemas/GID" + description: Vendor ID + order_by: + $ref: "#/components/schemas/VendorServiceOrderBy" + description: Vendor service order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListVendorServicesOutput: + type: object + required: + - vendor_services + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + vendor_services: + type: array + items: + $ref: "#/components/schemas/VendorService" + + AddVendorServiceInput: + type: object + required: + - vendor_id + - name + properties: + vendor_id: + $ref: "#/components/schemas/GID" + description: Vendor ID + name: + type: string + description: Service name + description: + type: string + description: Service description + + AddVendorServiceOutput: + type: object + required: + - vendor_service + properties: + vendor_service: + $ref: "#/components/schemas/VendorService" + + UpdateVendorServiceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Vendor service ID + name: + type: string + description: Service name + description: + type: string + description: Service description + + UpdateVendorServiceOutput: + type: object + required: + - vendor_service + properties: + vendor_service: + $ref: "#/components/schemas/VendorService" + + DeleteVendorServiceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Vendor service ID + + DeleteVendorServiceOutput: + type: object + required: + - deleted_vendor_service_id + properties: + deleted_vendor_service_id: + $ref: "#/components/schemas/GID" + description: Deleted vendor service ID + GetUserInput: type: object required: @@ -2157,6 +2475,24 @@ components: asset: $ref: "#/components/schemas/Asset" + DeleteAssetInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Asset ID + + DeleteAssetOutput: + type: object + required: + - deleted_asset_id + properties: + deleted_asset_id: + $ref: "#/components/schemas/GID" + description: Deleted asset ID + DataClassification: type: string enum: @@ -2337,6 +2673,24 @@ components: datum: $ref: "#/components/schemas/Datum" + DeleteDatumInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Datum ID + + DeleteDatumOutput: + type: object + required: + - deleted_datum_id + properties: + deleted_datum_id: + $ref: "#/components/schemas/GID" + description: Deleted datum ID + FindingKind: type: string enum: @@ -3111,6 +3465,24 @@ components: obligation: $ref: "#/components/schemas/Obligation" + DeleteObligationInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Obligation ID + + DeleteObligationOutput: + type: object + required: + - deleted_obligation_id + properties: + deleted_obligation_id: + $ref: "#/components/schemas/GID" + description: Deleted obligation ID + ProcessingActivityRole: type: string enum: @@ -4169,6 +4541,24 @@ components: audit: $ref: "#/components/schemas/Audit" + DeleteAuditInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Audit ID + + DeleteAuditOutput: + type: object + required: + - deleted_audit_id + properties: + deleted_audit_id: + $ref: "#/components/schemas/GID" + description: Deleted audit ID + ControlOrderField: type: string enum: @@ -7824,6 +8214,837 @@ components: go.probo.inc/mcpgen/type: time.Time description: When the action was performed + RightsRequestType: + type: string + enum: + - ACCESS + - DELETION + - PORTABILITY + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RightsRequestType + + RightsRequestState: + type: string + enum: + - TODO + - IN_PROGRESS + - DONE + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RightsRequestState + + RightsRequestOrderField: + type: string + enum: + - CREATED_AT + - DEADLINE + - STATE + - TYPE + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.RightsRequestOrderField + + RightsRequestOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/RightsRequestOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + RightsRequest: + type: object + required: + - id + - organization_id + - request_type + - request_state + - data_subject + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + request_type: + $ref: "#/components/schemas/RightsRequestType" + request_state: + $ref: "#/components/schemas/RightsRequestState" + data_subject: + type: string + contact: + type: + - string + - "null" + details: + type: + - string + - "null" + deadline: + anyOf: + - type: string + format: date-time + - type: "null" + action_taken: + type: + - string + - "null" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ListRightsRequestsInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + order_by: + $ref: "#/components/schemas/RightsRequestOrderBy" + description: Rights request order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListRightsRequestsOutput: + type: object + required: + - rights_requests + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + rights_requests: + type: array + items: + $ref: "#/components/schemas/RightsRequest" + + GetRightsRequestInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Rights request ID + + GetRightsRequestOutput: + type: object + required: + - rights_request + properties: + rights_request: + $ref: "#/components/schemas/RightsRequest" + + AddRightsRequestInput: + type: object + required: + - organization_id + - request_type + - request_state + - data_subject + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + request_type: + $ref: "#/components/schemas/RightsRequestType" + description: Rights request type + request_state: + $ref: "#/components/schemas/RightsRequestState" + description: Rights request state + data_subject: + type: string + description: Data subject + contact: + type: string + description: Contact information + details: + type: string + description: Request details + deadline: + anyOf: + - type: string + format: date-time + - type: "null" + description: Deadline + action_taken: + type: string + description: Action taken + + AddRightsRequestOutput: + type: object + required: + - rights_request + properties: + rights_request: + $ref: "#/components/schemas/RightsRequest" + + UpdateRightsRequestInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Rights request ID + request_type: + $ref: "#/components/schemas/RightsRequestType" + description: Rights request type + request_state: + $ref: "#/components/schemas/RightsRequestState" + description: Rights request state + data_subject: + type: string + description: Data subject + contact: + type: ["string", "null"] + description: Contact information + go.probo.inc/mcpgen/omittable: true + details: + type: ["string", "null"] + description: Request details + go.probo.inc/mcpgen/omittable: true + deadline: + anyOf: + - type: string + format: date-time + - type: "null" + description: Deadline + go.probo.inc/mcpgen/omittable: true + action_taken: + type: ["string", "null"] + description: Action taken + go.probo.inc/mcpgen/omittable: true + + UpdateRightsRequestOutput: + type: object + required: + - rights_request + properties: + rights_request: + $ref: "#/components/schemas/RightsRequest" + + DeleteRightsRequestInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Rights request ID + + DeleteRightsRequestOutput: + type: object + required: + - deleted_rights_request_id + properties: + deleted_rights_request_id: + $ref: "#/components/schemas/GID" + description: Deleted rights request ID + + SearchEngineIndexing: + type: string + enum: + - INDEXABLE + - NOT_INDEXABLE + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.SearchEngineIndexing + + SSLStatus: + type: string + enum: + - PENDING + - PROVISIONING + - ACTIVE + - RENEWING + - EXPIRED + - FAILED + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.CustomDomainSSLStatus + + TrustCenterReferenceOrderField: + type: string + enum: + - RANK + - NAME + - CREATED_AT + - UPDATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.TrustCenterReferenceOrderField + + TrustCenterReferenceOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/TrustCenterReferenceOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + TrustCenterFileOrderField: + type: string + enum: + - NAME + - CREATED_AT + - UPDATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.TrustCenterFileOrderField + + TrustCenterFileOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/TrustCenterFileOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + ComplianceExternalURLOrderField: + type: string + enum: + - CREATED_AT + - RANK + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.ComplianceExternalURLOrderField + + ComplianceExternalURLOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/ComplianceExternalURLOrderField" + direction: + $ref: "#/components/schemas/OrderDirection" + + TrustCenter: + type: object + required: + - id + - organization_id + - active + - search_engine_indexing + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + active: + type: boolean + search_engine_indexing: + $ref: "#/components/schemas/SearchEngineIndexing" + logo_file_url: + type: + - string + - "null" + dark_logo_file_url: + type: + - string + - "null" + nda_file_name: + type: + - string + - "null" + nda_file_url: + type: + - string + - "null" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + TrustCenterReference: + type: object + required: + - id + - name + - rank + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + name: + type: string + description: + type: + - string + - "null" + website_url: + type: + - string + - "null" + logo_url: + type: + - string + - "null" + rank: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + TrustCenterFile: + type: object + required: + - id + - name + - category + - file_url + - trust_center_visibility + - organization_id + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + name: + type: string + category: + type: string + file_url: + type: string + trust_center_visibility: + $ref: "#/components/schemas/TrustCenterVisibility" + organization_id: + $ref: "#/components/schemas/GID" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + ComplianceExternalURL: + type: object + required: + - id + - name + - url + - rank + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + name: + type: string + url: + type: string + rank: + type: integer + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + CustomDomain: + type: object + required: + - id + - organization_id + - domain + - ssl_status + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + organization_id: + $ref: "#/components/schemas/GID" + domain: + type: string + ssl_status: + $ref: "#/components/schemas/SSLStatus" + ssl_expires_at: + anyOf: + - type: string + format: date-time + - type: "null" + created_at: + type: string + format: date-time + updated_at: + type: string + format: date-time + + GetTrustCenterInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + + GetTrustCenterOutput: + type: object + required: + - trust_center + properties: + trust_center: + $ref: "#/components/schemas/TrustCenter" + + UpdateTrustCenterInput: + type: object + required: + - trust_center_id + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + active: + type: + - boolean + - "null" + description: Whether the trust center is active + go.probo.inc/mcpgen/omittable: true + search_engine_indexing: + anyOf: + - $ref: "#/components/schemas/SearchEngineIndexing" + - type: "null" + description: Search engine indexing setting + go.probo.inc/mcpgen/omittable: true + + UpdateTrustCenterOutput: + type: object + required: + - trust_center + properties: + trust_center: + $ref: "#/components/schemas/TrustCenter" + + ListTrustCenterReferencesInput: + type: object + required: + - trust_center_id + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + order_by: + $ref: "#/components/schemas/TrustCenterReferenceOrderBy" + description: Trust center reference order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListTrustCenterReferencesOutput: + type: object + required: + - trust_center_references + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + trust_center_references: + type: array + items: + $ref: "#/components/schemas/TrustCenterReference" + + AddTrustCenterReferenceInput: + type: object + required: + - trust_center_id + - name + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + name: + type: string + description: Reference name + description: + type: string + description: Reference description + website_url: + type: string + description: Reference website URL + + AddTrustCenterReferenceOutput: + type: object + required: + - trust_center_reference + properties: + trust_center_reference: + $ref: "#/components/schemas/TrustCenterReference" + + UpdateTrustCenterReferenceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Trust center reference ID + name: + type: + - string + - "null" + description: Reference name + go.probo.inc/mcpgen/omittable: true + description: + type: + - string + - "null" + description: Reference description + go.probo.inc/mcpgen/omittable: true + website_url: + type: + - string + - "null" + description: Reference website URL + go.probo.inc/mcpgen/omittable: true + rank: + type: + - integer + - "null" + description: Reference rank + go.probo.inc/mcpgen/omittable: true + + UpdateTrustCenterReferenceOutput: + type: object + required: + - trust_center_reference + properties: + trust_center_reference: + $ref: "#/components/schemas/TrustCenterReference" + + DeleteTrustCenterReferenceInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Trust center reference ID + + DeleteTrustCenterReferenceOutput: + type: object + required: + - deleted_trust_center_reference_id + properties: + deleted_trust_center_reference_id: + $ref: "#/components/schemas/GID" + description: Deleted trust center reference ID + + ListTrustCenterFilesInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + order_by: + $ref: "#/components/schemas/TrustCenterFileOrderBy" + description: Trust center file order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListTrustCenterFilesOutput: + type: object + required: + - trust_center_files + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + trust_center_files: + type: array + items: + $ref: "#/components/schemas/TrustCenterFile" + + DeleteTrustCenterFileInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Trust center file ID + + DeleteTrustCenterFileOutput: + type: object + required: + - deleted_trust_center_file_id + properties: + deleted_trust_center_file_id: + $ref: "#/components/schemas/GID" + description: Deleted trust center file ID + + ListComplianceExternalURLsInput: + type: object + required: + - trust_center_id + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + order_by: + $ref: "#/components/schemas/ComplianceExternalURLOrderBy" + description: Compliance external URL order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListComplianceExternalURLsOutput: + type: object + required: + - compliance_external_urls + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + compliance_external_urls: + type: array + items: + $ref: "#/components/schemas/ComplianceExternalURL" + + AddComplianceExternalURLInput: + type: object + required: + - trust_center_id + - name + - url + properties: + trust_center_id: + $ref: "#/components/schemas/GID" + description: Trust center ID + name: + type: string + description: External URL name + url: + type: string + description: External URL + + AddComplianceExternalURLOutput: + type: object + required: + - compliance_external_url + properties: + compliance_external_url: + $ref: "#/components/schemas/ComplianceExternalURL" + + UpdateComplianceExternalURLInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Compliance external URL ID + name: + type: + - string + - "null" + description: External URL name + go.probo.inc/mcpgen/omittable: true + url: + type: + - string + - "null" + description: External URL + go.probo.inc/mcpgen/omittable: true + rank: + type: + - integer + - "null" + description: External URL rank + go.probo.inc/mcpgen/omittable: true + + UpdateComplianceExternalURLOutput: + type: object + required: + - compliance_external_url + properties: + compliance_external_url: + $ref: "#/components/schemas/ComplianceExternalURL" + + DeleteComplianceExternalURLInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Compliance external URL ID + + DeleteComplianceExternalURLOutput: + type: object + required: + - deleted_compliance_external_url_id + properties: + deleted_compliance_external_url_id: + $ref: "#/components/schemas/GID" + description: Deleted compliance external URL ID + + CreateCustomDomainInput: + type: object + required: + - organization_id + - domain + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + domain: + type: string + description: Custom domain name + + CreateCustomDomainOutput: + type: object + required: + - custom_domain + properties: + custom_domain: + $ref: "#/components/schemas/CustomDomain" + + DeleteCustomDomainInput: + type: object + required: + - organization_id + properties: + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + + DeleteCustomDomainOutput: + type: object + required: + - deleted_custom_domain + properties: + deleted_custom_domain: + $ref: "#/components/schemas/CustomDomain" + tools: - name: listOrganizations description: List all organizations the user has access to @@ -7943,6 +9164,74 @@ tools: $ref: "#/components/schemas/DeleteVendorInput" outputSchema: $ref: "#/components/schemas/DeleteVendorOutput" + - name: listVendorContacts + description: List all contacts for a vendor + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListVendorContactsInput" + outputSchema: + $ref: "#/components/schemas/ListVendorContactsOutput" + - name: addVendorContact + description: Add a new contact to a vendor + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddVendorContactInput" + outputSchema: + $ref: "#/components/schemas/AddVendorContactOutput" + - name: updateVendorContact + description: Update an existing vendor contact + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateVendorContactInput" + outputSchema: + $ref: "#/components/schemas/UpdateVendorContactOutput" + - name: deleteVendorContact + description: Delete a vendor contact + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteVendorContactInput" + outputSchema: + $ref: "#/components/schemas/DeleteVendorContactOutput" + - name: listVendorServices + description: List all services for a vendor + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListVendorServicesInput" + outputSchema: + $ref: "#/components/schemas/ListVendorServicesOutput" + - name: addVendorService + description: Add a new service to a vendor + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddVendorServiceInput" + outputSchema: + $ref: "#/components/schemas/AddVendorServiceOutput" + - name: updateVendorService + description: Update an existing vendor service + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateVendorServiceInput" + outputSchema: + $ref: "#/components/schemas/UpdateVendorServiceOutput" + - name: deleteVendorService + description: Delete a vendor service + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteVendorServiceInput" + outputSchema: + $ref: "#/components/schemas/DeleteVendorServiceOutput" - name: listRisks description: List all risks for the organization hints: @@ -8158,6 +9447,15 @@ tools: $ref: "#/components/schemas/UpdateAssetInput" outputSchema: $ref: "#/components/schemas/UpdateAssetOutput" + - name: deleteAsset + description: Delete an asset + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteAssetInput" + outputSchema: + $ref: "#/components/schemas/DeleteAssetOutput" - name: listData description: List all data for the organization hints: @@ -8192,6 +9490,15 @@ tools: $ref: "#/components/schemas/UpdateDatumInput" outputSchema: $ref: "#/components/schemas/UpdateDatumOutput" + - name: deleteDatum + description: Delete a datum + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteDatumInput" + outputSchema: + $ref: "#/components/schemas/DeleteDatumOutput" - name: listFindings description: List all findings (nonconformities, observations, exceptions) for the organization hints: @@ -8295,6 +9602,15 @@ tools: $ref: "#/components/schemas/UpdateObligationInput" outputSchema: $ref: "#/components/schemas/UpdateObligationOutput" + - name: deleteObligation + description: Delete an obligation + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteObligationInput" + outputSchema: + $ref: "#/components/schemas/DeleteObligationOutput" - name: listProcessingActivities description: List all processing activities for the organization hints: @@ -8455,6 +9771,15 @@ tools: $ref: "#/components/schemas/UpdateAuditInput" outputSchema: $ref: "#/components/schemas/UpdateAuditOutput" + - name: deleteAudit + description: Delete an audit + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteAuditInput" + outputSchema: + $ref: "#/components/schemas/DeleteAuditOutput" - name: getAuditReportUrl description: Get a presigned download URL for an audit's attached report. Returns a time-limited URL valid for 15 minutes. The audit must have an attached report. hints: @@ -9214,3 +10539,166 @@ tools: $ref: "#/components/schemas/GetDocumentVersionApprovalDecisionInput" outputSchema: $ref: "#/components/schemas/GetDocumentVersionApprovalDecisionOutput" + - name: listRightsRequests + description: List all rights requests for the organization + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListRightsRequestsInput" + outputSchema: + $ref: "#/components/schemas/ListRightsRequestsOutput" + - name: getRightsRequest + description: Get a rights request by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetRightsRequestInput" + outputSchema: + $ref: "#/components/schemas/GetRightsRequestOutput" + - name: addRightsRequest + description: Add a new rights request to the organization + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddRightsRequestInput" + outputSchema: + $ref: "#/components/schemas/AddRightsRequestOutput" + - name: updateRightsRequest + description: Update an existing rights request + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateRightsRequestInput" + outputSchema: + $ref: "#/components/schemas/UpdateRightsRequestOutput" + - name: deleteRightsRequest + description: Delete a rights request + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteRightsRequestInput" + outputSchema: + $ref: "#/components/schemas/DeleteRightsRequestOutput" + - name: getTrustCenter + description: Get the trust center for an organization + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetTrustCenterInput" + outputSchema: + $ref: "#/components/schemas/GetTrustCenterOutput" + - name: updateTrustCenter + description: Update a trust center's settings + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateTrustCenterInput" + outputSchema: + $ref: "#/components/schemas/UpdateTrustCenterOutput" + - name: listTrustCenterReferences + description: List all references for a trust center + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListTrustCenterReferencesInput" + outputSchema: + $ref: "#/components/schemas/ListTrustCenterReferencesOutput" + - name: addTrustCenterReference + description: Add a new reference to a trust center + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddTrustCenterReferenceInput" + outputSchema: + $ref: "#/components/schemas/AddTrustCenterReferenceOutput" + - name: updateTrustCenterReference + description: Update an existing trust center reference + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateTrustCenterReferenceInput" + outputSchema: + $ref: "#/components/schemas/UpdateTrustCenterReferenceOutput" + - name: deleteTrustCenterReference + description: Delete a trust center reference + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteTrustCenterReferenceInput" + outputSchema: + $ref: "#/components/schemas/DeleteTrustCenterReferenceOutput" + - name: listTrustCenterFiles + description: List all files for the trust center + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListTrustCenterFilesInput" + outputSchema: + $ref: "#/components/schemas/ListTrustCenterFilesOutput" + - name: deleteTrustCenterFile + description: Delete a trust center file + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteTrustCenterFileInput" + outputSchema: + $ref: "#/components/schemas/DeleteTrustCenterFileOutput" + - name: listComplianceExternalURLs + description: List all compliance external URLs for a trust center + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListComplianceExternalURLsInput" + outputSchema: + $ref: "#/components/schemas/ListComplianceExternalURLsOutput" + - name: addComplianceExternalURL + description: Add a new compliance external URL to a trust center + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/AddComplianceExternalURLInput" + outputSchema: + $ref: "#/components/schemas/AddComplianceExternalURLOutput" + - name: updateComplianceExternalURL + description: Update an existing compliance external URL + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/UpdateComplianceExternalURLInput" + outputSchema: + $ref: "#/components/schemas/UpdateComplianceExternalURLOutput" + - name: deleteComplianceExternalURL + description: Delete a compliance external URL + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteComplianceExternalURLInput" + outputSchema: + $ref: "#/components/schemas/DeleteComplianceExternalURLOutput" + - name: createCustomDomain + description: Create a custom domain for an organization + hints: + readonly: false + inputSchema: + $ref: "#/components/schemas/CreateCustomDomainInput" + outputSchema: + $ref: "#/components/schemas/CreateCustomDomainOutput" + - name: deleteCustomDomain + description: Delete the custom domain for an organization + hints: + readonly: false + destructive: true + inputSchema: + $ref: "#/components/schemas/DeleteCustomDomainInput" + outputSchema: + $ref: "#/components/schemas/DeleteCustomDomainOutput" diff --git a/pkg/server/api/mcp/v1/types/compliance_external_url.go b/pkg/server/api/mcp/v1/types/compliance_external_url.go new file mode 100644 index 000000000..558229b98 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/compliance_external_url.go @@ -0,0 +1,49 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewComplianceExternalURL(c *coredata.ComplianceExternalURL) *ComplianceExternalURL { + return &ComplianceExternalURL{ + ID: c.ID, + Name: c.Name, + URL: c.URL, + Rank: c.Rank, + CreatedAt: c.CreatedAt, + UpdatedAt: c.UpdatedAt, + } +} + +func NewListComplianceExternalURLsOutput(p *page.Page[*coredata.ComplianceExternalURL, coredata.ComplianceExternalURLOrderField]) ListComplianceExternalURLsOutput { + urls := make([]*ComplianceExternalURL, 0, len(p.Data)) + for _, c := range p.Data { + urls = append(urls, NewComplianceExternalURL(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 ListComplianceExternalURLsOutput{ + NextCursor: nextCursor, + ComplianceExternalUrls: urls, + } +} diff --git a/pkg/server/api/mcp/v1/types/custom_domain.go b/pkg/server/api/mcp/v1/types/custom_domain.go new file mode 100644 index 000000000..2812f1342 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/custom_domain.go @@ -0,0 +1,31 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" +) + +func NewCustomDomain(d *coredata.CustomDomain) *CustomDomain { + return &CustomDomain{ + ID: d.ID, + OrganizationID: d.OrganizationID, + Domain: d.Domain, + SslStatus: d.SSLStatus, + SslExpiresAt: d.SSLExpiresAt, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + } +} diff --git a/pkg/server/api/mcp/v1/types/rights_request.go b/pkg/server/api/mcp/v1/types/rights_request.go new file mode 100644 index 000000000..1f414988c --- /dev/null +++ b/pkg/server/api/mcp/v1/types/rights_request.go @@ -0,0 +1,59 @@ +// Copyright (c) 2025-2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewRightsRequest(rr *coredata.RightsRequest) *RightsRequest { + rightsRequest := &RightsRequest{ + ID: rr.ID, + OrganizationID: rr.OrganizationID, + RequestType: rr.RequestType, + RequestState: rr.RequestState, + Contact: rr.Contact, + Details: rr.Details, + Deadline: rr.Deadline, + ActionTaken: rr.ActionTaken, + CreatedAt: rr.CreatedAt, + UpdatedAt: rr.UpdatedAt, + } + + if rr.DataSubject != nil { + rightsRequest.DataSubject = *rr.DataSubject + } + + return rightsRequest +} + +func NewListRightsRequestsOutput(rightsRequestPage *page.Page[*coredata.RightsRequest, coredata.RightsRequestOrderField]) ListRightsRequestsOutput { + rightsRequests := make([]*RightsRequest, 0, len(rightsRequestPage.Data)) + for _, v := range rightsRequestPage.Data { + rightsRequests = append(rightsRequests, NewRightsRequest(v)) + } + + var nextCursor *page.CursorKey + if len(rightsRequestPage.Data) > 0 { + cursorKey := rightsRequestPage.Data[len(rightsRequestPage.Data)-1].CursorKey(rightsRequestPage.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListRightsRequestsOutput{ + NextCursor: nextCursor, + RightsRequests: rightsRequests, + } +} diff --git a/pkg/server/api/mcp/v1/types/trust_center.go b/pkg/server/api/mcp/v1/types/trust_center.go new file mode 100644 index 000000000..111e69d12 --- /dev/null +++ b/pkg/server/api/mcp/v1/types/trust_center.go @@ -0,0 +1,87 @@ +// Copyright (c) 2026 Probo Inc . +// +// Permission to use, copy, modify, and/or distribute this software for any +// purpose with or without fee is hereby granted, provided that the above +// copyright notice and this permission notice appear in all copies. +// +// THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH +// REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY +// AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, +// INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM +// LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR +// OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR +// PERFORMANCE OF THIS SOFTWARE. + +package types + +import ( + "go.probo.inc/probo/pkg/coredata" + "go.probo.inc/probo/pkg/page" +) + +func NewTrustCenter(tc *coredata.TrustCenter) *TrustCenter { + return &TrustCenter{ + ID: tc.ID, + OrganizationID: tc.OrganizationID, + Active: tc.Active, + SearchEngineIndexing: tc.SearchEngineIndexing, + CreatedAt: tc.CreatedAt, + UpdatedAt: tc.UpdatedAt, + } +} + +func NewTrustCenterReference(r *coredata.TrustCenterReference) *TrustCenterReference { + return &TrustCenterReference{ + ID: r.ID, + Name: r.Name, + Description: r.Description, + WebsiteURL: &r.WebsiteURL, + Rank: r.Rank, + CreatedAt: r.CreatedAt, + UpdatedAt: r.UpdatedAt, + } +} + +func NewListTrustCenterReferencesOutput(p *page.Page[*coredata.TrustCenterReference, coredata.TrustCenterReferenceOrderField]) ListTrustCenterReferencesOutput { + refs := make([]*TrustCenterReference, 0, len(p.Data)) + for _, r := range p.Data { + refs = append(refs, NewTrustCenterReference(r)) + } + + var nextCursor *page.CursorKey + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListTrustCenterReferencesOutput{ + NextCursor: nextCursor, + TrustCenterReferences: refs, + } +} + +func NewTrustCenterFile(f *coredata.TrustCenterFile, fileURL string) *TrustCenterFile { + return &TrustCenterFile{ + ID: f.ID, + OrganizationID: f.OrganizationID, + Name: f.Name, + Category: f.Category, + FileURL: fileURL, + TrustCenterVisibility: f.TrustCenterVisibility, + CreatedAt: f.CreatedAt, + UpdatedAt: f.UpdatedAt, + } +} + +func NewListTrustCenterFilesOutput(files []*TrustCenterFile, p *page.Page[*coredata.TrustCenterFile, coredata.TrustCenterFileOrderField]) ListTrustCenterFilesOutput { + var nextCursor *page.CursorKey + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListTrustCenterFilesOutput{ + NextCursor: nextCursor, + TrustCenterFiles: files, + } +} diff --git a/pkg/server/api/mcp/v1/types/vendor.go b/pkg/server/api/mcp/v1/types/vendor.go index f9676d88b..08526870c 100644 --- a/pkg/server/api/mcp/v1/types/vendor.go +++ b/pkg/server/api/mcp/v1/types/vendor.go @@ -120,3 +120,88 @@ func NewUpdateVendorOutput(v *coredata.Vendor) UpdateVendorOutput { Vendor: NewVendor(v), } } + +func NewVendorContact(vc *coredata.VendorContact) *VendorContact { + var fullName string + if vc.FullName != nil { + fullName = *vc.FullName + } + + var email string + if vc.Email != nil { + email = vc.Email.String() + } + + var phone string + if vc.Phone != nil { + phone = *vc.Phone + } + + var role string + if vc.Role != nil { + role = *vc.Role + } + + return &VendorContact{ + ID: vc.ID, + VendorID: vc.VendorID, + FullName: fullName, + Email: email, + Phone: phone, + Role: role, + CreatedAt: vc.CreatedAt, + UpdatedAt: vc.UpdatedAt, + } +} + +func NewListVendorContactsOutput(p *page.Page[*coredata.VendorContact, coredata.VendorContactOrderField]) ListVendorContactsOutput { + contacts := make([]*VendorContact, 0, len(p.Data)) + for _, vc := range p.Data { + contacts = append(contacts, NewVendorContact(vc)) + } + + var nextCursor *page.CursorKey + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListVendorContactsOutput{ + NextCursor: nextCursor, + VendorContacts: contacts, + } +} + +func NewVendorService(vs *coredata.VendorService) *VendorService { + var description string + if vs.Description != nil { + description = *vs.Description + } + + return &VendorService{ + ID: vs.ID, + VendorID: vs.VendorID, + Name: vs.Name, + Description: description, + CreatedAt: vs.CreatedAt, + UpdatedAt: vs.UpdatedAt, + } +} + +func NewListVendorServicesOutput(p *page.Page[*coredata.VendorService, coredata.VendorServiceOrderField]) ListVendorServicesOutput { + services := make([]*VendorService, 0, len(p.Data)) + for _, vs := range p.Data { + services = append(services, NewVendorService(vs)) + } + + var nextCursor *page.CursorKey + if len(p.Data) > 0 { + cursorKey := p.Data[len(p.Data)-1].CursorKey(p.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListVendorServicesOutput{ + NextCursor: nextCursor, + VendorServices: services, + } +}