Skip to content

Commit ae21b50

Browse files
authored
Document extraction for control + subcontrol refs should be async (#2306)
1 parent aef5b3b commit ae21b50

10 files changed

Lines changed: 181 additions & 64 deletions

File tree

.task/checksum/generate-ent-smart

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2205155eb89e6bff7f80240b5dc043
1+
5c4fd560f5af11c2e8c76c1dfa910647
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
76e1ae53815c369bd25a0d735abf7690
1+
b7aee90061ab56e45ce54858de8b8385
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
71434179a5ad10caed7fabf528bcdcc9bec5ce03b425b66c66b2b1034c654a9e
1+
f227db62ffc3fc3f9de100b8e18ac45a2f0aa1fb53a1d1a5524c047375d10838
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
d6ab508c9091731765993d63a4644d543db1c74f2a41c0eea915ac0b4c59f5c9
1+
a61c0d6804b980ca0ae94ae836db20e3e28dd791bc1f419d0df6ca0f3e96d2cd

internal/ent/hooks/documentextract.go

Lines changed: 9 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -7,29 +7,21 @@ import (
77
"strings"
88

99
"entgo.io/ent"
10+
11+
"github.com/theopenlane/core/internal/ent/generated"
1012
"github.com/theopenlane/core/internal/ent/generated/control"
1113
"github.com/theopenlane/core/internal/ent/generated/hook"
1214
"github.com/theopenlane/core/pkg/logx"
1315
)
1416

15-
// addControlsMutation allows for adding to the controls edge in a mutation
16-
type addControlsMutation interface {
17-
AddControlIDs(ids ...string)
18-
}
19-
20-
// addSubcontrolsMutation allows for adding to the subcontrols edge in a mutation
21-
type addSubcontrolsMutation interface {
22-
AddSubcontrolIDs(ids ...string)
23-
}
24-
2517
// versionMutation allows for setting the revision field in a mutation
2618
type versionMutation interface {
2719
SetRevision(revision string)
2820
}
2921

30-
// HookParseAssociations is an ent hook that parses associations from a document
31-
// such as referenced controls and adds the necessary edges
32-
func HookParseAssociations() ent.Hook {
22+
// HookDetailsVersion is an ent hook that parses the versions from the details of a document
23+
// creation
24+
func HookDetailsVersion() ent.Hook {
3325
return hook.If(func(next ent.Mutator) ent.Mutator {
3426
return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) {
3527
mut := m.(detailsMutation)
@@ -39,30 +31,10 @@ func HookParseAssociations() ent.Hook {
3931
return next.Mutate(ctx, m)
4032
}
4133

42-
edgeLinks := getDocumentAssociations(ctx, mut)
43-
44-
if edgeLinks == nil {
45-
return next.Mutate(ctx, m)
46-
}
47-
48-
if len(edgeLinks.controlIDs) > 0 {
49-
conMut, ok := mut.(addControlsMutation)
50-
if ok {
51-
conMut.AddControlIDs(edgeLinks.controlIDs...)
52-
}
53-
}
54-
55-
if len(edgeLinks.subcontrolIDs) > 0 {
56-
subconMut, ok := mut.(addSubcontrolsMutation)
57-
if ok {
58-
subconMut.AddSubcontrolIDs(edgeLinks.subcontrolIDs...)
59-
}
60-
}
61-
62-
if edgeLinks.version != "" {
34+
if version := findVersion(details); version != "" {
6335
verMut, ok := mut.(versionMutation)
6436
if ok {
65-
verMut.SetRevision(edgeLinks.version)
37+
verMut.SetRevision(version)
6638
}
6739
}
6840

@@ -74,31 +46,10 @@ func HookParseAssociations() ent.Hook {
7446

7547
// edgeLinks is a struct that holds the IDs of associated entities that should be linked to the document being created or updated, such as controlIDs
7648
type edgeLinks struct {
77-
version string
7849
controlIDs []string
7950
subcontrolIDs []string
8051
}
8152

82-
// getDocumentAssociations will read text details and try to extract any associations to other entities in the system, such as referenced control IDs, asset IDs, and identity holder IDs. This is a placeholder implementation and should be replaced with actual parsing logic based on the expected format of the details text.
83-
func getDocumentAssociations(ctx context.Context, m detailsMutation) *edgeLinks {
84-
orgControls := getOrganizationControls(ctx, m)
85-
86-
if orgControls == nil {
87-
return nil
88-
}
89-
90-
details, ok := m.Details()
91-
if !ok || details == "" {
92-
return nil
93-
}
94-
95-
edgeLinks := findControlMatches(details, orgControls)
96-
97-
edgeLinks.version = findVersion(details)
98-
99-
return edgeLinks
100-
}
101-
10253
// findVersion attempts to find the version of the document in the details by
10354
// looking for a line that starts with "Version:" and extracting the version number from it. It returns the version in semver format if found, or an empty string if not found.
10455
func findVersion(details string) string {
@@ -178,9 +129,9 @@ type controlInfo struct {
178129
// controlMapping contains the refCode with additional info
179130
type controlMapping map[string]controlInfo
180131

181-
func getOrganizationControls(ctx context.Context, m detailsMutation) controlMapping {
132+
func getOrganizationControlsFromClient(ctx context.Context, client *generated.Client) controlMapping {
182133
result := controlMapping{}
183-
controls, err := m.Client().Control.Query().Where(
134+
controls, err := client.Control.Query().Where(
184135
control.IsTrustCenterControl(false),
185136
control.SystemOwned(false),
186137
).WithSubcontrols().All(ctx)
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
package hooks
2+
3+
import (
4+
"context"
5+
6+
"entgo.io/ent"
7+
"github.com/samber/lo"
8+
9+
"github.com/theopenlane/core/internal/ent/eventqueue"
10+
"github.com/theopenlane/core/internal/ent/generated"
11+
"github.com/theopenlane/core/internal/workflows"
12+
"github.com/theopenlane/core/pkg/gala"
13+
)
14+
15+
// RegisterGalaDocumentAssociationListeners registers listeners that link
16+
// referenced controls to documents asynchronously after document creation.
17+
func RegisterGalaDocumentAssociationListeners(registry *gala.Registry) ([]gala.ListenerID, error) {
18+
return gala.RegisterListeners(registry,
19+
documentAssociationDefinition(generated.TypeActionPlan),
20+
documentAssociationDefinition(generated.TypeInternalPolicy),
21+
documentAssociationDefinition(generated.TypeProcedure),
22+
)
23+
}
24+
25+
func documentAssociationDefinition(schemaType string) gala.Definition[eventqueue.MutationGalaPayload] {
26+
topic := eventqueue.MutationTopic(eventqueue.MutationConcernDirect, schemaType)
27+
28+
return gala.Definition[eventqueue.MutationGalaPayload]{
29+
Topic: topic,
30+
Name: "document.associations." + schemaType,
31+
Operations: []string{ent.OpCreate.String()},
32+
Handle: handleDocumentAssociationCreated,
33+
}
34+
}
35+
36+
func handleDocumentAssociationCreated(ctx gala.HandlerContext, payload eventqueue.MutationGalaPayload) error {
37+
ctx, client, ok := eventqueue.ClientFromHandler(ctx)
38+
if !ok {
39+
return nil
40+
}
41+
42+
documentID, ok := eventqueue.MutationEntityID(payload, ctx.Envelope.Headers.Properties)
43+
if !ok || documentID == "" {
44+
return nil
45+
}
46+
47+
switch payload.MutationType {
48+
case generated.TypeActionPlan:
49+
return parseActionPlanAssociations(ctx.Context, client, documentID)
50+
case generated.TypeInternalPolicy:
51+
return parseInternalPolicyAssociations(ctx.Context, client, documentID)
52+
case generated.TypeProcedure:
53+
return parseProcedureAssociations(ctx.Context, client, documentID)
54+
default:
55+
return nil
56+
}
57+
}
58+
59+
func parseActionPlanAssociations(ctx context.Context, client *generated.Client, documentID string) error {
60+
doc, err := client.ActionPlan.Get(ctx, documentID)
61+
if err != nil {
62+
if generated.IsNotFound(err) {
63+
return nil
64+
}
65+
66+
return err
67+
}
68+
69+
links := getDocumentAssociationsForDetails(ctx, client, doc.Details)
70+
if links == nil || len(links.controlIDs) == 0 {
71+
return nil
72+
}
73+
74+
return client.ActionPlan.UpdateOneID(doc.ID).
75+
SetRevision(doc.Revision).
76+
AddControlIDs(lo.Uniq(links.controlIDs)...).
77+
Exec(workflows.AllowContext(ctx))
78+
}
79+
80+
func parseInternalPolicyAssociations(ctx context.Context, client *generated.Client, documentID string) error {
81+
doc, err := client.InternalPolicy.Get(ctx, documentID)
82+
if err != nil {
83+
if generated.IsNotFound(err) {
84+
return nil
85+
}
86+
87+
return err
88+
}
89+
90+
links := getDocumentAssociationsForDetails(ctx, client, doc.Details)
91+
if links == nil || !links.hasAssociations() {
92+
return nil
93+
}
94+
95+
update := client.InternalPolicy.UpdateOneID(doc.ID).SetRevision(doc.Revision)
96+
if len(links.controlIDs) > 0 {
97+
update.AddControlIDs(lo.Uniq(links.controlIDs)...)
98+
}
99+
100+
if len(links.subcontrolIDs) > 0 {
101+
update.AddSubcontrolIDs(lo.Uniq(links.subcontrolIDs)...)
102+
}
103+
104+
return update.Exec(workflows.AllowContext(ctx))
105+
}
106+
107+
func parseProcedureAssociations(ctx context.Context, client *generated.Client, documentID string) error {
108+
doc, err := client.Procedure.Get(ctx, documentID)
109+
if err != nil {
110+
if generated.IsNotFound(err) {
111+
return nil
112+
}
113+
114+
return err
115+
}
116+
117+
links := getDocumentAssociationsForDetails(ctx, client, doc.Details)
118+
if links == nil || !links.hasAssociations() {
119+
return nil
120+
}
121+
122+
update := client.Procedure.UpdateOneID(doc.ID).SetRevision(doc.Revision)
123+
if len(links.controlIDs) > 0 {
124+
update.AddControlIDs(lo.Uniq(links.controlIDs)...)
125+
}
126+
127+
if len(links.subcontrolIDs) > 0 {
128+
update.AddSubcontrolIDs(lo.Uniq(links.subcontrolIDs)...)
129+
}
130+
131+
return update.Exec(workflows.AllowContext(ctx))
132+
}
133+
134+
func getDocumentAssociationsForDetails(ctx context.Context, client *generated.Client, details string) *edgeLinks {
135+
if details == "" {
136+
return nil
137+
}
138+
139+
orgControls := getOrganizationControlsFromClient(ctx, client)
140+
if orgControls == nil {
141+
return nil
142+
}
143+
144+
return findControlMatches(details, orgControls)
145+
}
146+
147+
func (e *edgeLinks) hasAssociations() bool {
148+
return e != nil && (len(e.controlIDs) > 0 || len(e.subcontrolIDs) > 0)
149+
}

internal/ent/hooks/listeners_gala_registration_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,22 @@ func TestRegisterGalaIdentityResolutionListeners(t *testing.T) {
110110
require.False(t, registry.InterestedIn(topic, ent.OpDelete.String()))
111111
}
112112

113+
func TestRegisterGalaDocumentAssociationListeners(t *testing.T) {
114+
t.Parallel()
115+
116+
registry := gala.NewRegistry()
117+
118+
ids, err := RegisterGalaDocumentAssociationListeners(registry)
119+
require.NoError(t, err)
120+
require.Len(t, ids, 3)
121+
122+
for _, schemaType := range []string{entgen.TypeActionPlan, entgen.TypeInternalPolicy, entgen.TypeProcedure} {
123+
topic := eventqueue.MutationTopicName(eventqueue.MutationConcernDirect, schemaType)
124+
require.True(t, registry.InterestedIn(topic, ent.OpCreate.String()))
125+
require.False(t, registry.InterestedIn(topic, ent.OpUpdate.String()))
126+
}
127+
}
128+
113129
func TestRegisterGalaNotificationListeners(t *testing.T) {
114130
t.Parallel()
115131

internal/ent/schema/mixin_document.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ func (d DocumentMixin) Hooks() []ent.Hook {
6262
ent.OpCreate|ent.OpUpdateOne|ent.OpUpdateOne,
6363
),
6464
hooks.HookImportDocument(),
65-
hooks.HookParseAssociations(),
65+
hooks.HookDetailsVersion(),
6666
hooks.HookSummarizeDetails(),
6767
hook.On(
6868
hooks.HookStatusApproval(),
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9107d46ce0a234bc6a827510c2e4ceeb7d45e80db73d6ea3839161a6785c927a
1+
3f04e7b3b3e5d92c7741accff77e4daefc04d658611b2518517cde2641931b7c

internal/httpserve/serveropts/gala.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func ConfigureGala(ctx context.Context, galaApp, notificationGala *gala.Gala, db
9191
{galaApp, hooks.RegisterGalaSlackListeners},
9292
{galaApp, hooks.RegisterGalaVendorScoringListeners},
9393
{galaApp, hooks.RegisterGalaIdentityResolutionListeners},
94+
{galaApp, hooks.RegisterGalaDocumentAssociationListeners},
9495
{notificationGala, hooks.RegisterGalaNotificationListeners},
9596
}
9697

0 commit comments

Comments
 (0)