Files
probo/pkg/server/api/console/v1/measure_resolvers.go
Sacha Al Himdani c635492f75 SOA as document: replace export with publish workflow
Statements of Applicability are no longer exported as one-off PDFs.
Instead, each SOA owns a persistent document that accumulates versions
over time, following the same publish/approve lifecycle as authored
documents.

Publishing without approvers publishes immediately; publishing with
approvers creates a draft pending approval via the existing quorum
system. SOAs can also store default approvers that are pre-populated in
the publish dialog.

The SOA is removed from the snapshot system — applicability statements
are now queried directly (snapshot_id IS NULL) rather than through
snapshot copies.

A standalone migration script (cmd/migrate-soa-snapshots-to-documents)
converts existing SOA snapshots into documents with proper ProseMirror
content, preserving version history and approval decisions.

Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
2026-04-16 00:01:13 +02:00

400 lines
14 KiB
Go

package console_v1
// This file will be automatically regenerated based on the schema, any resolver
// implementations
// will be copied through when generating and any unknown code will be moved to the end.
// Code generated by github.com/99designs/gqlgen version v0.17.87
import (
"context"
"encoding/json"
"errors"
"go.gearno.de/kit/log"
"go.probo.inc/probo/pkg/coredata"
"go.probo.inc/probo/pkg/page"
"go.probo.inc/probo/pkg/probo"
"go.probo.inc/probo/pkg/server/api/console/v1/schema"
"go.probo.inc/probo/pkg/server/api/console/v1/types"
"go.probo.inc/probo/pkg/server/gqlutils"
"go.probo.inc/probo/pkg/validator"
)
// Evidences is the resolver for the evidences field.
func (r *measureResolver) Evidences(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.EvidenceOrderBy) (*types.EvidenceConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionEvidenceList); err != nil {
return nil, err
}
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.EvidenceOrderField]{
Field: coredata.EvidenceOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.EvidenceOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := prb.Evidences.ListForMeasureID(ctx, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list measure evidences", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewEvidenceConnection(page, r, obj.ID), nil
}
// Tasks is the resolver for the tasks field.
func (r *measureResolver) Tasks(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.TaskOrderBy) (*types.TaskConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionTaskList); err != nil {
return nil, err
}
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.TaskOrderField]{
Field: coredata.TaskOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.TaskOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
page, err := prb.Tasks.ListForMeasureID(ctx, obj.ID, cursor)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list measure tasks", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewTaskConnection(page, r, obj.ID), nil
}
// Risks is the resolver for the risks field.
func (r *measureResolver) Risks(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.RiskOrderBy, filter *types.RiskFilter) (*types.RiskConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionRiskList); err != nil {
return nil, err
}
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.RiskOrderField]{
Field: coredata.RiskOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.RiskOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var riskFilter = coredata.NewRiskFilter(nil, nil)
if filter != nil {
riskFilter = coredata.NewRiskFilter(filter.Query, &filter.SnapshotID)
}
page, err := prb.Risks.ListForMeasureID(ctx, obj.ID, cursor, riskFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list measure risks", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewRiskConnection(page, r, obj.ID, riskFilter), nil
}
// Controls is the resolver for the controls field.
func (r *measureResolver) Controls(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.ControlOrderBy, filter *types.ControlFilter) (*types.ControlConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionControlList); err != nil {
return nil, err
}
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.ControlOrderField]{
Field: coredata.ControlOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.ControlOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var controlFilter = coredata.NewControlFilter(nil)
if filter != nil {
controlFilter = coredata.NewControlFilter(filter.Query)
}
page, err := prb.Controls.ListForMeasureID(ctx, obj.ID, cursor, controlFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list measure controls", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewControlConnection(page, r, obj.ID, controlFilter), nil
}
// Documents is the resolver for the documents field.
func (r *measureResolver) Documents(ctx context.Context, obj *types.Measure, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.DocumentOrderBy, filter *types.DocumentFilter) (*types.DocumentConnection, error) {
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentList); err != nil {
return nil, err
}
prb := r.ProboService(ctx, obj.ID.TenantID())
pageOrderBy := page.OrderBy[coredata.DocumentOrderField]{
Field: coredata.DocumentOrderFieldCreatedAt,
Direction: page.OrderDirectionDesc,
}
if orderBy != nil {
pageOrderBy = page.OrderBy[coredata.DocumentOrderField]{
Field: orderBy.Field,
Direction: orderBy.Direction,
}
}
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
var documentFilter = coredata.NewDocumentFilter(nil)
if filter != nil {
documentFilter = coredata.NewDocumentFilter(filter.Query).
WithWriteModes(filter.WriteModes).
WithDocumentTypes(filter.DocumentTypes).
WithClassifications(filter.Classifications)
}
pg, err := prb.Documents.ListForMeasureID(ctx, obj.ID, cursor, documentFilter)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot list documents", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return types.NewDocumentConnection(pg, r, obj.ID, documentFilter), nil
}
// Permission is the resolver for the permission field.
func (r *measureResolver) Permission(ctx context.Context, obj *types.Measure, action string) (bool, error) {
return r.Resolver.Permission(ctx, obj, action)
}
// TotalCount is the resolver for the totalCount field.
func (r *measureConnectionResolver) TotalCount(ctx context.Context, obj *types.MeasureConnection) (int, error) {
if err := r.authorize(ctx, obj.ParentID, probo.ActionMeasureList); err != nil {
return 0, err
}
prb := r.ProboService(ctx, obj.ParentID.TenantID())
switch obj.Resolver.(type) {
case *organizationResolver:
count, err := prb.Measures.CountForOrganizationID(ctx, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
case *controlResolver:
count, err := prb.Measures.CountForControlID(ctx, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
case *riskResolver:
count, err := prb.Measures.CountForRiskID(ctx, obj.ParentID, obj.Filters)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot count measures", log.Error(err))
return 0, gqlutils.Internal(ctx)
}
return count, nil
}
r.logger.ErrorCtx(ctx, "unsupported resolver")
return 0, gqlutils.Internal(ctx)
}
// // CreateMeasure is the resolver for the createMeasure field.
func (r *mutationResolver) CreateMeasure(ctx context.Context, input types.CreateMeasureInput) (*types.CreateMeasurePayload, error) {
if err := r.authorize(ctx, input.OrganizationID, probo.ActionMeasureCreate); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
measure, err := prb.Measures.Create(
ctx,
probo.CreateMeasureRequest{
OrganizationID: input.OrganizationID,
Name: input.Name,
Description: input.Description,
Category: input.Category,
},
)
if err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return nil, gqlutils.Conflict(ctx, err)
}
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot create measure", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CreateMeasurePayload{
MeasureEdge: types.NewMeasureEdge(measure, coredata.MeasureOrderFieldCreatedAt),
}, nil
}
// UpdateMeasure is the resolver for the updateMeasure field.
func (r *mutationResolver) UpdateMeasure(ctx context.Context, input types.UpdateMeasureInput) (*types.UpdateMeasurePayload, error) {
if err := r.authorize(ctx, input.ID, probo.ActionMeasureUpdate); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.ID.TenantID())
measure, err := prb.Measures.Update(
ctx,
probo.UpdateMeasureRequest{
ID: input.ID,
Name: input.Name,
Description: gqlutils.UnwrapOmittable(input.Description),
Category: input.Category,
State: input.State,
},
)
if err != nil {
if validationErrors, ok := errors.AsType[validator.ValidationErrors](err); ok {
return nil, gqlutils.InvalidValidationErrors(ctx, validationErrors)
}
r.logger.ErrorCtx(ctx, "cannot update measure", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.UpdateMeasurePayload{
Measure: types.NewMeasure(measure),
}, nil
}
// ImportMeasure is the resolver for the importMeasure field.
func (r *mutationResolver) ImportMeasure(ctx context.Context, input types.ImportMeasureInput) (*types.ImportMeasurePayload, error) {
if err := r.authorize(ctx, input.OrganizationID, probo.ActionMeasureImport); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
var req probo.ImportMeasureRequest
if err := json.NewDecoder(input.File.File).Decode(&req.Measures); err != nil {
r.logger.ErrorCtx(ctx, "cannot unmarshal measure", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
measures, err := prb.Measures.Import(ctx, input.OrganizationID, req)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot import measure", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
measureEdges := make([]*types.MeasureEdge, len(measures.Data))
for i, measure := range measures.Data {
measureEdges[i] = types.NewMeasureEdge(measure, coredata.MeasureOrderFieldCreatedAt)
}
return &types.ImportMeasurePayload{
MeasureEdges: measureEdges,
}, nil
}
// DeleteMeasure is the resolver for the deleteMeasure field.
func (r *mutationResolver) DeleteMeasure(ctx context.Context, input types.DeleteMeasureInput) (*types.DeleteMeasurePayload, error) {
if err := r.authorize(ctx, input.MeasureID, probo.ActionMeasureDelete); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.MeasureID.TenantID())
err := prb.Measures.Delete(ctx, input.MeasureID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot delete measure", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteMeasurePayload{
DeletedMeasureID: input.MeasureID,
}, nil
}
// CreateMeasureDocumentMapping is the resolver for the createMeasureDocumentMapping field.
func (r *mutationResolver) CreateMeasureDocumentMapping(ctx context.Context, input types.CreateMeasureDocumentMappingInput) (*types.CreateMeasureDocumentMappingPayload, error) {
if err := r.authorize(ctx, input.MeasureID, probo.ActionMeasureDocumentMappingCreate); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.MeasureID.TenantID())
measure, document, err := prb.Measures.CreateDocumentMapping(ctx, input.MeasureID, input.DocumentID)
if err != nil {
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
return nil, gqlutils.Conflict(ctx, err)
}
r.logger.ErrorCtx(ctx, "cannot create measure document mapping", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.CreateMeasureDocumentMappingPayload{
MeasureEdge: types.NewMeasureEdge(measure, coredata.MeasureOrderFieldCreatedAt),
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldTitle),
}, nil
}
// DeleteMeasureDocumentMapping is the resolver for the deleteMeasureDocumentMapping field.
func (r *mutationResolver) DeleteMeasureDocumentMapping(ctx context.Context, input types.DeleteMeasureDocumentMappingInput) (*types.DeleteMeasureDocumentMappingPayload, error) {
if err := r.authorize(ctx, input.MeasureID, probo.ActionMeasureDocumentMappingDelete); err != nil {
return nil, err
}
prb := r.ProboService(ctx, input.MeasureID.TenantID())
measure, document, err := prb.Measures.DeleteDocumentMapping(ctx, input.MeasureID, input.DocumentID)
if err != nil {
r.logger.ErrorCtx(ctx, "cannot delete measure document mapping", log.Error(err))
return nil, gqlutils.Internal(ctx)
}
return &types.DeleteMeasureDocumentMappingPayload{
DeletedMeasureID: measure.ID,
DeletedDocumentID: document.ID,
}, nil
}
// Measure returns schema.MeasureResolver implementation.
func (r *Resolver) Measure() schema.MeasureResolver { return &measureResolver{r} }
// MeasureConnection returns schema.MeasureConnectionResolver implementation.
func (r *Resolver) MeasureConnection() schema.MeasureConnectionResolver {
return &measureConnectionResolver{r}
}
type measureResolver struct{ *Resolver }
type measureConnectionResolver struct{ *Resolver }