Skip to content

Commit dfa551a

Browse files
committed
feat: integrate indexer-service types for NATS messaging
- Use indexerTypes.IndexerMessageEnvelope from indexer-service instead of local types - Add IndexingConfig() methods to ProjectBase and ProjectSettings domain models - ProjectBase uses "viewer" for access check, "writer" for history - ProjectSettings uses "auditor" for access check with project UID - Remove duplicate IndexerMessage and SettingsIndexerMessage local types - Update SendIndexerMessage to accept IndexerMessageEnvelope with IndexingConfig - Refactor message building to eliminate duplicate IndexingConfig initialization - Update all tests to use indexer-service types - Update root-project-setup script to use new message format This change establishes the framework for passing indexing configuration to the indexer service via NATS messages for proper access control and search functionality. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Signed-off-by: Andres Tobon <andrest2455@gmail.com>
1 parent d830254 commit dfa551a

File tree

11 files changed

+126
-251
lines changed

11 files changed

+126
-251
lines changed

cmd/project-api/service_endpoint_project_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,9 +153,8 @@ func TestCreateProject(t *testing.T) {
153153
// Mock successful project creation
154154
mockRepo.On("CreateProject", mock.Anything, mock.AnythingOfType("*models.ProjectBase"), mock.AnythingOfType("*models.ProjectSettings")).Return(nil)
155155
// Mock message sending
156-
mockMsg.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectIndexerMessage"), mock.AnythingOfType("bool")).Return(nil)
156+
mockMsg.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("types.IndexerMessageEnvelope"), mock.AnythingOfType("bool")).Return(nil).Times(2)
157157
mockMsg.On("SendAccessMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectAccessMessage"), mock.AnythingOfType("bool")).Return(nil)
158-
mockMsg.On("SendIndexerMessage", mock.Anything, mock.AnythingOfType("string"), mock.AnythingOfType("models.ProjectSettingsIndexerMessage"), mock.AnythingOfType("bool")).Return(nil)
159158
},
160159
expectedError: false,
161160
},

go.mod

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ module github.com/linuxfoundation/lfx-v2-project-service
55
go 1.24.0
66

77
require (
8+
github.com/linuxfoundation/lfx-v2-indexer-service v0.4.13
89
github.com/auth0/go-jwt-middleware/v2 v2.3.0
910
github.com/aws/aws-sdk-go-v2/config v1.32.0
1011
github.com/aws/aws-sdk-go-v2/feature/s3/manager v1.20.10
@@ -42,6 +43,7 @@ require (
4243
github.com/gohugoio/hashstructure v0.6.0 // indirect
4344
github.com/gorilla/websocket v1.5.3 // indirect
4445
github.com/klauspost/compress v1.18.1 // indirect
46+
github.com/linuxfoundation/lfx-v2-indexer-service v0.4.13 // indirect
4547
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
4648
github.com/nats-io/nkeys v0.4.11 // indirect
4749
github.com/nats-io/nuid v1.0.1 // indirect

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aN
6262
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
6363
github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co=
6464
github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0=
65+
github.com/linuxfoundation/lfx-v2-indexer-service v0.4.13 h1:SANx5u+qJ+1DkMadpSDhPYJgPgnHDBPxw9Am6LC5pjk=
66+
github.com/linuxfoundation/lfx-v2-indexer-service v0.4.13/go.mod h1:j013GdKST/hMWFhciRuzJd0sy764sNtlmO3gqmsnaCA=
6567
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d h1:Zj+PHjnhRYWBK6RqCDBcAhLXoi3TzC27Zad/Vn+gnVQ=
6668
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d/go.mod h1:WZy8Q5coAB1zhY9AOBJP0O6J4BuDfbupUDavKY+I3+s=
6769
github.com/manveru/gobdd v0.0.0-20131210092515-f1a17fdd710b h1:3E44bLeN8uKYdfQqVQycPnaVviZdBLbizFhU49mtbe4=

internal/domain/models/message.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,41 +3,6 @@
33

44
package models
55

6-
// MessageAction is a type for the action of a project message.
7-
type MessageAction string
8-
9-
// MessageAction constants for the action of a project message.
10-
const (
11-
// ActionCreated is the action for a resource creation message.
12-
ActionCreated MessageAction = "created"
13-
// ActionUpdated is the action for a resource update message.
14-
ActionUpdated MessageAction = "updated"
15-
// ActionDeleted is the action for a resource deletion message.
16-
ActionDeleted MessageAction = "deleted"
17-
)
18-
19-
// ProjectIndexerMessage is a type-safe NATS message for project indexing operations.
20-
type ProjectIndexerMessage struct {
21-
Action MessageAction `json:"action"`
22-
Data ProjectBase `json:"data"`
23-
Tags []string `json:"tags"`
24-
}
25-
26-
// ProjectSettingsIndexerMessage is a type-safe NATS message for project settings indexing operations.
27-
type ProjectSettingsIndexerMessage struct {
28-
Action MessageAction `json:"action"`
29-
Data ProjectSettings `json:"data"`
30-
Tags []string `json:"tags"`
31-
}
32-
33-
// IndexerMessageEnvelope is the actual message format sent to NATS for indexing operations.
34-
type IndexerMessageEnvelope struct {
35-
Action MessageAction `json:"action"`
36-
Headers map[string]string `json:"headers"`
37-
Data any `json:"data"`
38-
Tags []string `json:"tags"`
39-
}
40-
416
// ProjectAccessData is the schema for the data in the message sent to the fga-sync service.
427
// These are the fields that the fga-sync service needs in order to update the OpenFGA permissions.
438
type ProjectAccessData struct {

internal/domain/models/message_test.go

Lines changed: 0 additions & 155 deletions
Original file line numberDiff line numberDiff line change
@@ -9,116 +9,6 @@ import (
99
"github.com/stretchr/testify/assert"
1010
)
1111

12-
func TestMessageActionConstants(t *testing.T) {
13-
tests := []struct {
14-
name string
15-
action MessageAction
16-
expected string
17-
}{
18-
{
19-
name: "action created",
20-
action: ActionCreated,
21-
expected: "created",
22-
},
23-
{
24-
name: "action updated",
25-
action: ActionUpdated,
26-
expected: "updated",
27-
},
28-
{
29-
name: "action deleted",
30-
action: ActionDeleted,
31-
expected: "deleted",
32-
},
33-
}
34-
35-
for _, tt := range tests {
36-
t.Run(tt.name, func(t *testing.T) {
37-
assert.Equal(t, tt.expected, string(tt.action))
38-
})
39-
}
40-
}
41-
42-
func TestProjectIndexerMessage(t *testing.T) {
43-
tests := []struct {
44-
name string
45-
message ProjectIndexerMessage
46-
verify func(t *testing.T, msg ProjectIndexerMessage)
47-
}{
48-
{
49-
name: "project indexer message with all fields",
50-
message: ProjectIndexerMessage{
51-
Action: ActionCreated,
52-
Data: ProjectBase{
53-
UID: "project-123",
54-
Slug: "test-project",
55-
Name: "Test Project",
56-
},
57-
Tags: []string{"project-123", "test-project", "Test Project"},
58-
},
59-
verify: func(t *testing.T, msg ProjectIndexerMessage) {
60-
assert.Equal(t, ActionCreated, msg.Action)
61-
assert.Equal(t, "project-123", msg.Data.UID)
62-
assert.Equal(t, "test-project", msg.Data.Slug)
63-
assert.Equal(t, "Test Project", msg.Data.Name)
64-
assert.Len(t, msg.Tags, 3)
65-
},
66-
},
67-
{
68-
name: "project indexer message with minimal fields",
69-
message: ProjectIndexerMessage{
70-
Action: ActionDeleted,
71-
Data: ProjectBase{
72-
UID: "project-456",
73-
},
74-
},
75-
verify: func(t *testing.T, msg ProjectIndexerMessage) {
76-
assert.Equal(t, ActionDeleted, msg.Action)
77-
assert.Equal(t, "project-456", msg.Data.UID)
78-
assert.Empty(t, msg.Tags)
79-
},
80-
},
81-
}
82-
83-
for _, tt := range tests {
84-
t.Run(tt.name, func(t *testing.T) {
85-
tt.verify(t, tt.message)
86-
})
87-
}
88-
}
89-
90-
func TestProjectSettingsIndexerMessage(t *testing.T) {
91-
tests := []struct {
92-
name string
93-
message ProjectSettingsIndexerMessage
94-
verify func(t *testing.T, msg ProjectSettingsIndexerMessage)
95-
}{
96-
{
97-
name: "project settings indexer message with all fields",
98-
message: ProjectSettingsIndexerMessage{
99-
Action: ActionUpdated,
100-
Data: ProjectSettings{
101-
UID: "settings-123",
102-
MissionStatement: "Our mission",
103-
},
104-
Tags: []string{"settings-123", "Our mission"},
105-
},
106-
verify: func(t *testing.T, msg ProjectSettingsIndexerMessage) {
107-
assert.Equal(t, ActionUpdated, msg.Action)
108-
assert.Equal(t, "settings-123", msg.Data.UID)
109-
assert.Equal(t, "Our mission", msg.Data.MissionStatement)
110-
assert.Len(t, msg.Tags, 2)
111-
},
112-
},
113-
}
114-
115-
for _, tt := range tests {
116-
t.Run(tt.name, func(t *testing.T) {
117-
tt.verify(t, tt.message)
118-
})
119-
}
120-
}
121-
12212
func TestProjectAccessMessage(t *testing.T) {
12313
tests := []struct {
12414
name string
@@ -154,48 +44,3 @@ func TestProjectAccessMessage(t *testing.T) {
15444
})
15545
}
15646
}
157-
158-
func TestIndexerMessageEnvelope(t *testing.T) {
159-
tests := []struct {
160-
name string
161-
message IndexerMessageEnvelope
162-
verify func(t *testing.T, msg IndexerMessageEnvelope)
163-
}{
164-
{
165-
name: "indexer message envelope with all fields",
166-
message: IndexerMessageEnvelope{
167-
Action: ActionCreated,
168-
Headers: map[string]string{
169-
"request-id": "test-request-123",
170-
"user-id": "user-456",
171-
},
172-
Data: map[string]interface{}{
173-
"uid": "project-123",
174-
"slug": "test-project",
175-
"name": "Test Project",
176-
},
177-
Tags: []string{"project-123", "test-project"},
178-
},
179-
verify: func(t *testing.T, msg IndexerMessageEnvelope) {
180-
assert.Equal(t, ActionCreated, msg.Action)
181-
assert.Equal(t, "test-request-123", msg.Headers["request-id"])
182-
assert.Equal(t, "user-456", msg.Headers["user-id"])
183-
assert.NotNil(t, msg.Data)
184-
assert.Len(t, msg.Tags, 2)
185-
186-
// Verify data can be type asserted
187-
data, ok := msg.Data.(map[string]interface{})
188-
assert.True(t, ok)
189-
assert.Equal(t, "project-123", data["uid"])
190-
assert.Equal(t, "test-project", data["slug"])
191-
assert.Equal(t, "Test Project", data["name"])
192-
},
193-
},
194-
}
195-
196-
for _, tt := range tests {
197-
t.Run(tt.name, func(t *testing.T) {
198-
tt.verify(t, tt.message)
199-
})
200-
}
201-
}

internal/domain/models/project.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ package models
55

66
import (
77
"fmt"
8+
"strings"
89
"time"
10+
11+
indexerTypes "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
912
)
1013

1114
// UserInfo represents user information including profile details.
@@ -101,6 +104,27 @@ func (p *ProjectBase) Tags() []string {
101104
return tags
102105
}
103106

107+
// IndexingConfig generates an IndexingConfig for indexing this project.
108+
func (p *ProjectBase) IndexingConfig() *indexerTypes.IndexingConfig {
109+
if p == nil {
110+
return nil
111+
}
112+
113+
return &indexerTypes.IndexingConfig{
114+
ObjectID: p.UID,
115+
Public: &p.Public,
116+
AccessCheckObject: fmt.Sprintf("project:%s", p.UID),
117+
AccessCheckRelation: "viewer",
118+
HistoryCheckObject: fmt.Sprintf("project:%s", p.UID),
119+
HistoryCheckRelation: "writer",
120+
SortName: p.Name,
121+
NameAndAliases: []string{p.Name, p.Slug},
122+
ParentRefs: []string{fmt.Sprintf("project:%s", p.ParentUID)},
123+
Fulltext: strings.Join([]string{p.Name, p.Slug, p.Description}, " "),
124+
Tags: p.Tags(),
125+
}
126+
}
127+
104128
// Tags generates a consistent set of tags for the project settings.
105129
// IMPORTANT: If you modify this method, please update the Project Tags documentation in the README.md
106130
// to ensure consumers understand how to use these tags for searching.
@@ -127,3 +151,20 @@ func (p *ProjectSettings) Tags() []string {
127151

128152
return tags
129153
}
154+
155+
// IndexingConfig generates an IndexingConfig for indexing this project settings.
156+
// Note: Project settings use the project UID for access checks, not the settings UID.
157+
func (p *ProjectSettings) IndexingConfig(projectUID string) *indexerTypes.IndexingConfig {
158+
if p == nil {
159+
return nil
160+
}
161+
162+
return &indexerTypes.IndexingConfig{
163+
ObjectID: p.UID,
164+
AccessCheckObject: fmt.Sprintf("project:%s", projectUID),
165+
AccessCheckRelation: "auditor",
166+
HistoryCheckObject: fmt.Sprintf("project:%s", projectUID),
167+
HistoryCheckRelation: "writer",
168+
Tags: p.Tags(),
169+
}
170+
}

internal/infrastructure/nats/message.go

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import (
1111
"time"
1212

1313
"github.com/go-viper/mapstructure/v2"
14+
indexerConstants "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/constants"
15+
indexerTypes "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
1416
"github.com/linuxfoundation/lfx-v2-project-service/internal/domain/models"
1517
"github.com/linuxfoundation/lfx-v2-project-service/pkg/constants"
1618
"github.com/nats-io/nats.go"
@@ -59,9 +61,10 @@ func (m *MessageBuilder) requestMessage(subject string, data []byte, timeout tim
5961
func (m *MessageBuilder) sendIndexerMessage(
6062
ctx context.Context,
6163
subject string,
62-
action models.MessageAction,
64+
action indexerConstants.MessageAction,
6365
data []byte,
6466
tags []string,
67+
indexingConfig *indexerTypes.IndexingConfig,
6568
sync bool,
6669
) error {
6770
headers := make(map[string]string)
@@ -74,7 +77,7 @@ func (m *MessageBuilder) sendIndexerMessage(
7477

7578
var payload any
7679
switch action {
77-
case models.ActionCreated, models.ActionUpdated:
80+
case indexerConstants.ActionCreated, indexerConstants.ActionUpdated:
7881
// The data should be a JSON object.
7982
var jsonData any
8083
if err := json.Unmarshal(data, &jsonData); err != nil {
@@ -97,18 +100,17 @@ func (m *MessageBuilder) sendIndexerMessage(
97100
slog.ErrorContext(ctx, "error decoding data", constants.ErrKey, err, "subject", subject)
98101
return err
99102
}
100-
case models.ActionDeleted:
103+
case indexerConstants.ActionDeleted:
101104
// The data should just be a string of the UID being deleted.
102105
payload = data
103106
}
104107

105-
// TODO: use the model from the indexer service to keep the message body consistent.
106-
// Ticket https://linuxfoundation.atlassian.net/browse/LFXV2-147
107-
message := models.IndexerMessageEnvelope{
108-
Action: action,
109-
Headers: headers,
110-
Data: payload,
111-
Tags: tags,
108+
message := indexerTypes.IndexerMessageEnvelope{
109+
Action: action,
110+
Headers: headers,
111+
Data: payload,
112+
Tags: tags,
113+
IndexingConfig: indexingConfig,
112114
}
113115

114116
messageBytes, err := json.Marshal(message)
@@ -125,25 +127,17 @@ func (m *MessageBuilder) sendIndexerMessage(
125127
// SendIndexerMessage sends indexer messages to NATS for search indexing.
126128
func (m *MessageBuilder) SendIndexerMessage(ctx context.Context, subject string, message interface{}, sync bool) error {
127129
switch msg := message.(type) {
128-
case models.ProjectIndexerMessage:
130+
case indexerTypes.IndexerMessageEnvelope:
129131
dataBytes, err := json.Marshal(msg.Data)
130132
if err != nil {
131-
slog.ErrorContext(ctx, "error marshalling project data into JSON", constants.ErrKey, err)
133+
slog.ErrorContext(ctx, "error marshalling message data into JSON", constants.ErrKey, err)
132134
return err
133135
}
134-
return m.sendIndexerMessage(ctx, subject, msg.Action, dataBytes, msg.Tags, sync)
135-
136-
case models.ProjectSettingsIndexerMessage:
137-
dataBytes, err := json.Marshal(msg.Data)
138-
if err != nil {
139-
slog.ErrorContext(ctx, "error marshalling project settings data into JSON", constants.ErrKey, err)
140-
return err
141-
}
142-
return m.sendIndexerMessage(ctx, subject, msg.Action, dataBytes, msg.Tags, sync)
136+
return m.sendIndexerMessage(ctx, subject, msg.Action, dataBytes, msg.Tags, msg.IndexingConfig, sync)
143137

144138
case string:
145139
// For delete operations, the message is just the UID string
146-
return m.sendIndexerMessage(ctx, subject, models.ActionDeleted, []byte(msg), nil, sync)
140+
return m.sendIndexerMessage(ctx, subject, indexerConstants.ActionDeleted, []byte(msg), nil, nil, sync)
147141

148142
default:
149143
slog.ErrorContext(ctx, "unsupported indexer message type", "type", fmt.Sprintf("%T", message))

0 commit comments

Comments
 (0)