Assets as document: replace snapshot with publish workflow
Remove assets from the snapshot system and replace with a publish-based document workflow that generates versioned ProseMirror documents. - Remove snapshot_id/source_id from asset and asset_vendor models - Delete AssetFilter (no longer needed without snapshot filtering) - Add PublishAssetList service, GraphQL mutation, MCP tool, CLI command, and n8n operation - Add asset_list_document_id column to generated_documents table - Generate ProseMirror documents with asset inventory tables (name, type, amount, data types stored, owner, vendors) - Add AssetListDocument resolver on Organization type - Update frontend to remove snapshot routes/params and add publish dialog - Add e2e tests for asset publish (immediate, with approvers, reuse, RBAC) - Add migration script for converting legacy asset snapshots to documents - Exclude ASSETS from snapshot type lists and e2e snapshot tests - Move generated_documents SQL to coredata methods on Datum and Asset - Clear generated document and SOA references on soft delete and archive Signed-off-by: Sacha Al Himdani <sacha@getprobo.com>
This commit is contained in:
@@ -117,12 +117,7 @@ func (r *assetConnectionResolver) TotalCount(ctx context.Context, obj *types.Ass
|
||||
|
||||
switch obj.Resolver.(type) {
|
||||
case *organizationResolver:
|
||||
assetFilter := coredata.NewAssetFilter(nil)
|
||||
if obj.Filter != nil {
|
||||
assetFilter = coredata.NewAssetFilter(&obj.Filter.SnapshotID)
|
||||
}
|
||||
|
||||
count, err := prb.Assets.CountForOrganizationID(ctx, obj.ParentID, assetFilter)
|
||||
count, err := prb.Assets.CountForOrganizationID(ctx, obj.ParentID)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot count assets", log.Error(err))
|
||||
return 0, gqlutils.Internal(ctx)
|
||||
@@ -423,6 +418,29 @@ func (r *mutationResolver) PublishDataList(ctx context.Context, input types.Publ
|
||||
}, nil
|
||||
}
|
||||
|
||||
// PublishAssetList is the resolver for the publishAssetList field.
|
||||
func (r *mutationResolver) PublishAssetList(ctx context.Context, input types.PublishAssetListInput) (*types.PublishAssetListPayload, error) {
|
||||
if err := r.authorize(ctx, input.OrganizationID, probo.ActionAssetPublish); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, input.OrganizationID.TenantID())
|
||||
|
||||
document, documentVersion, err := prb.GeneratedDocuments.PublishAssetList(ctx, input.OrganizationID, input.ApproverIds)
|
||||
if err != nil {
|
||||
if errors.Is(err, coredata.ErrResourceAlreadyExists) {
|
||||
return nil, gqlutils.Conflict(ctx, err)
|
||||
}
|
||||
r.logger.ErrorCtx(ctx, "cannot publish asset list", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return &types.PublishAssetListPayload{
|
||||
DocumentEdge: types.NewDocumentEdge(document, coredata.DocumentOrderFieldCreatedAt),
|
||||
DocumentVersionEdge: types.NewDocumentVersionEdge(documentVersion, coredata.DocumentVersionOrderFieldCreatedAt),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Asset returns schema.AssetResolver implementation.
|
||||
func (r *Resolver) Asset() schema.AssetResolver { return &assetResolver{r} }
|
||||
|
||||
|
||||
@@ -62,13 +62,8 @@ input DatumOrder
|
||||
field: DatumOrderField!
|
||||
}
|
||||
|
||||
input AssetFilter {
|
||||
snapshotId: ID
|
||||
}
|
||||
|
||||
type Asset implements Node {
|
||||
id: ID!
|
||||
snapshotId: ID
|
||||
name: String!
|
||||
amount: Int!
|
||||
owner: Profile! @goField(forceResolver: true)
|
||||
@@ -148,6 +143,9 @@ extend type Mutation {
|
||||
publishDataList(
|
||||
input: PublishDataListInput!
|
||||
): PublishDataListPayload!
|
||||
publishAssetList(
|
||||
input: PublishAssetListInput!
|
||||
): PublishAssetListPayload!
|
||||
}
|
||||
|
||||
input CreateAssetInput {
|
||||
@@ -227,3 +225,13 @@ type PublishDataListPayload {
|
||||
documentEdge: DocumentEdge!
|
||||
documentVersionEdge: DocumentVersionEdge!
|
||||
}
|
||||
|
||||
input PublishAssetListInput {
|
||||
organizationId: ID!
|
||||
approverIds: [ID!]
|
||||
}
|
||||
|
||||
type PublishAssetListPayload {
|
||||
documentEdge: DocumentEdge!
|
||||
documentVersionEdge: DocumentVersionEdge!
|
||||
}
|
||||
|
||||
@@ -121,13 +121,14 @@ type Organization implements Node {
|
||||
orderBy: AccessReviewCampaignOrder
|
||||
): AccessReviewCampaignConnection! @goField(forceResolver: true)
|
||||
|
||||
assetListDocument: Document @goField(forceResolver: true)
|
||||
|
||||
assets(
|
||||
first: Int
|
||||
after: CursorKey
|
||||
last: Int
|
||||
before: CursorKey
|
||||
orderBy: AssetOrder
|
||||
filter: AssetFilter = { snapshotId: null }
|
||||
): AssetConnection! @goField(forceResolver: true)
|
||||
|
||||
dataListDocument: Document @goField(forceResolver: true)
|
||||
|
||||
@@ -3,7 +3,6 @@ enum SnapshotsType
|
||||
RISKS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeRisks")
|
||||
VENDORS
|
||||
@goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeVendors")
|
||||
ASSETS @goEnum(value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeAssets")
|
||||
FINDINGS
|
||||
@goEnum(
|
||||
value: "go.probo.inc/probo/pkg/coredata.SnapshotsTypeFindings"
|
||||
|
||||
@@ -221,8 +221,32 @@ func (r *organizationResolver) AccessReviewCampaigns(ctx context.Context, obj *t
|
||||
return types.NewAccessReviewCampaignConnection(p, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// AssetListDocument is the resolver for the assetListDocument field.
|
||||
func (r *organizationResolver) AssetListDocument(ctx context.Context, obj *types.Organization) (*types.Document, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionDocumentGet); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
prb := r.ProboService(ctx, obj.ID.TenantID())
|
||||
|
||||
assetDocumentID, err := prb.GeneratedDocuments.GetAssetListDocumentID(ctx, obj.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get asset list document ID: %w", err)
|
||||
}
|
||||
if assetDocumentID == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
doc, err := prb.Documents.Get(ctx, *assetDocumentID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("cannot get asset list document: %w", err)
|
||||
}
|
||||
|
||||
return types.NewDocument(doc), nil
|
||||
}
|
||||
|
||||
// Assets is the resolver for the assets field.
|
||||
func (r *organizationResolver) Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy, filter *types.AssetFilter) (*types.AssetConnection, error) {
|
||||
func (r *organizationResolver) Assets(ctx context.Context, obj *types.Organization, first *int, after *page.CursorKey, last *int, before *page.CursorKey, orderBy *types.AssetOrderBy) (*types.AssetConnection, error) {
|
||||
if err := r.authorize(ctx, obj.ID, probo.ActionAssetList); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -242,18 +266,13 @@ func (r *organizationResolver) Assets(ctx context.Context, obj *types.Organizati
|
||||
|
||||
cursor := types.NewCursor(first, after, last, before, pageOrderBy)
|
||||
|
||||
assetFilter := coredata.NewAssetFilter(nil)
|
||||
if filter != nil {
|
||||
assetFilter = coredata.NewAssetFilter(&filter.SnapshotID)
|
||||
}
|
||||
|
||||
page, err := prb.Assets.ListForOrganizationID(ctx, obj.ID, cursor, assetFilter)
|
||||
page, err := prb.Assets.ListForOrganizationID(ctx, obj.ID, cursor)
|
||||
if err != nil {
|
||||
r.logger.ErrorCtx(ctx, "cannot list organization assets", log.Error(err))
|
||||
return nil, gqlutils.Internal(ctx)
|
||||
}
|
||||
|
||||
return types.NewAssetConnection(page, r, obj.ID, filter), nil
|
||||
return types.NewAssetConnection(page, r, obj.ID), nil
|
||||
}
|
||||
|
||||
// DataListDocument is the resolver for the dataListDocument field.
|
||||
|
||||
@@ -30,7 +30,6 @@ type (
|
||||
|
||||
Resolver any
|
||||
ParentID gid.GID
|
||||
Filter *AssetFilter
|
||||
}
|
||||
)
|
||||
|
||||
@@ -38,7 +37,6 @@ func NewAssetConnection(
|
||||
p *page.Page[*coredata.Asset, coredata.AssetOrderField],
|
||||
resolver any,
|
||||
parentID gid.GID,
|
||||
filter *AssetFilter,
|
||||
) *AssetConnection {
|
||||
edges := make([]*AssetEdge, len(p.Data))
|
||||
for i, asset := range p.Data {
|
||||
@@ -51,7 +49,6 @@ func NewAssetConnection(
|
||||
|
||||
Resolver: resolver,
|
||||
ParentID: parentID,
|
||||
Filter: filter,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,10 +61,9 @@ func NewAssetEdge(asset *coredata.Asset, orderField coredata.AssetOrderField) *A
|
||||
|
||||
func NewAsset(asset *coredata.Asset) *Asset {
|
||||
return &Asset{
|
||||
ID: asset.ID,
|
||||
SnapshotID: asset.SnapshotID,
|
||||
Name: asset.Name,
|
||||
Amount: asset.Amount,
|
||||
ID: asset.ID,
|
||||
Name: asset.Name,
|
||||
Amount: asset.Amount,
|
||||
Owner: &Profile{
|
||||
ID: asset.OwnerID,
|
||||
},
|
||||
|
||||
@@ -559,13 +559,7 @@ func (r *Resolver) ListAssetsTool(ctx context.Context, req *mcp.CallToolRequest,
|
||||
|
||||
cursor := types.NewCursor(input.Size, input.Cursor, pageOrderBy)
|
||||
|
||||
noSnapshot := (*gid.GID)(nil)
|
||||
assetFilter := coredata.NewAssetFilter(&noSnapshot)
|
||||
if input.Filter != nil {
|
||||
assetFilter = coredata.NewAssetFilter(&input.Filter.SnapshotID)
|
||||
}
|
||||
|
||||
page, err := prb.Assets.ListForOrganizationID(ctx, input.OrganizationID, cursor, assetFilter)
|
||||
page, err := prb.Assets.ListForOrganizationID(ctx, input.OrganizationID, cursor)
|
||||
if err != nil {
|
||||
panic(fmt.Errorf("cannot list organization assets: %w", err))
|
||||
}
|
||||
@@ -4022,3 +4016,19 @@ func (r *Resolver) PublishDataListTool(ctx context.Context, req *mcp.CallToolReq
|
||||
DocumentVersionID: documentVersion.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (r *Resolver) PublishAssetListTool(ctx context.Context, req *mcp.CallToolRequest, input *types.PublishAssetListInput) (*mcp.CallToolResult, types.PublishAssetListOutput, error) {
|
||||
r.MustAuthorize(ctx, input.OrganizationID, probo.ActionAssetPublish)
|
||||
|
||||
svc := r.ProboService(ctx, input.OrganizationID)
|
||||
|
||||
document, documentVersion, err := svc.GeneratedDocuments.PublishAssetList(ctx, input.OrganizationID, input.ApproverIds)
|
||||
if err != nil {
|
||||
return nil, types.PublishAssetListOutput{}, fmt.Errorf("cannot publish asset list: %w", err)
|
||||
}
|
||||
|
||||
return nil, types.PublishAssetListOutput{
|
||||
DocumentID: document.ID,
|
||||
DocumentVersionID: documentVersion.ID,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -2003,11 +2003,6 @@ components:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
snapshot_id:
|
||||
type:
|
||||
- string
|
||||
- "null"
|
||||
description: Snapshot ID
|
||||
name:
|
||||
type: string
|
||||
description: Asset name
|
||||
@@ -2049,15 +2044,6 @@ components:
|
||||
cursor:
|
||||
$ref: "#/components/schemas/CursorKey"
|
||||
description: Page cursor
|
||||
filter:
|
||||
type: object
|
||||
properties:
|
||||
snapshot_id:
|
||||
anyOf:
|
||||
- $ref: "#/components/schemas/GID"
|
||||
- type: "null"
|
||||
description: Filter by snapshot ID. Defaults to null, which returns only assets with no snapshot (current live data). Pass a specific snapshot ID to retrieve assets as they were at that snapshot.
|
||||
default: null
|
||||
|
||||
ListAssetsOutput:
|
||||
type: object
|
||||
@@ -4999,7 +4985,6 @@ components:
|
||||
enum:
|
||||
- RISKS
|
||||
- VENDORS
|
||||
- ASSETS
|
||||
- NONCONFORMITIES
|
||||
- OBLIGATIONS
|
||||
- CONTINUAL_IMPROVEMENTS
|
||||
@@ -6584,6 +6569,33 @@ components:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created document version ID
|
||||
|
||||
PublishAssetListInput:
|
||||
type: object
|
||||
required:
|
||||
- organization_id
|
||||
properties:
|
||||
organization_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Organization ID
|
||||
approver_ids:
|
||||
type: array
|
||||
items:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Optional approver profile IDs. If provided, creates a draft pending approval instead of publishing immediately.
|
||||
|
||||
PublishAssetListOutput:
|
||||
type: object
|
||||
required:
|
||||
- document_id
|
||||
- document_version_id
|
||||
properties:
|
||||
document_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created or updated document ID
|
||||
document_version_id:
|
||||
$ref: "#/components/schemas/GID"
|
||||
description: Created document version ID
|
||||
|
||||
PublishStatementOfApplicabilityInput:
|
||||
type: object
|
||||
required:
|
||||
@@ -8650,7 +8662,7 @@ tools:
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/GetSnapshotOutput"
|
||||
- name: takeSnapshot
|
||||
description: Take a snapshot of a collection of objects (risks, vendors, assets, findings, obligations, or processing activities)
|
||||
description: Take a snapshot of a collection of objects (risks, vendors, findings, obligations, or processing activities)
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
@@ -8870,6 +8882,14 @@ tools:
|
||||
$ref: "#/components/schemas/PublishDataListInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishDataListOutput"
|
||||
- name: publishAssetList
|
||||
description: Publish the asset list for an organization as a document. If a document already exists, a new version is created.
|
||||
hints:
|
||||
readonly: false
|
||||
inputSchema:
|
||||
$ref: "#/components/schemas/PublishAssetListInput"
|
||||
outputSchema:
|
||||
$ref: "#/components/schemas/PublishAssetListOutput"
|
||||
- name: publishStatementOfApplicability
|
||||
description: Publish a statement of applicability as a document. If a document already exists, a new version is created.
|
||||
hints:
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
)
|
||||
|
||||
func NewAsset(a *coredata.Asset) *Asset {
|
||||
asset := &Asset{
|
||||
return &Asset{
|
||||
ID: a.ID,
|
||||
Name: a.Name,
|
||||
Amount: a.Amount,
|
||||
@@ -31,13 +31,6 @@ func NewAsset(a *coredata.Asset) *Asset {
|
||||
CreatedAt: a.CreatedAt,
|
||||
UpdatedAt: a.UpdatedAt,
|
||||
}
|
||||
|
||||
if a.SnapshotID != nil {
|
||||
s := a.SnapshotID.String()
|
||||
asset.SnapshotID = &s
|
||||
}
|
||||
|
||||
return asset
|
||||
}
|
||||
|
||||
func NewListAssetsOutput(assetPage *page.Page[*coredata.Asset, coredata.AssetOrderField]) ListAssetsOutput {
|
||||
|
||||
Reference in New Issue
Block a user