From 5b8918bd5aaf216780ef5ab36c17e502a10d4a52 Mon Sep 17 00:00:00 2001 From: Sacha Al Himdani Date: Mon, 20 Apr 2026 10:10:39 +0200 Subject: [PATCH] Add approval quorum and decision read tools Expose document version approval quorums and decisions through MCP, CLI, and n8n. This lets users inspect who approved or rejected a document version, including the rejection comment, without relying solely on the audit log. MCP tools: listDocumentVersionApprovalQuorums, getDocumentVersionApprovalQuorum, listDocumentVersionApprovalDecisions, getDocumentVersionApprovalDecision. CLI commands: document list-approval-quorums, view-approval-quorum, list-approval-decisions, view-approval-decision. n8n operations: Get/Get Many Approval Quorums and Approval Decisions on the Document resource. Signed-off-by: Sacha Al Himdani --- .../getAllApprovalDecisions.operation.ts | 119 ++++++++ .../getAllApprovalQuorums.operation.ts | 113 ++++++++ .../document/getApprovalDecision.operation.ts | 66 +++++ .../document/getApprovalQuorum.operation.ts | 60 ++++ .../nodes/Probo/actions/document/index.ts | 36 +++ pkg/cmd/document/document.go | 8 + .../list_approval_decisions.go | 206 +++++++++++++ .../list_approval_quorums.go | 186 ++++++++++++ .../view_approval_decision.go | 155 ++++++++++ .../view_approval_quorum.go | 134 +++++++++ pkg/probo/document_approval_service.go | 23 ++ pkg/server/api/mcp/v1/schema.resolvers.go | 88 ++++++ pkg/server/api/mcp/v1/specification.yaml | 273 ++++++++++++++++++ pkg/server/api/mcp/v1/types/document.go | 61 ++++ 14 files changed, 1528 insertions(+) create mode 100644 packages/n8n-node/nodes/Probo/actions/document/getAllApprovalDecisions.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/document/getAllApprovalQuorums.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/document/getApprovalDecision.operation.ts create mode 100644 packages/n8n-node/nodes/Probo/actions/document/getApprovalQuorum.operation.ts create mode 100644 pkg/cmd/document/list-approval-decisions/list_approval_decisions.go create mode 100644 pkg/cmd/document/list-approval-quorums/list_approval_quorums.go create mode 100644 pkg/cmd/document/view-approval-decision/view_approval_decision.go create mode 100644 pkg/cmd/document/view-approval-quorum/view_approval_quorum.go diff --git a/packages/n8n-node/nodes/Probo/actions/document/getAllApprovalDecisions.operation.ts b/packages/n8n-node/nodes/Probo/actions/document/getAllApprovalDecisions.operation.ts new file mode 100644 index 000000000..ee417339d --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/document/getAllApprovalDecisions.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: 'Approval Quorum ID', + name: 'approvalQuorumId', + type: 'string', + displayOptions: { + show: { + resource: ['document'], + operation: ['getAllApprovalDecisions'], + }, + }, + default: '', + description: 'The ID of the approval quorum', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['document'], + operation: ['getAllApprovalDecisions'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['document'], + operation: ['getAllApprovalDecisions'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const approvalQuorumId = this.getNodeParameter('approvalQuorumId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetDocumentVersionApprovalDecisions($approvalQuorumId: ID!, $first: Int, $after: CursorKey) { + node(id: $approvalQuorumId) { + ... on DocumentVersionApprovalQuorum { + decisions(first: $first, after: $after) { + edges { + node { + id + state + comment + decidedAt + approver { + id + fullName + } + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const approvalDecisions = await proboApiRequestAllItems.call( + this, + query, + { approvalQuorumId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.decisions as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { approvalDecisions }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/document/getAllApprovalQuorums.operation.ts b/packages/n8n-node/nodes/Probo/actions/document/getAllApprovalQuorums.operation.ts new file mode 100644 index 000000000..48da41293 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/document/getAllApprovalQuorums.operation.ts @@ -0,0 +1,113 @@ +// 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: 'Document Version ID', + name: 'documentVersionId', + type: 'string', + displayOptions: { + show: { + resource: ['document'], + operation: ['getAllApprovalQuorums'], + }, + }, + default: '', + description: 'The ID of the document version', + required: true, + }, + { + displayName: 'Return All', + name: 'returnAll', + type: 'boolean', + displayOptions: { + show: { + resource: ['document'], + operation: ['getAllApprovalQuorums'], + }, + }, + default: false, + description: 'Whether to return all results or only up to a given limit', + }, + { + displayName: 'Limit', + name: 'limit', + type: 'number', + displayOptions: { + show: { + resource: ['document'], + operation: ['getAllApprovalQuorums'], + returnAll: [false], + }, + }, + typeOptions: { + minValue: 1, + }, + default: 50, + description: 'Max number of results to return', + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const documentVersionId = this.getNodeParameter('documentVersionId', itemIndex) as string; + const returnAll = this.getNodeParameter('returnAll', itemIndex) as boolean; + const limit = this.getNodeParameter('limit', itemIndex, 50) as number; + + const query = ` + query GetDocumentVersionApprovalQuorums($documentVersionId: ID!, $first: Int, $after: CursorKey) { + node(id: $documentVersionId) { + ... on DocumentVersion { + approvalQuorums(first: $first, after: $after) { + edges { + node { + id + status + createdAt + updatedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } + } + `; + + const approvalQuorums = await proboApiRequestAllItems.call( + this, + query, + { documentVersionId }, + (response) => { + const data = response?.data as IDataObject | undefined; + const node = data?.node as IDataObject | undefined; + return node?.approvalQuorums as IDataObject | undefined; + }, + returnAll, + limit, + ); + + return { + json: { approvalQuorums }, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/document/getApprovalDecision.operation.ts b/packages/n8n-node/nodes/Probo/actions/document/getApprovalDecision.operation.ts new file mode 100644 index 000000000..a73814799 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/document/getApprovalDecision.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: 'Approval Decision ID', + name: 'approvalDecisionId', + type: 'string', + displayOptions: { + show: { + resource: ['document'], + operation: ['getApprovalDecision'], + }, + }, + default: '', + description: 'The ID of the document version approval decision', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const approvalDecisionId = this.getNodeParameter('approvalDecisionId', itemIndex) as string; + + const query = ` + query GetDocumentVersionApprovalDecision($approvalDecisionId: ID!) { + node(id: $approvalDecisionId) { + ... on DocumentVersionApprovalDecision { + id + state + comment + decidedAt + approver { + id + fullName + } + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { approvalDecisionId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/document/getApprovalQuorum.operation.ts b/packages/n8n-node/nodes/Probo/actions/document/getApprovalQuorum.operation.ts new file mode 100644 index 000000000..8b31f7d01 --- /dev/null +++ b/packages/n8n-node/nodes/Probo/actions/document/getApprovalQuorum.operation.ts @@ -0,0 +1,60 @@ +// 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: 'Approval Quorum ID', + name: 'approvalQuorumId', + type: 'string', + displayOptions: { + show: { + resource: ['document'], + operation: ['getApprovalQuorum'], + }, + }, + default: '', + description: 'The ID of the document version approval quorum', + required: true, + }, +]; + +export async function execute( + this: IExecuteFunctions, + itemIndex: number, +): Promise { + const approvalQuorumId = this.getNodeParameter('approvalQuorumId', itemIndex) as string; + + const query = ` + query GetDocumentVersionApprovalQuorum($approvalQuorumId: ID!) { + node(id: $approvalQuorumId) { + ... on DocumentVersionApprovalQuorum { + id + status + createdAt + updatedAt + } + } + } + `; + + const responseData = await proboApiRequest.call(this, query, { approvalQuorumId }); + + return { + json: responseData, + pairedItem: { item: itemIndex }, + }; +} diff --git a/packages/n8n-node/nodes/Probo/actions/document/index.ts b/packages/n8n-node/nodes/Probo/actions/document/index.ts index 19e9c8e93..00b9adefb 100644 --- a/packages/n8n-node/nodes/Probo/actions/document/index.ts +++ b/packages/n8n-node/nodes/Probo/actions/document/index.ts @@ -34,6 +34,10 @@ import * as getAllSignaturesOp from './getAllSignatures.operation'; import * as requestSignatureOp from './requestSignature.operation'; import * as cancelSignatureOp from './cancelSignature.operation'; import * as sendSigningNotificationsOp from './sendSigningNotifications.operation'; +import * as getApprovalQuorumOp from './getApprovalQuorum.operation'; +import * as getAllApprovalQuorumsOp from './getAllApprovalQuorums.operation'; +import * as getApprovalDecisionOp from './getApprovalDecision.operation'; +import * as getAllApprovalDecisionsOp from './getAllApprovalDecisions.operation'; export const description: INodeProperties[] = [ { @@ -89,12 +93,36 @@ export const description: INodeProperties[] = [ description: 'Get a document', action: 'Get a document', }, + { + name: 'Get Approval Decision', + value: 'getApprovalDecision', + description: 'Get an approval decision', + action: 'Get an approval decision', + }, + { + name: 'Get Approval Quorum', + value: 'getApprovalQuorum', + description: 'Get an approval quorum', + action: 'Get an approval quorum', + }, { name: 'Get Many', value: 'getAll', description: 'Get many documents', action: 'Get many documents', }, + { + name: 'Get Many Approval Decisions', + value: 'getAllApprovalDecisions', + description: 'Get many approval decisions for an approval quorum', + action: 'Get many approval decisions', + }, + { + name: 'Get Many Approval Quorums', + value: 'getAllApprovalQuorums', + description: 'Get many approval quorums for a document version', + action: 'Get many approval quorums', + }, { name: 'Get Many Signatures', value: 'getAllSignatures', @@ -197,6 +225,10 @@ export const description: INodeProperties[] = [ ...requestSignatureOp.description, ...cancelSignatureOp.description, ...sendSigningNotificationsOp.description, + ...getApprovalQuorumOp.description, + ...getAllApprovalQuorumsOp.description, + ...getApprovalDecisionOp.description, + ...getAllApprovalDecisionsOp.description, ]; export { @@ -221,4 +253,8 @@ export { requestSignatureOp as requestSignature, cancelSignatureOp as cancelSignature, sendSigningNotificationsOp as sendSigningNotifications, + getApprovalQuorumOp as getApprovalQuorum, + getAllApprovalQuorumsOp as getAllApprovalQuorums, + getApprovalDecisionOp as getApprovalDecision, + getAllApprovalDecisionsOp as getAllApprovalDecisions, }; diff --git a/pkg/cmd/document/document.go b/pkg/cmd/document/document.go index e05dfc0fb..448180a8b 100644 --- a/pkg/cmd/document/document.go +++ b/pkg/cmd/document/document.go @@ -22,12 +22,16 @@ import ( "go.probo.inc/probo/pkg/cmd/document/delete" deletedraft "go.probo.inc/probo/pkg/cmd/document/delete-draft" "go.probo.inc/probo/pkg/cmd/document/list" + listapprovaldecisions "go.probo.inc/probo/pkg/cmd/document/list-approval-decisions" + listapprovalquorums "go.probo.inc/probo/pkg/cmd/document/list-approval-quorums" listversions "go.probo.inc/probo/pkg/cmd/document/list-versions" publishmajor "go.probo.inc/probo/pkg/cmd/document/publish-major" publishminor "go.probo.inc/probo/pkg/cmd/document/publish-minor" "go.probo.inc/probo/pkg/cmd/document/unarchive" "go.probo.inc/probo/pkg/cmd/document/update" "go.probo.inc/probo/pkg/cmd/document/view" + viewapprovaldecision "go.probo.inc/probo/pkg/cmd/document/view-approval-decision" + viewapprovalquorum "go.probo.inc/probo/pkg/cmd/document/view-approval-quorum" viewversion "go.probo.inc/probo/pkg/cmd/document/view-version" ) @@ -49,6 +53,10 @@ func NewCmdDocument(f *cmdutil.Factory) *cobra.Command { cmd.AddCommand(deletedraft.NewCmdDeleteDraft(f)) cmd.AddCommand(publishmajor.NewCmdPublishMajor(f)) cmd.AddCommand(publishminor.NewCmdPublishMinor(f)) + cmd.AddCommand(listapprovalquorums.NewCmdListApprovalQuorums(f)) + cmd.AddCommand(viewapprovalquorum.NewCmdViewApprovalQuorum(f)) + cmd.AddCommand(listapprovaldecisions.NewCmdListApprovalDecisions(f)) + cmd.AddCommand(viewapprovaldecision.NewCmdViewApprovalDecision(f)) return cmd } diff --git a/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go b/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go new file mode 100644 index 000000000..c962ab149 --- /dev/null +++ b/pkg/cmd/document/list-approval-decisions/list_approval_decisions.go @@ -0,0 +1,206 @@ +// 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 listapprovaldecisions + +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: DocumentVersionApprovalDecisionOrder) { + node(id: $id) { + __typename + ... on DocumentVersionApprovalQuorum { + decisions(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + approver { + fullName + } + state + comment + decidedAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type approvalDecision struct { + ID string `json:"id"` + Approver struct { + FullName string `json:"fullName"` + } `json:"approver"` + State string `json:"state"` + Comment *string `json:"comment"` + DecidedAt *string `json:"decidedAt"` +} + +func NewCmdListApprovalDecisions(f *cmdutil.Factory) *cobra.Command { + var ( + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list-approval-decisions ", + Short: "List approval decisions for a quorum", + Example: ` # List approval decisions for a quorum + prb document list-approval-decisions `, + 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(), + ) + + variables := map[string]any{ + "id": args[0], + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum( + "order-by", + flagOrderBy, + []string{"CREATED_AT"}, + ); err != nil { + return err + } + if err := cmdutil.ValidateEnum( + "order-direction", + flagOrderDir, + []string{"ASC", "DESC"}, + ); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + decisions, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[approvalDecision], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + Decisions api.Connection[approvalDecision] `json:"decisions"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("approval quorum %s not found", args[0]) + } + if resp.Node.Typename != "DocumentVersionApprovalQuorum" { + return nil, fmt.Errorf("expected DocumentVersionApprovalQuorum node, got %s", resp.Node.Typename) + } + return &resp.Node.Decisions, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, decisions) + } + + if len(decisions) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No approval decisions found.") + return nil + } + + rows := make([][]string, 0, len(decisions)) + for _, d := range decisions { + comment := "" + if d.Comment != nil { + comment = *d.Comment + } + + decidedAt := "" + if d.DecidedAt != nil { + decidedAt = cmdutil.FormatTime(*d.DecidedAt) + } + + rows = append(rows, []string{ + d.ID, + d.Approver.FullName, + d.State, + comment, + decidedAt, + }) + } + + t := cmdutil.NewTable("ID", "APPROVER", "STATE", "COMMENT", "DECIDED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(decisions) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d approval decisions\n", + len(decisions), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of approval decisions 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/document/list-approval-quorums/list_approval_quorums.go b/pkg/cmd/document/list-approval-quorums/list_approval_quorums.go new file mode 100644 index 000000000..0a1a9b837 --- /dev/null +++ b/pkg/cmd/document/list-approval-quorums/list_approval_quorums.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 listapprovalquorums + +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: DocumentVersionApprovalQuorumOrder) { + node(id: $id) { + __typename + ... on DocumentVersion { + approvalQuorums(first: $first, after: $after, orderBy: $orderBy) { + totalCount + edges { + node { + id + status + createdAt + } + } + pageInfo { + hasNextPage + endCursor + } + } + } + } +} +` + +type approvalQuorum struct { + ID string `json:"id"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` +} + +func NewCmdListApprovalQuorums(f *cmdutil.Factory) *cobra.Command { + var ( + flagLimit int + flagOrderBy string + flagOrderDir string + flagOutput *string + ) + + cmd := &cobra.Command{ + Use: "list-approval-quorums ", + Short: "List approval quorums for a document version", + Example: ` # List approval quorums for a document version + prb document list-approval-quorums `, + 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(), + ) + + variables := map[string]any{ + "id": args[0], + } + + if flagOrderBy != "" { + if err := cmdutil.ValidateEnum( + "order-by", + flagOrderBy, + []string{"CREATED_AT"}, + ); err != nil { + return err + } + if err := cmdutil.ValidateEnum( + "order-direction", + flagOrderDir, + []string{"ASC", "DESC"}, + ); err != nil { + return err + } + variables["orderBy"] = map[string]any{ + "field": flagOrderBy, + "direction": flagOrderDir, + } + } + + quorums, totalCount, err := api.Paginate( + client, + listQuery, + variables, + flagLimit, + func(data json.RawMessage) (*api.Connection[approvalQuorum], error) { + var resp struct { + Node *struct { + Typename string `json:"__typename"` + ApprovalQuorums api.Connection[approvalQuorum] `json:"approvalQuorums"` + } `json:"node"` + } + if err := json.Unmarshal(data, &resp); err != nil { + return nil, err + } + if resp.Node == nil { + return nil, fmt.Errorf("document version %s not found", args[0]) + } + if resp.Node.Typename != "DocumentVersion" { + return nil, fmt.Errorf("expected DocumentVersion node, got %s", resp.Node.Typename) + } + return &resp.Node.ApprovalQuorums, nil + }, + ) + if err != nil { + return err + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, quorums) + } + + if len(quorums) == 0 { + _, _ = fmt.Fprintln(f.IOStreams.Out, "No approval quorums found.") + return nil + } + + rows := make([][]string, 0, len(quorums)) + for _, q := range quorums { + rows = append(rows, []string{ + q.ID, + q.Status, + cmdutil.FormatTime(q.CreatedAt), + }) + } + + t := cmdutil.NewTable("ID", "STATUS", "CREATED AT").Rows(rows...) + + _, _ = fmt.Fprintln(f.IOStreams.Out, t) + + if totalCount > len(quorums) { + _, _ = fmt.Fprintf( + f.IOStreams.ErrOut, + "\nShowing %d of %d approval quorums\n", + len(quorums), + totalCount, + ) + } + + return nil + }, + } + + cmd.Flags().IntVarP(&flagLimit, "limit", "L", 30, "Maximum number of approval quorums 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/document/view-approval-decision/view_approval_decision.go b/pkg/cmd/document/view-approval-decision/view_approval_decision.go new file mode 100644 index 000000000..f277c6f5f --- /dev/null +++ b/pkg/cmd/document/view-approval-decision/view_approval_decision.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 viewapprovaldecision + +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 DocumentVersionApprovalDecision { + id + quorum { + id + } + approver { + id + fullName + } + state + comment + decidedAt + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + Quorum struct { + ID string `json:"id"` + } `json:"quorum"` + Approver struct { + ID string `json:"id"` + FullName string `json:"fullName"` + } `json:"approver"` + State string `json:"state"` + Comment *string `json:"comment"` + DecidedAt *string `json:"decidedAt"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdViewApprovalDecision(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view-approval-decision ", + Short: "View an approval decision", + 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(), + ) + + 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("approval decision %s not found", args[0]) + } + + if resp.Node.Typename != "DocumentVersionApprovalDecision" { + return fmt.Errorf("expected DocumentVersionApprovalDecision 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("Approval Decision")) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), d.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Quorum ID:"), d.Quorum.ID) + _, _ = fmt.Fprintf(out, "%s%s (%s)\n", label.Render("Approver:"), d.Approver.FullName, d.Approver.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("State:"), d.State) + + if d.Comment != nil && *d.Comment != "" { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Comment:"), *d.Comment) + } + + _, _ = fmt.Fprintln(out) + + if d.DecidedAt != nil { + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Decided:"), cmdutil.FormatTime(*d.DecidedAt)) + } + _, _ = 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/document/view-approval-quorum/view_approval_quorum.go b/pkg/cmd/document/view-approval-quorum/view_approval_quorum.go new file mode 100644 index 000000000..a55e50445 --- /dev/null +++ b/pkg/cmd/document/view-approval-quorum/view_approval_quorum.go @@ -0,0 +1,134 @@ +// 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 viewapprovalquorum + +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 DocumentVersionApprovalQuorum { + id + documentVersion { + id + } + status + createdAt + updatedAt + } + } +} +` + +type viewResponse struct { + Node *struct { + Typename string `json:"__typename"` + ID string `json:"id"` + DocumentVersion struct { + ID string `json:"id"` + } `json:"documentVersion"` + Status string `json:"status"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` + } `json:"node"` +} + +func NewCmdViewApprovalQuorum(f *cmdutil.Factory) *cobra.Command { + var flagOutput *string + + cmd := &cobra.Command{ + Use: "view-approval-quorum ", + Short: "View an approval quorum", + 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(), + ) + + 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("approval quorum %s not found", args[0]) + } + + if resp.Node.Typename != "DocumentVersionApprovalQuorum" { + return fmt.Errorf("expected DocumentVersionApprovalQuorum node, got %s", resp.Node.Typename) + } + + if *flagOutput == cmdutil.OutputJSON { + return cmdutil.PrintJSON(f.IOStreams.Out, resp.Node) + } + + q := 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("Approval Quorum")) + + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("ID:"), q.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Version ID:"), q.DocumentVersion.ID) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Status:"), q.Status) + + _, _ = fmt.Fprintln(out) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Created:"), cmdutil.FormatTime(q.CreatedAt)) + _, _ = fmt.Fprintf(out, "%s%s\n", label.Render("Updated:"), cmdutil.FormatTime(q.UpdatedAt)) + + return nil + }, + } + + flagOutput = cmdutil.AddOutputFlag(cmd) + + return cmd +} diff --git a/pkg/probo/document_approval_service.go b/pkg/probo/document_approval_service.go index 81b3a621f..f9aa9ea32 100644 --- a/pkg/probo/document_approval_service.go +++ b/pkg/probo/document_approval_service.go @@ -739,6 +739,29 @@ func (s *DocumentApprovalService) CountDecisions( return count, nil } +func (s *DocumentApprovalService) GetDecision( + ctx context.Context, + decisionID gid.GID, +) (*coredata.DocumentVersionApprovalDecision, error) { + decision := &coredata.DocumentVersionApprovalDecision{} + + err := s.svc.pg.WithConn( + ctx, + func(ctx context.Context, conn pg.Querier) error { + if err := decision.LoadByID(ctx, conn, s.svc.scope, decisionID); err != nil { + return fmt.Errorf("cannot load approval decision: %w", err) + } + return nil + }, + ) + + if err != nil { + return nil, err + } + + return decision, nil +} + func (s *DocumentApprovalService) GetViewerDecision( ctx context.Context, documentVersionID gid.GID, diff --git a/pkg/server/api/mcp/v1/schema.resolvers.go b/pkg/server/api/mcp/v1/schema.resolvers.go index ad5740a38..700576caf 100644 --- a/pkg/server/api/mcp/v1/schema.resolvers.go +++ b/pkg/server/api/mcp/v1/schema.resolvers.go @@ -4048,3 +4048,91 @@ func (r *Resolver) ListWebhookEventsTool(ctx context.Context, req *mcp.CallToolR return nil, types.NewListWebhookEventsOutput(page), nil } + +func (r *Resolver) ListDocumentVersionApprovalQuorumsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionApprovalQuorumsInput) (*mcp.CallToolResult, types.ListDocumentVersionApprovalQuorumsOutput, error) { + r.MustAuthorize(ctx, input.DocumentVersionID, probo.ActionDocumentVersionApprovalList) + + svc := r.ProboService(ctx, input.DocumentVersionID) + + pageOrderBy := page.OrderBy[coredata.DocumentVersionApprovalQuorumOrderField]{ + Field: coredata.DocumentVersionApprovalQuorumOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.DocumentVersionApprovalQuorumOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + p, err := svc.DocumentApprovals.ListQuorums(ctx, input.DocumentVersionID, cursor) + if err != nil { + panic(fmt.Errorf("cannot list approval quorums: %w", err)) + } + + return nil, types.NewListDocumentVersionApprovalQuorumsOutput(p), nil +} + +func (r *Resolver) GetDocumentVersionApprovalQuorumTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentVersionApprovalQuorumInput) (*mcp.CallToolResult, types.GetDocumentVersionApprovalQuorumOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionDocumentVersionApprovalList) + + svc := r.ProboService(ctx, input.ID) + + quorum, err := svc.DocumentApprovals.GetQuorum(ctx, input.ID) + if err != nil { + panic(fmt.Errorf("cannot get approval quorum: %w", err)) + } + + return nil, types.GetDocumentVersionApprovalQuorumOutput{ + ApprovalQuorum: types.NewDocumentVersionApprovalQuorum(quorum), + }, nil +} + +func (r *Resolver) ListDocumentVersionApprovalDecisionsTool(ctx context.Context, req *mcp.CallToolRequest, input *types.ListDocumentVersionApprovalDecisionsInput) (*mcp.CallToolResult, types.ListDocumentVersionApprovalDecisionsOutput, error) { + r.MustAuthorize(ctx, input.QuorumID, probo.ActionDocumentVersionApprovalList) + + svc := r.ProboService(ctx, input.QuorumID) + + pageOrderBy := page.OrderBy[coredata.DocumentVersionApprovalDecisionOrderField]{ + Field: coredata.DocumentVersionApprovalDecisionOrderFieldCreatedAt, + Direction: page.OrderDirectionDesc, + } + if input.OrderBy != nil { + pageOrderBy = page.OrderBy[coredata.DocumentVersionApprovalDecisionOrderField]{ + Field: input.OrderBy.Field, + Direction: input.OrderBy.Direction, + } + } + + cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy) + + var states []coredata.DocumentVersionApprovalDecisionState + if input.Filter != nil { + states = input.Filter.States + } + filter := coredata.NewDocumentVersionApprovalDecisionFilter(states) + + p, err := svc.DocumentApprovals.ListDecisions(ctx, input.QuorumID, cursor, filter) + if err != nil { + panic(fmt.Errorf("cannot list approval decisions: %w", err)) + } + + return nil, types.NewListDocumentVersionApprovalDecisionsOutput(p), nil +} + +func (r *Resolver) GetDocumentVersionApprovalDecisionTool(ctx context.Context, req *mcp.CallToolRequest, input *types.GetDocumentVersionApprovalDecisionInput) (*mcp.CallToolResult, types.GetDocumentVersionApprovalDecisionOutput, error) { + r.MustAuthorize(ctx, input.ID, probo.ActionDocumentVersionApprovalList) + + svc := r.ProboService(ctx, input.ID) + + decision, err := svc.DocumentApprovals.GetDecision(ctx, input.ID) + if err != nil { + panic(fmt.Errorf("cannot get approval decision: %w", err)) + } + + return nil, types.GetDocumentVersionApprovalDecisionOutput{ + ApprovalDecision: types.NewDocumentVersionApprovalDecision(decision), + }, nil +} diff --git a/pkg/server/api/mcp/v1/specification.yaml b/pkg/server/api/mcp/v1/specification.yaml index fbb3ab954..060829242 100644 --- a/pkg/server/api/mcp/v1/specification.yaml +++ b/pkg/server/api/mcp/v1/specification.yaml @@ -82,6 +82,32 @@ components: $ref: "#/components/schemas/OrderDirection" description: Vendor order direction + DocumentVersionApprovalQuorumOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/DocumentVersionApprovalQuorumOrderField" + description: Order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Order direction + + DocumentVersionApprovalDecisionOrderBy: + type: object + required: + - field + - direction + properties: + field: + $ref: "#/components/schemas/DocumentVersionApprovalDecisionOrderField" + description: Order field + direction: + $ref: "#/components/schemas/OrderDirection" + description: Order direction + ProfileOrderBy: type: object required: @@ -5164,6 +5190,36 @@ components: - PUBLISHED go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentVersionStatus + DocumentVersionApprovalQuorumStatus: + type: string + enum: + - PENDING + - APPROVED + - REJECTED + - VOIDED + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentVersionApprovalQuorumStatus + + DocumentVersionApprovalQuorumOrderField: + type: string + enum: + - CREATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentVersionApprovalQuorumOrderField + + DocumentVersionApprovalDecisionState: + type: string + enum: + - PENDING + - APPROVED + - REJECTED + - VOIDED + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentVersionApprovalDecisionState + + DocumentVersionApprovalDecisionOrderField: + type: string + enum: + - CREATED_AT + go.probo.inc/mcpgen/type: go.probo.inc/probo/pkg/coredata.DocumentVersionApprovalDecisionOrderField + DocumentStatus: type: string enum: @@ -5362,6 +5418,83 @@ components: format: date-time description: Update timestamp + DocumentVersionApprovalQuorum: + type: object + required: + - id + - organization_id + - version_id + - status + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Approval quorum ID + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + version_id: + $ref: "#/components/schemas/GID" + description: Document version ID + status: + $ref: "#/components/schemas/DocumentVersionApprovalQuorumStatus" + description: Approval quorum status + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + + DocumentVersionApprovalDecision: + type: object + required: + - id + - organization_id + - quorum_id + - approver_id + - state + - created_at + - updated_at + properties: + id: + $ref: "#/components/schemas/GID" + description: Approval decision ID + organization_id: + $ref: "#/components/schemas/GID" + description: Organization ID + quorum_id: + $ref: "#/components/schemas/GID" + description: Approval quorum ID + approver_id: + $ref: "#/components/schemas/GID" + description: Approver profile ID + state: + $ref: "#/components/schemas/DocumentVersionApprovalDecisionState" + description: Decision state + comment: + type: + - string + - "null" + description: Approval or rejection comment + decided_at: + type: + - string + - "null" + format: date-time + description: Decision timestamp + created_at: + type: string + format: date-time + description: Creation timestamp + updated_at: + type: string + format: date-time + description: Update timestamp + DocumentVersionSignature: type: object required: @@ -5876,6 +6009,110 @@ components: document_version: $ref: "#/components/schemas/DocumentVersion" + ListDocumentVersionApprovalQuorumsInput: + type: object + required: + - document_version_id + properties: + document_version_id: + $ref: "#/components/schemas/GID" + description: Document version ID + order_by: + $ref: "#/components/schemas/DocumentVersionApprovalQuorumOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + + ListDocumentVersionApprovalQuorumsOutput: + type: object + required: + - approval_quorums + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + approval_quorums: + type: array + items: + $ref: "#/components/schemas/DocumentVersionApprovalQuorum" + + GetDocumentVersionApprovalQuorumInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Approval quorum ID + + GetDocumentVersionApprovalQuorumOutput: + type: object + required: + - approval_quorum + properties: + approval_quorum: + $ref: "#/components/schemas/DocumentVersionApprovalQuorum" + + ListDocumentVersionApprovalDecisionsInput: + type: object + required: + - quorum_id + properties: + quorum_id: + $ref: "#/components/schemas/GID" + description: Approval quorum ID + order_by: + $ref: "#/components/schemas/DocumentVersionApprovalDecisionOrderBy" + description: Order by + size: + type: integer + description: Page size + cursor: + $ref: "#/components/schemas/CursorKey" + description: Page cursor + filter: + type: object + properties: + states: + type: array + items: + $ref: "#/components/schemas/DocumentVersionApprovalDecisionState" + description: Filter by decision states + + ListDocumentVersionApprovalDecisionsOutput: + type: object + required: + - approval_decisions + properties: + next_cursor: + $ref: "#/components/schemas/CursorKey" + description: Next cursor + approval_decisions: + type: array + items: + $ref: "#/components/schemas/DocumentVersionApprovalDecision" + + GetDocumentVersionApprovalDecisionInput: + type: object + required: + - id + properties: + id: + $ref: "#/components/schemas/GID" + description: Approval decision ID + + GetDocumentVersionApprovalDecisionOutput: + type: object + required: + - approval_decision + properties: + approval_decision: + $ref: "#/components/schemas/DocumentVersionApprovalDecision" + SendSigningNotificationsInput: type: object required: @@ -9175,3 +9412,39 @@ tools: $ref: "#/components/schemas/ListWebhookEventsInput" outputSchema: $ref: "#/components/schemas/ListWebhookEventsOutput" + - name: listDocumentVersionApprovalQuorums + description: List approval quorums for a document version + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListDocumentVersionApprovalQuorumsInput" + outputSchema: + $ref: "#/components/schemas/ListDocumentVersionApprovalQuorumsOutput" + - name: getDocumentVersionApprovalQuorum + description: Get a document version approval quorum by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetDocumentVersionApprovalQuorumInput" + outputSchema: + $ref: "#/components/schemas/GetDocumentVersionApprovalQuorumOutput" + - name: listDocumentVersionApprovalDecisions + description: List approval decisions for an approval quorum + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/ListDocumentVersionApprovalDecisionsInput" + outputSchema: + $ref: "#/components/schemas/ListDocumentVersionApprovalDecisionsOutput" + - name: getDocumentVersionApprovalDecision + description: Get a document version approval decision by ID + hints: + readonly: true + idempotent: true + inputSchema: + $ref: "#/components/schemas/GetDocumentVersionApprovalDecisionInput" + outputSchema: + $ref: "#/components/schemas/GetDocumentVersionApprovalDecisionOutput" diff --git a/pkg/server/api/mcp/v1/types/document.go b/pkg/server/api/mcp/v1/types/document.go index 3e1adef51..ec0dda7a9 100644 --- a/pkg/server/api/mcp/v1/types/document.go +++ b/pkg/server/api/mcp/v1/types/document.go @@ -190,3 +190,64 @@ func NewListDocumentVersionSignaturesOutput(signaturePage *page.Page[*coredata.D DocumentVersionSignatures: signatures, } } + +func NewDocumentVersionApprovalQuorum(q *coredata.DocumentVersionApprovalQuorum) *DocumentVersionApprovalQuorum { + return &DocumentVersionApprovalQuorum{ + ID: q.ID, + OrganizationID: q.OrganizationID, + VersionID: q.VersionID, + Status: q.Status, + CreatedAt: q.CreatedAt, + UpdatedAt: q.UpdatedAt, + } +} + +func NewListDocumentVersionApprovalQuorumsOutput(quorumPage *page.Page[*coredata.DocumentVersionApprovalQuorum, coredata.DocumentVersionApprovalQuorumOrderField]) ListDocumentVersionApprovalQuorumsOutput { + quorums := make([]*DocumentVersionApprovalQuorum, 0, len(quorumPage.Data)) + for _, q := range quorumPage.Data { + quorums = append(quorums, NewDocumentVersionApprovalQuorum(q)) + } + + var nextCursor *page.CursorKey + if len(quorumPage.Data) > 0 { + cursorKey := quorumPage.Data[len(quorumPage.Data)-1].CursorKey(quorumPage.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListDocumentVersionApprovalQuorumsOutput{ + NextCursor: nextCursor, + ApprovalQuorums: quorums, + } +} + +func NewDocumentVersionApprovalDecision(d *coredata.DocumentVersionApprovalDecision) *DocumentVersionApprovalDecision { + return &DocumentVersionApprovalDecision{ + ID: d.ID, + OrganizationID: d.OrganizationID, + QuorumID: d.QuorumID, + ApproverID: d.ApproverID, + State: d.State, + Comment: d.Comment, + DecidedAt: d.DecidedAt, + CreatedAt: d.CreatedAt, + UpdatedAt: d.UpdatedAt, + } +} + +func NewListDocumentVersionApprovalDecisionsOutput(decisionPage *page.Page[*coredata.DocumentVersionApprovalDecision, coredata.DocumentVersionApprovalDecisionOrderField]) ListDocumentVersionApprovalDecisionsOutput { + decisions := make([]*DocumentVersionApprovalDecision, 0, len(decisionPage.Data)) + for _, d := range decisionPage.Data { + decisions = append(decisions, NewDocumentVersionApprovalDecision(d)) + } + + var nextCursor *page.CursorKey + if len(decisionPage.Data) > 0 { + cursorKey := decisionPage.Data[len(decisionPage.Data)-1].CursorKey(decisionPage.Cursor.OrderBy.Field) + nextCursor = &cursorKey + } + + return ListDocumentVersionApprovalDecisionsOutput{ + NextCursor: nextCursor, + ApprovalDecisions: decisions, + } +}