Skip to content
Open
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -406,15 +406,25 @@ Every data modification publishes NATS messages:
- Index messages for search service
- Access control updates for authorization service

### 3. Request Context
### 3. NATS Event Wire Types (`pkg/events/`)

NATS message payload types that other services consume belong in `pkg/events/`, **not** `internal/`. This lets downstream services (e.g., `lfx-v2-invite-service`) import the canonical struct definitions directly.

- Domain types in `internal/domain/models/` may differ from wire types and can evolve independently.
- Explicit converter functions in `internal/service/converters.go` map from domain → event type before publishing.
- Example: `DomainSettingsToEvent(*models.ProjectSettings) events.ProjectSettings`

**Rule:** if a struct appears in a NATS message payload, it belongs in `pkg/events/`, not `internal/`.

### 4. Request Context

Important context values:

- `request-id`: Unique request identifier
- `authorization`: JWT token from header
- `etag`: ETag value for optimistic concurrency (sent as If-Match header in requests)

### 4. Error Handling
### 5. Error Handling

Domain errors are mapped to HTTP status codes:

Expand Down
49 changes: 42 additions & 7 deletions cmd/project-api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import (

genhttp "github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/gen/http/project_service/server"
genquerysvc "github.com/linuxfoundation/lfx-v2-project-service/api/project/v1/gen/project_service"
"github.com/linuxfoundation/lfx-v2-project-service/internal/domain"
"github.com/linuxfoundation/lfx-v2-project-service/internal/domain/models"
"github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/auth"
internalnats "github.com/linuxfoundation/lfx-v2-project-service/internal/infrastructure/nats"
Expand Down Expand Up @@ -94,7 +95,8 @@ func main() {

// Generated service initialization.
service := service.NewProjectsService(jwtAuth, service.ServiceConfig{
SkipEtagValidation: env.SkipEtagValidation,
SkipEtagValidation: env.SkipEtagValidation,
LFXSelfServeBaseURL: env.LFXSelfServeBaseURL,
})
svc := NewProjectsAPI(service)

Expand Down Expand Up @@ -156,9 +158,10 @@ func parseFlags(defaultPort string) flags {

// environment are the environment variables for the project service.
type environment struct {
NatsURL string
Port string
SkipEtagValidation bool
NatsURL string
Port string
SkipEtagValidation bool
LFXSelfServeBaseURL string
}

func parseEnv() environment {
Expand All @@ -175,10 +178,22 @@ func parseEnv() environment {
if skipEtagValidationStr == "true" {
skipEtagValidation = true
}
lfxSelfServeBaseURL := os.Getenv("LFX_SELF_SERVE_BASE_URL")
if lfxSelfServeBaseURL == "" {
switch os.Getenv("LFX_ENVIRONMENT") {
case "prod":
lfxSelfServeBaseURL = "https://app.lfx.dev"
case "staging":
lfxSelfServeBaseURL = "https://staging.app.lfx.dev"
default:
lfxSelfServeBaseURL = "https://dev.app.lfx.dev"
}
Comment thread
andrest50 marked this conversation as resolved.
}
return environment{
NatsURL: natsURL,
Port: port,
SkipEtagValidation: skipEtagValidation,
NatsURL: natsURL,
Port: port,
SkipEtagValidation: skipEtagValidation,
LFXSelfServeBaseURL: lfxSelfServeBaseURL,
}
}

Expand Down Expand Up @@ -422,6 +437,26 @@ func createNatsSubcriptions(ctx context.Context, svc *ProjectsAPI, natsConn *nat
}
}

type eventHandler struct {
subject string
handle func(ctx context.Context, msg domain.Message) error
}
for _, eh := range []eventHandler{
{constants.ProjectSettingsUpdatedSubject, svc.service.HandleProjectSettingsUpdated},
} {
slog.With("subject", eh.subject, "queue", queueName).Debug("subscribing to NATS subject")
_, err := natsConn.QueueSubscribe(eh.subject, queueName, func(msg *nats.Msg) {
natsMsg := &internalnats.NatsMsg{Msg: msg}
if handlerErr := eh.handle(ctx, natsMsg); handlerErr != nil {
slog.WarnContext(ctx, "event handler failed", errKey, handlerErr, "subject", eh.subject)
}
})
if err != nil {
slog.ErrorContext(ctx, "error creating NATS queue subscription", errKey, err)
return err
}
}

return nil
}

Expand Down
9 changes: 3 additions & 6 deletions go.mod
Comment thread
github-license-compliance[bot] marked this conversation as resolved.
Fixed
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
// SPDX-License-Identifier: MIT
module github.com/linuxfoundation/lfx-v2-project-service

go 1.24.0
go 1.25

require (
github.com/auth0/go-jwt-middleware/v2 v2.3.0
Expand All @@ -11,6 +11,8 @@ require (
github.com/aws/aws-sdk-go-v2/service/s3 v1.92.0
github.com/go-viper/mapstructure/v2 v2.4.0
github.com/google/uuid v1.6.0
github.com/linuxfoundation/lfx-v2-email-service v0.0.0-20260512204955-693efd22ee37
github.com/linuxfoundation/lfx-v2-fga-sync v0.2.17
github.com/linuxfoundation/lfx-v2-indexer-service v0.4.14-0.20260109191409-7371e293d8b5
Comment thread
andrest50 marked this conversation as resolved.
Comment thread
andrest50 marked this conversation as resolved.
github.com/nats-io/nats.go v1.47.0
github.com/remychantenay/slog-otel v1.3.4
Expand Down Expand Up @@ -63,23 +65,18 @@ require (
github.com/gorilla/websocket v1.5.3 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 // indirect
github.com/klauspost/compress v1.18.1 // indirect
github.com/linuxfoundation/lfx-v2-fga-sync v0.2.17 // indirect
github.com/manveru/faker v0.0.0-20171103152722-9fbc68a78c4d // indirect
github.com/nats-io/nkeys v0.4.11 // indirect
github.com/nats-io/nuid v1.0.1 // indirect
github.com/openfga/go-sdk v0.7.1 // indirect
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
github.com/rustyoz/Mtransform v0.0.0-20250628105438-00796a985d0a // indirect
github.com/rustyoz/genericlexer v0.0.0-20250522144106-d3cfee480384 // indirect
github.com/sourcegraph/conc v0.3.0 // indirect
github.com/stretchr/objx v0.5.3 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect
go.opentelemetry.io/proto/otlp v1.9.0 // indirect
go.uber.org/atomic v1.7.0 // indirect
go.uber.org/multierr v1.9.0 // indirect
golang.org/x/crypto v0.47.0 // indirect
golang.org/x/mod v0.31.0 // indirect
golang.org/x/net v0.49.0 // indirect
Expand Down
15 changes: 2 additions & 13 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -46,8 +46,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927 h1:SKI1/fuSdodxmNNyVBR8d7X/HuLnRpvvFO0AgyQk764=
github.com/cheekybits/is v0.0.0-20150225183255-68e9c0620927/go.mod h1:h/aW8ynjgkuj+NQRlZcDbAbM1ORAbXjXX77sX7T289U=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dimfeld/httppath v0.0.0-20170720192232-ee938bf73598 h1:MGKhKyiYrvMDZsmLR/+RGffQSXwEkXgfLSA08qDn9AI=
Expand Down Expand Up @@ -81,6 +79,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/linuxfoundation/lfx-v2-email-service v0.0.0-20260512204955-693efd22ee37 h1:UllgXFzxz2m49uLC/C1TMkzzPRUzQ6G3iKCDCJNnQ5g=
github.com/linuxfoundation/lfx-v2-email-service v0.0.0-20260512204955-693efd22ee37/go.mod h1:23Jgcg5/7pT/EWWeUoKwGO8pR1BEfaC3iudwlj9s2B0=
github.com/linuxfoundation/lfx-v2-fga-sync v0.2.17 h1:ZW2PyrEPB6SmT14qa3qlrcU4rB/eKRumPUOwaoS5or4=
github.com/linuxfoundation/lfx-v2-fga-sync v0.2.17/go.mod h1:075J7/39UbsuLsJCXgVkbpKK1VIuPa7gbyhLsoTBAXo=
github.com/linuxfoundation/lfx-v2-indexer-service v0.4.14-0.20260109191409-7371e293d8b5 h1:Ic06r/3OO4wP2Y2eL9InXEKRSVq2cqbNGBTsdr493YA=
Expand All @@ -95,9 +95,6 @@ github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0=
github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/openfga/go-sdk v0.7.1 h1:ZFFDRoSWAHcbOzPFUWPLUpoIOJZRoQ6KgJp2vyfB82g=
github.com/openfga/go-sdk v0.7.1/go.mod h1:Fu00XYLWkfgmo3PV45EwSOhpaBNcuVMBOdklpKoaazw=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/remychantenay/slog-otel v1.3.4 h1:xoM41ayLff2U8zlK5PH31XwD7Lk3W9wKfl4+RcmKom4=
Expand All @@ -110,12 +107,8 @@ github.com/rustyoz/genericlexer v0.0.0-20250522144106-d3cfee480384 h1:jrCaAewj72
github.com/rustyoz/genericlexer v0.0.0-20250522144106-d3cfee480384/go.mod h1:m65JtsVg785EjQvQylesseVucezoQZqJozlPAfjXmbE=
github.com/rustyoz/svg v0.0.0-20250705135709-8b1786137cb3 h1:dFappt+gj/o9cCFfMmXV8Jq+hShQmFlM6Uh2Vd0YlzE=
github.com/rustyoz/svg v0.0.0-20250705135709-8b1786137cb3/go.mod h1:33v4CGNONT4+QIwIt3o1GVdBqTfLCeptHNc0HpZ1N14=
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.5.3 h1:jmXUvGomnU1o3W/V5h2VEradbpJDwGrzugQQvL0POH4=
github.com/stretchr/objx v0.5.3/go.mod h1:rDQraq+vQZU7Fde9LOZLr8Tax6zZvy4kuNKF+QYS+U0=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down Expand Up @@ -156,12 +149,8 @@ go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZY
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.9.0 h1:7fIwc/ZtS0q++VgcfqFDxSBZVv/Xo49/SYnDFupUwlI=
go.uber.org/multierr v1.9.0/go.mod h1:X2jQV1h+kxSjClGpnseKVIxpmcjrj7MNnI0bnlfKTVQ=
goa.design/goa/v3 v3.22.6 h1:D2qDkAvdpf6ePr2iXKT+Ple5WDrjyes3iOfYD2yCpw0=
goa.design/goa/v3 v3.22.6/go.mod h1:rhssEXxox3+sKnYp18hPNFCz65I4hLWHEtJKewoNJWk=
golang.org/x/crypto v0.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8=
Expand Down
3 changes: 3 additions & 0 deletions internal/domain/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ package domain

import (
"context"

emailapi "github.com/linuxfoundation/lfx-v2-email-service/pkg/api"
)

// Message represents a domain message interface
Expand All @@ -24,4 +26,5 @@ type MessageBuilder interface {
SendIndexerMessage(ctx context.Context, subject string, message any, sync bool) error
SendAccessMessage(ctx context.Context, subject string, message any, sync bool) error
SendProjectEventMessage(ctx context.Context, subject string, message any) error
SendEmailRequest(ctx context.Context, req emailapi.SendEmailRequest) error
}
6 changes: 6 additions & 0 deletions internal/domain/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

"github.com/stretchr/testify/mock"

emailapi "github.com/linuxfoundation/lfx-v2-email-service/pkg/api"
"github.com/linuxfoundation/lfx-v2-project-service/internal/domain/models"
)

Expand Down Expand Up @@ -250,6 +251,11 @@ func (m *MockMessageBuilder) SendProjectEventMessage(ctx context.Context, subjec
return args.Error(0)
}

func (m *MockMessageBuilder) SendEmailRequest(ctx context.Context, req emailapi.SendEmailRequest) error {
args := m.Called(ctx, req)
return args.Error(0)
}

// MockMessage implements Message for testing
type MockMessage struct {
mock.Mock
Expand Down
12 changes: 0 additions & 12 deletions internal/domain/models/message.go

This file was deleted.

28 changes: 28 additions & 0 deletions internal/infrastructure/nats/message.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import (
"time"

"github.com/go-viper/mapstructure/v2"
emailapi "github.com/linuxfoundation/lfx-v2-email-service/pkg/api"
fgatypes "github.com/linuxfoundation/lfx-v2-fga-sync/pkg/types"
indexerConstants "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/constants"
indexerTypes "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
Expand Down Expand Up @@ -197,3 +198,30 @@ func (m *MessageBuilder) SendProjectEventMessage(ctx context.Context, subject st
slog.DebugContext(ctx, "published project event message to NATS", "subject", subject)
return nil
}

// SendEmailRequest sends a request to the email service and waits for a reply.
func (m *MessageBuilder) SendEmailRequest(ctx context.Context, req emailapi.SendEmailRequest) error {
data, err := json.Marshal(req)
if err != nil {
slog.ErrorContext(ctx, "error marshalling email request into JSON", constants.ErrKey, err)
return err
}

reply, err := m.NatsConn.RequestMsgWithContext(ctx, &nats.Msg{
Subject: emailapi.SendEmailSubject,
Data: data,
})
if err != nil {
slog.ErrorContext(ctx, "email service request failed", constants.ErrKey, err)
return fmt.Errorf("email service request: %w", err)
}
Comment thread
andrest50 marked this conversation as resolved.
Comment thread
andrest50 marked this conversation as resolved.

if len(reply.Data) > 0 {
var errResp emailapi.SendEmailErrorResponse
if jsonErr := json.Unmarshal(reply.Data, &errResp); jsonErr == nil && errResp.Error != "" {
return fmt.Errorf("email service error: %s", errResp.Error)
}
}

return nil
}
15 changes: 8 additions & 7 deletions internal/infrastructure/nats/message_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import (
indexerTypes "github.com/linuxfoundation/lfx-v2-indexer-service/pkg/types"
"github.com/linuxfoundation/lfx-v2-project-service/internal/domain/models"
"github.com/linuxfoundation/lfx-v2-project-service/pkg/constants"
"github.com/linuxfoundation/lfx-v2-project-service/pkg/events"
"github.com/nats-io/nats.go"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
Expand Down Expand Up @@ -429,20 +430,20 @@ func TestMessageBuilder_SendProjectEventMessage(t *testing.T) {
{
name: "successful send project settings updated message",
subject: constants.ProjectSettingsUpdatedSubject,
message: models.ProjectSettingsUpdatedMessage{
message: events.ProjectSettingsUpdatedMessage{
ProjectUID: "test-project-uid",
OldSettings: models.ProjectSettings{
OldSettings: events.ProjectSettings{
UID: "test-project-uid",
MissionStatement: "old mission",
},
NewSettings: models.ProjectSettings{
NewSettings: events.ProjectSettings{
UID: "test-project-uid",
MissionStatement: "new mission",
},
},
setupMocks: func(mockConn *MockNATSConn) {
mockConn.On("Publish", constants.ProjectSettingsUpdatedSubject, mock.MatchedBy(func(data []byte) bool {
var msg models.ProjectSettingsUpdatedMessage
var msg events.ProjectSettingsUpdatedMessage
err := json.Unmarshal(data, &msg)
if err != nil {
return false
Expand All @@ -457,10 +458,10 @@ func TestMessageBuilder_SendProjectEventMessage(t *testing.T) {
{
name: "nats publish error",
subject: constants.ProjectSettingsUpdatedSubject,
message: models.ProjectSettingsUpdatedMessage{
message: events.ProjectSettingsUpdatedMessage{
ProjectUID: "test-project-uid",
OldSettings: models.ProjectSettings{UID: "test"},
NewSettings: models.ProjectSettings{UID: "test"},
OldSettings: events.ProjectSettings{UID: "test"},
NewSettings: events.ProjectSettings{UID: "test"},
},
setupMocks: func(mockConn *MockNATSConn) {
mockConn.On("Publish", constants.ProjectSettingsUpdatedSubject, mock.AnythingOfType("[]uint8")).Return(errors.New("nats error"))
Expand Down
10 changes: 10 additions & 0 deletions internal/infrastructure/nats/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type INatsConn interface {
IsConnected() bool
Publish(subj string, data []byte) error
Request(subj string, data []byte, timeout time.Duration) (*nats.Msg, error)
RequestMsgWithContext(ctx context.Context, msg *nats.Msg) (*nats.Msg, error)
}

// MockNATSConn is a mock implementation of the [INatsConn] interface.
Expand Down Expand Up @@ -48,6 +49,15 @@ func (m *MockNATSConn) Request(subj string, data []byte, timeout time.Duration)
return args.Get(0).(*nats.Msg), args.Error(1)
}

// RequestMsgWithContext is a mock method for the [INatsConn] interface.
func (m *MockNATSConn) RequestMsgWithContext(ctx context.Context, msg *nats.Msg) (*nats.Msg, error) {
args := m.Called(ctx, msg)
if args.Get(0) == nil {
return nil, args.Error(1)
}
return args.Get(0).(*nats.Msg), args.Error(1)
}

// INatsMsg is an interface for [nats.Msg] that allows for mocking.
type INatsMsg interface {
Respond(data []byte) error
Expand Down
Loading
Loading