feat: wire Watermill CQRS — EventBus in usecases, event handlers, router lifecycle
- Add EventPublisher interface in biz layer for domain event publishing - Wire EventBusPublisher (Watermill EventBus adapter) into FileUsecase, FolderUsecase, ShareUsecase - Publish events after UploadFile, DeleteFile, CreateFolder, DeleteFolder, CreateShare - Implement CQRSHandler with logging event handlers for all 6 event types - Register event handlers via CQRSBus.RegisterHandlers using Watermill EventProcessor - Store subscriber and wmLogger in CQRSBus for EventProcessor wiring - Expose SqlDB() on Data struct for Watermill SQL pub/sub - Start Watermill router in goroutine alongside Kratos app with graceful close - Use appContext wrapper struct to pass CQRSBus through Wire DI graph
This commit is contained in:
parent
3eb1a1839d
commit
11315fd00b
@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"flag"
|
"flag"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
@ -42,13 +43,23 @@ func main() {
|
|||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
app, cleanup, err := initApp(&bc, logger)
|
ctx, cleanup, err := initApp(&bc, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
if err := app.Run(); err != nil {
|
// Start Watermill router alongside the Kratos app.
|
||||||
|
if ctx.CQRSBus != nil {
|
||||||
|
go func() {
|
||||||
|
if err := ctx.CQRSBus.Router.Run(context.Background()); err != nil {
|
||||||
|
logger.Log(log.LevelError, "msg", "watermill router error", "error", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer ctx.CQRSBus.Router.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := ctx.App.Run(); err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -3,11 +3,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
|
|
||||||
"rag/file-system/internal/biz"
|
"rag/file-system/internal/biz"
|
||||||
"rag/file-system/internal/conf"
|
"rag/file-system/internal/conf"
|
||||||
"rag/file-system/internal/data"
|
"rag/file-system/internal/data"
|
||||||
"rag/file-system/internal/server"
|
"rag/file-system/internal/server"
|
||||||
"rag/file-system/internal/service"
|
"rag/file-system/internal/service"
|
||||||
|
"rag/file-system/internal/watermark"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v2"
|
"github.com/go-kratos/kratos/v2"
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
@ -16,6 +19,12 @@ import (
|
|||||||
"github.com/google/wire"
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// appContext holds the Kratos app and CQRS bus together for Wire.
|
||||||
|
type appContext struct {
|
||||||
|
App *kratos.App
|
||||||
|
CQRSBus *watermark.CQRSBus
|
||||||
|
}
|
||||||
|
|
||||||
// newApp creates a new Kratos application with HTTP and gRPC servers.
|
// newApp creates a new Kratos application with HTTP and gRPC servers.
|
||||||
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
||||||
return kratos.New(
|
return kratos.New(
|
||||||
@ -25,6 +34,11 @@ func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newAppContext wraps the app and CQRSBus into an appContext.
|
||||||
|
func newAppContext(app *kratos.App, bus *watermark.CQRSBus) *appContext {
|
||||||
|
return &appContext{App: app, CQRSBus: bus}
|
||||||
|
}
|
||||||
|
|
||||||
// newConfServer extracts the Server config from Bootstrap.
|
// newConfServer extracts the Server config from Bootstrap.
|
||||||
func newConfServer(bc *conf.Bootstrap) *conf.Server {
|
func newConfServer(bc *conf.Bootstrap) *conf.Server {
|
||||||
return bc.GetServer()
|
return bc.GetServer()
|
||||||
@ -40,8 +54,13 @@ func newConfAuth(bc *conf.Bootstrap) *conf.Auth {
|
|||||||
return bc.GetAuth()
|
return bc.GetAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newSQLDB extracts the underlying *sql.DB from Data for Watermill.
|
||||||
|
func newSQLDB(d *data.Data) (*sql.DB, error) {
|
||||||
|
return d.SqlDB()
|
||||||
|
}
|
||||||
|
|
||||||
// initApp wires up the entire dependency graph.
|
// initApp wires up the entire dependency graph.
|
||||||
func initApp(*conf.Bootstrap, log.Logger) (*kratos.App, func(), error) {
|
func initApp(*conf.Bootstrap, log.Logger) (*appContext, func(), error) {
|
||||||
panic(wire.Build(
|
panic(wire.Build(
|
||||||
// Config extraction
|
// Config extraction
|
||||||
newConfServer,
|
newConfServer,
|
||||||
@ -53,15 +72,23 @@ func initApp(*conf.Bootstrap, log.Logger) (*kratos.App, func(), error) {
|
|||||||
biz.ProviderSet,
|
biz.ProviderSet,
|
||||||
service.ProviderSet,
|
service.ProviderSet,
|
||||||
server.ProviderSet,
|
server.ProviderSet,
|
||||||
|
watermark.ProviderSet,
|
||||||
|
|
||||||
// Interface bindings: biz interfaces → data implementations
|
// Extract *sql.DB from Data for Watermill
|
||||||
|
newSQLDB,
|
||||||
|
|
||||||
|
// Interface bindings: biz interfaces -> data implementations
|
||||||
wire.Bind(new(biz.FileRepo), new(*data.FileRepo)),
|
wire.Bind(new(biz.FileRepo), new(*data.FileRepo)),
|
||||||
wire.Bind(new(biz.FolderRepo), new(*data.FolderRepo)),
|
wire.Bind(new(biz.FolderRepo), new(*data.FolderRepo)),
|
||||||
wire.Bind(new(biz.FileMetaRepo), new(*data.FileMetaRepo)),
|
wire.Bind(new(biz.FileMetaRepo), new(*data.FileMetaRepo)),
|
||||||
wire.Bind(new(biz.FileMetaRepoForShare), new(*data.FileMetaRepo)),
|
wire.Bind(new(biz.FileMetaRepoForShare), new(*data.FileMetaRepo)),
|
||||||
wire.Bind(new(biz.ShareRepo), new(*data.ShareRepo)),
|
wire.Bind(new(biz.ShareRepo), new(*data.ShareRepo)),
|
||||||
|
|
||||||
// App constructor
|
// Interface binding: biz EventPublisher -> watermark EventBusPublisher
|
||||||
|
wire.Bind(new(biz.EventPublisher), new(*watermark.EventBusPublisher)),
|
||||||
|
|
||||||
|
// Wire up app + CQRSBus into appContext
|
||||||
|
newAppContext,
|
||||||
newApp,
|
newApp,
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,6 +7,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"database/sql"
|
||||||
"github.com/go-kratos/kratos/v2"
|
"github.com/go-kratos/kratos/v2"
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
"github.com/go-kratos/kratos/v2/transport/grpc"
|
"github.com/go-kratos/kratos/v2/transport/grpc"
|
||||||
@ -16,43 +17,68 @@ import (
|
|||||||
"rag/file-system/internal/data"
|
"rag/file-system/internal/data"
|
||||||
"rag/file-system/internal/server"
|
"rag/file-system/internal/server"
|
||||||
"rag/file-system/internal/service"
|
"rag/file-system/internal/service"
|
||||||
|
"rag/file-system/internal/watermark"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Injectors from wire.go:
|
// Injectors from wire.go:
|
||||||
|
|
||||||
// initApp wires up the entire dependency graph.
|
// initApp wires up the entire dependency graph.
|
||||||
func initApp(bootstrap *conf.Bootstrap, logger log.Logger) (*kratos.App, func(), error) {
|
func initApp(bootstrap *conf.Bootstrap, logger log.Logger) (*appContext, func(), error) {
|
||||||
confServer := newConfServer(bootstrap)
|
confServer := newConfServer(bootstrap)
|
||||||
auth := newConfAuth(bootstrap)
|
auth := newConfAuth(bootstrap)
|
||||||
confData := newConfData(bootstrap)
|
confData := newConfData(bootstrap)
|
||||||
fileRepo := data.NewFileRepo(confData)
|
fileRepo := data.NewFileRepo(confData)
|
||||||
fileUsecase := biz.NewFileUsecase(fileRepo, logger)
|
|
||||||
bucketUsecase := biz.NewBucketUsecase(fileRepo, logger)
|
|
||||||
dataData, cleanup, err := data.NewData(confData, logger)
|
dataData, cleanup, err := data.NewData(confData, logger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
db, err := newSQLDB(dataData)
|
||||||
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
cqrsHandler := watermark.NewCQRSHandler(logger)
|
||||||
|
cqrsBus, err := watermark.NewCQRSBusWithHandlers(db, cqrsHandler, logger)
|
||||||
|
if err != nil {
|
||||||
|
cleanup()
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
eventBusPublisher := watermark.NewEventBusPublisher(cqrsBus)
|
||||||
|
fileUsecase := biz.NewFileUsecase(fileRepo, eventBusPublisher, logger)
|
||||||
|
bucketUsecase := biz.NewBucketUsecase(fileRepo, logger)
|
||||||
folderRepo := data.NewFolderRepo(dataData, logger)
|
folderRepo := data.NewFolderRepo(dataData, logger)
|
||||||
fileMetaRepo := data.NewFileMetaRepo(dataData, logger)
|
fileMetaRepo := data.NewFileMetaRepo(dataData, logger)
|
||||||
folderUsecase := biz.NewFolderUsecase(folderRepo, fileMetaRepo, fileRepo, logger)
|
folderUsecase := biz.NewFolderUsecase(folderRepo, fileMetaRepo, fileRepo, eventBusPublisher, logger)
|
||||||
shareRepo := data.NewShareRepo(dataData, logger)
|
shareRepo := data.NewShareRepo(dataData, logger)
|
||||||
shareUsecase := biz.NewShareUsecase(shareRepo, fileMetaRepo, fileRepo, logger)
|
shareUsecase := biz.NewShareUsecase(shareRepo, fileMetaRepo, fileRepo, eventBusPublisher, logger)
|
||||||
fileService := service.NewFileService(fileUsecase, bucketUsecase, folderUsecase, shareUsecase, logger)
|
fileService := service.NewFileService(fileUsecase, bucketUsecase, folderUsecase, shareUsecase, logger)
|
||||||
httpServer := server.NewHTTPServer(confServer, auth, fileService, logger)
|
httpServer := server.NewHTTPServer(confServer, auth, fileService, logger)
|
||||||
grpcServer := server.NewGRPCServer(confServer, auth, fileService, logger)
|
grpcServer := server.NewGRPCServer(confServer, auth, fileService, logger)
|
||||||
app := newApp(logger, httpServer, grpcServer)
|
app := newApp(logger, httpServer, grpcServer)
|
||||||
return app, func() {
|
mainAppContext := newAppContext(app, cqrsBus)
|
||||||
|
return mainAppContext, func() {
|
||||||
cleanup()
|
cleanup()
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// wire.go:
|
// wire.go:
|
||||||
|
|
||||||
|
// appContext holds the Kratos app and CQRS bus together for Wire.
|
||||||
|
type appContext struct {
|
||||||
|
App *kratos.App
|
||||||
|
CQRSBus *watermark.CQRSBus
|
||||||
|
}
|
||||||
|
|
||||||
// newApp creates a new Kratos application with HTTP and gRPC servers.
|
// newApp creates a new Kratos application with HTTP and gRPC servers.
|
||||||
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
func newApp(logger log.Logger, hs *http.Server, gs *grpc.Server) *kratos.App {
|
||||||
return kratos.New(kratos.Name("file-system"), kratos.Logger(logger), kratos.Server(hs, gs))
|
return kratos.New(kratos.Name("file-system"), kratos.Logger(logger), kratos.Server(hs, gs))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newAppContext wraps the app and CQRSBus into an appContext.
|
||||||
|
func newAppContext(app *kratos.App, bus *watermark.CQRSBus) *appContext {
|
||||||
|
return &appContext{App: app, CQRSBus: bus}
|
||||||
|
}
|
||||||
|
|
||||||
// newConfServer extracts the Server config from Bootstrap.
|
// newConfServer extracts the Server config from Bootstrap.
|
||||||
func newConfServer(bc *conf.Bootstrap) *conf.Server {
|
func newConfServer(bc *conf.Bootstrap) *conf.Server {
|
||||||
return bc.GetServer()
|
return bc.GetServer()
|
||||||
@ -67,3 +93,8 @@ func newConfData(bc *conf.Bootstrap) *conf.Data {
|
|||||||
func newConfAuth(bc *conf.Bootstrap) *conf.Auth {
|
func newConfAuth(bc *conf.Bootstrap) *conf.Auth {
|
||||||
return bc.GetAuth()
|
return bc.GetAuth()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// newSQLDB extracts the underlying *sql.DB from Data for Watermill.
|
||||||
|
func newSQLDB(d *data.Data) (*sql.DB, error) {
|
||||||
|
return d.SqlDB()
|
||||||
|
}
|
||||||
|
|||||||
8
internal/biz/event.go
Normal file
8
internal/biz/event.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package biz
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
// EventPublisher defines the interface for publishing domain events.
|
||||||
|
type EventPublisher interface {
|
||||||
|
Publish(ctx context.Context, event interface{}) error
|
||||||
|
}
|
||||||
@ -6,6 +6,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"rag/file-system/internal/data"
|
"rag/file-system/internal/data"
|
||||||
|
"rag/file-system/internal/watermark"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
)
|
)
|
||||||
@ -30,20 +31,32 @@ type FileRepo interface {
|
|||||||
// FileUsecase wraps FileRepo and provides file-level business operations.
|
// FileUsecase wraps FileRepo and provides file-level business operations.
|
||||||
type FileUsecase struct {
|
type FileUsecase struct {
|
||||||
repo FileRepo
|
repo FileRepo
|
||||||
|
eventPub EventPublisher
|
||||||
log *log.Helper
|
log *log.Helper
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFileUsecase creates a new FileUsecase.
|
// NewFileUsecase creates a new FileUsecase.
|
||||||
func NewFileUsecase(repo FileRepo, logger log.Logger) *FileUsecase {
|
func NewFileUsecase(repo FileRepo, eventPub EventPublisher, logger log.Logger) *FileUsecase {
|
||||||
return &FileUsecase{
|
return &FileUsecase{
|
||||||
repo: repo,
|
repo: repo,
|
||||||
|
eventPub: eventPub,
|
||||||
log: log.NewHelper(logger),
|
log: log.NewHelper(logger),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// UploadFile uploads data to the specified bucket and key.
|
// UploadFile uploads data to the specified bucket and key.
|
||||||
func (uc *FileUsecase) UploadFile(ctx context.Context, bucket, key string, fileData io.Reader) error {
|
func (uc *FileUsecase) UploadFile(ctx context.Context, bucket, key string, fileData io.Reader) error {
|
||||||
return uc.repo.UploadFile(ctx, bucket, key, fileData)
|
if err := uc.repo.UploadFile(ctx, bucket, key, fileData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Publish domain event on success.
|
||||||
|
if err := uc.eventPub.Publish(ctx, &watermark.FileUploadedEvent{
|
||||||
|
BucketName: bucket,
|
||||||
|
ObjectKey: key,
|
||||||
|
}); err != nil {
|
||||||
|
uc.log.Errorf("failed to publish FileUploadedEvent: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadFile downloads an object from S3.
|
// DownloadFile downloads an object from S3.
|
||||||
@ -58,7 +71,17 @@ func (uc *FileUsecase) GetFileContent(ctx context.Context, bucket, key string) (
|
|||||||
|
|
||||||
// DeleteFile removes a file from S3.
|
// DeleteFile removes a file from S3.
|
||||||
func (uc *FileUsecase) DeleteFile(ctx context.Context, bucket, key string) error {
|
func (uc *FileUsecase) DeleteFile(ctx context.Context, bucket, key string) error {
|
||||||
return uc.repo.DeleteFile(ctx, bucket, key)
|
if err := uc.repo.DeleteFile(ctx, bucket, key); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Publish domain event on success.
|
||||||
|
if err := uc.eventPub.Publish(ctx, &watermark.FileDeletedEvent{
|
||||||
|
BucketName: bucket,
|
||||||
|
ObjectKey: key,
|
||||||
|
}); err != nil {
|
||||||
|
uc.log.Errorf("failed to publish FileDeletedEvent: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ListObjectsV2 lists files with pagination support.
|
// ListObjectsV2 lists files with pagination support.
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"rag/file-system/internal/data"
|
"rag/file-system/internal/data"
|
||||||
"rag/file-system/internal/pkg/sanitize"
|
"rag/file-system/internal/pkg/sanitize"
|
||||||
|
"rag/file-system/internal/watermark"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@ -38,15 +39,17 @@ type FolderUsecase struct {
|
|||||||
folderRepo FolderRepo
|
folderRepo FolderRepo
|
||||||
fileRepo FileMetaRepo
|
fileRepo FileMetaRepo
|
||||||
s3Repo FileRepo
|
s3Repo FileRepo
|
||||||
|
eventPub EventPublisher
|
||||||
log *log.Helper
|
log *log.Helper
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewFolderUsecase creates a new FolderUsecase.
|
// NewFolderUsecase creates a new FolderUsecase.
|
||||||
func NewFolderUsecase(folderRepo FolderRepo, fileRepo FileMetaRepo, s3Repo FileRepo, logger log.Logger) *FolderUsecase {
|
func NewFolderUsecase(folderRepo FolderRepo, fileRepo FileMetaRepo, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *FolderUsecase {
|
||||||
return &FolderUsecase{
|
return &FolderUsecase{
|
||||||
folderRepo: folderRepo,
|
folderRepo: folderRepo,
|
||||||
fileRepo: fileRepo,
|
fileRepo: fileRepo,
|
||||||
s3Repo: s3Repo,
|
s3Repo: s3Repo,
|
||||||
|
eventPub: eventPub,
|
||||||
log: log.NewHelper(logger),
|
log: log.NewHelper(logger),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -61,6 +64,14 @@ func (uc *FolderUsecase) CreateFolder(ctx context.Context, parentID *string, nam
|
|||||||
if err := uc.folderRepo.Create(ctx, folder); err != nil {
|
if err := uc.folderRepo.Create(ctx, folder); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create folder: %w", err)
|
return nil, fmt.Errorf("failed to create folder: %w", err)
|
||||||
}
|
}
|
||||||
|
// Publish domain event on success.
|
||||||
|
if err := uc.eventPub.Publish(ctx, &watermark.FolderCreatedEvent{
|
||||||
|
FolderID: folder.ID,
|
||||||
|
Name: folder.Name,
|
||||||
|
OwnerID: folder.OwnerID,
|
||||||
|
}); err != nil {
|
||||||
|
uc.log.Errorf("failed to publish FolderCreatedEvent: %v", err)
|
||||||
|
}
|
||||||
return folder, nil
|
return folder, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -118,6 +129,15 @@ func (uc *FolderUsecase) DeleteFolder(ctx context.Context, id, ownerID string) e
|
|||||||
if err := uc.folderRepo.Delete(ctx, id, ownerID); err != nil {
|
if err := uc.folderRepo.Delete(ctx, id, ownerID); err != nil {
|
||||||
return fmt.Errorf("failed to delete folder: %w", err)
|
return fmt.Errorf("failed to delete folder: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish domain event on success.
|
||||||
|
if err := uc.eventPub.Publish(ctx, &watermark.FolderDeletedEvent{
|
||||||
|
FolderID: id,
|
||||||
|
OwnerID: ownerID,
|
||||||
|
}); err != nil {
|
||||||
|
uc.log.Errorf("failed to publish FolderDeletedEvent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"rag/file-system/internal/data"
|
"rag/file-system/internal/data"
|
||||||
|
"rag/file-system/internal/watermark"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
)
|
)
|
||||||
@ -32,15 +33,17 @@ type ShareUsecase struct {
|
|||||||
shareRepo ShareRepo
|
shareRepo ShareRepo
|
||||||
fileRepo FileMetaRepoForShare
|
fileRepo FileMetaRepoForShare
|
||||||
s3Repo FileRepo
|
s3Repo FileRepo
|
||||||
|
eventPub EventPublisher
|
||||||
log *log.Helper
|
log *log.Helper
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewShareUsecase creates a new ShareUsecase.
|
// NewShareUsecase creates a new ShareUsecase.
|
||||||
func NewShareUsecase(shareRepo ShareRepo, fileRepo FileMetaRepoForShare, s3Repo FileRepo, logger log.Logger) *ShareUsecase {
|
func NewShareUsecase(shareRepo ShareRepo, fileRepo FileMetaRepoForShare, s3Repo FileRepo, eventPub EventPublisher, logger log.Logger) *ShareUsecase {
|
||||||
return &ShareUsecase{
|
return &ShareUsecase{
|
||||||
shareRepo: shareRepo,
|
shareRepo: shareRepo,
|
||||||
fileRepo: fileRepo,
|
fileRepo: fileRepo,
|
||||||
s3Repo: s3Repo,
|
s3Repo: s3Repo,
|
||||||
|
eventPub: eventPub,
|
||||||
log: log.NewHelper(logger),
|
log: log.NewHelper(logger),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -68,6 +71,18 @@ func (uc *ShareUsecase) CreateShare(ctx context.Context, resourceType, resourceI
|
|||||||
if err := uc.shareRepo.Create(ctx, share); err != nil {
|
if err := uc.shareRepo.Create(ctx, share); err != nil {
|
||||||
return nil, fmt.Errorf("failed to create share link: %w", err)
|
return nil, fmt.Errorf("failed to create share link: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Publish domain event on success.
|
||||||
|
if err := uc.eventPub.Publish(ctx, &watermark.ShareCreatedEvent{
|
||||||
|
ShareID: share.ID,
|
||||||
|
ResourceType: share.ResourceType,
|
||||||
|
ResourceID: share.ResourceID,
|
||||||
|
Token: share.Token,
|
||||||
|
CreatedBy: share.CreatedBy,
|
||||||
|
}); err != nil {
|
||||||
|
uc.log.Errorf("failed to publish ShareCreatedEvent: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return share, nil
|
return share, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package data
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"database/sql"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"rag/file-system/internal/conf"
|
"rag/file-system/internal/conf"
|
||||||
@ -47,6 +48,11 @@ func NewData(c *conf.Data, logger log.Logger) (*Data, func(), error) {
|
|||||||
return &Data{db: db, log: helper}, cleanup, nil
|
return &Data{db: db, log: helper}, cleanup, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SqlDB returns the underlying *sql.DB for use by Watermill.
|
||||||
|
func (d *Data) SqlDB() (*sql.DB, error) {
|
||||||
|
return d.db.DB()
|
||||||
|
}
|
||||||
|
|
||||||
// DB returns the *gorm.DB for the given context. If a transaction is active
|
// DB returns the *gorm.DB for the given context. If a transaction is active
|
||||||
// in the context, it returns the transaction DB; otherwise the global DB.
|
// in the context, it returns the transaction DB; otherwise the global DB.
|
||||||
func (d *Data) DB(ctx context.Context) *gorm.DB {
|
func (d *Data) DB(ctx context.Context) *gorm.DB {
|
||||||
|
|||||||
@ -8,14 +8,19 @@ import (
|
|||||||
"github.com/ThreeDotsLabs/watermill/components/cqrs"
|
"github.com/ThreeDotsLabs/watermill/components/cqrs"
|
||||||
"github.com/ThreeDotsLabs/watermill/message"
|
"github.com/ThreeDotsLabs/watermill/message"
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
|
"github.com/google/wire"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CQRSBus holds the Watermill CQRS buses, router, and internal references.
|
||||||
type CQRSBus struct {
|
type CQRSBus struct {
|
||||||
CommandBus *cqrs.CommandBus
|
CommandBus *cqrs.CommandBus
|
||||||
EventBus *cqrs.EventBus
|
EventBus *cqrs.EventBus
|
||||||
Router *message.Router
|
Router *message.Router
|
||||||
|
subscriber message.Subscriber
|
||||||
|
wmLogger watermill.LoggerAdapter
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCQRSBus creates a new CQRSBus with PostgreSQL-backed pub/sub.
|
||||||
func NewCQRSBus(db *sql.DB, logger log.Logger) (*CQRSBus, error) {
|
func NewCQRSBus(db *sql.DB, logger log.Logger) (*CQRSBus, error) {
|
||||||
helper := log.NewHelper(logger)
|
helper := log.NewHelper(logger)
|
||||||
wmLogger := watermill.NewStdLogger(false, false)
|
wmLogger := watermill.NewStdLogger(false, false)
|
||||||
@ -37,9 +42,6 @@ func NewCQRSBus(db *sql.DB, logger log.Logger) (*CQRSBus, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// subscriber will be used later to wire event handlers on the router
|
|
||||||
_ = subscriber
|
|
||||||
|
|
||||||
router, err := message.NewRouter(message.RouterConfig{}, wmLogger)
|
router, err := message.NewRouter(message.RouterConfig{}, wmLogger)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -74,5 +76,56 @@ func NewCQRSBus(db *sql.DB, logger log.Logger) (*CQRSBus, error) {
|
|||||||
CommandBus: commandBus,
|
CommandBus: commandBus,
|
||||||
EventBus: eventBus,
|
EventBus: eventBus,
|
||||||
Router: router,
|
Router: router,
|
||||||
|
subscriber: subscriber,
|
||||||
|
wmLogger: wmLogger,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterHandlers sets up all event handlers on the router.
|
||||||
|
func (b *CQRSBus) RegisterHandlers(handler *CQRSHandler) error {
|
||||||
|
eventProcessor, err := cqrs.NewEventProcessorWithConfig(
|
||||||
|
b.Router,
|
||||||
|
cqrs.EventProcessorConfig{
|
||||||
|
GenerateSubscribeTopic: func(params cqrs.EventProcessorGenerateSubscribeTopicParams) (string, error) {
|
||||||
|
return "events." + params.EventName, nil
|
||||||
|
},
|
||||||
|
SubscriberConstructor: func(params cqrs.EventProcessorSubscriberConstructorParams) (message.Subscriber, error) {
|
||||||
|
return b.subscriber, nil
|
||||||
|
},
|
||||||
|
Marshaler: cqrs.JSONMarshaler{},
|
||||||
|
Logger: b.wmLogger,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = eventProcessor.AddHandlers(
|
||||||
|
cqrs.NewEventHandler("OnFileUploaded", handler.OnFileUploaded),
|
||||||
|
cqrs.NewEventHandler("OnFileDeleted", handler.OnFileDeleted),
|
||||||
|
cqrs.NewEventHandler("OnFolderCreated", handler.OnFolderCreated),
|
||||||
|
cqrs.NewEventHandler("OnFolderDeleted", handler.OnFolderDeleted),
|
||||||
|
cqrs.NewEventHandler("OnShareCreated", handler.OnShareCreated),
|
||||||
|
cqrs.NewEventHandler("OnShareDownloaded", handler.OnShareDownloaded),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ProviderSet is the Wire provider set for the watermark layer.
|
||||||
|
var ProviderSet = wire.NewSet(NewCQRSBusWithHandlers, NewCQRSHandler, NewEventBusPublisher)
|
||||||
|
|
||||||
|
// NewCQRSBusWithHandlers creates the CQRSBus and registers all event handlers.
|
||||||
|
func NewCQRSBusWithHandlers(db *sql.DB, handler *CQRSHandler, logger log.Logger) (*CQRSBus, error) {
|
||||||
|
bus, err := NewCQRSBus(db, logger)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := bus.RegisterHandlers(handler); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return bus, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -1,13 +1,54 @@
|
|||||||
package watermark
|
package watermark
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
"github.com/go-kratos/kratos/v2/log"
|
"github.com/go-kratos/kratos/v2/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// CQRSHandler holds all Watermill event handlers.
|
||||||
type CQRSHandler struct {
|
type CQRSHandler struct {
|
||||||
log *log.Helper
|
log *log.Helper
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewCQRSHandler creates a new CQRSHandler.
|
||||||
func NewCQRSHandler(logger log.Logger) *CQRSHandler {
|
func NewCQRSHandler(logger log.Logger) *CQRSHandler {
|
||||||
return &CQRSHandler{log: log.NewHelper(logger)}
|
return &CQRSHandler{log: log.NewHelper(logger)}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// OnFileUploaded handles the FileUploadedEvent.
|
||||||
|
func (h *CQRSHandler) OnFileUploaded(ctx context.Context, event *FileUploadedEvent) error {
|
||||||
|
h.log.Infof("event: file uploaded, bucket=%s key=%s size=%d", event.BucketName, event.ObjectKey, event.Size)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFileDeleted handles the FileDeletedEvent.
|
||||||
|
func (h *CQRSHandler) OnFileDeleted(ctx context.Context, event *FileDeletedEvent) error {
|
||||||
|
h.log.Infof("event: file deleted, bucket=%s key=%s", event.BucketName, event.ObjectKey)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFolderCreated handles the FolderCreatedEvent.
|
||||||
|
func (h *CQRSHandler) OnFolderCreated(ctx context.Context, event *FolderCreatedEvent) error {
|
||||||
|
h.log.Infof("event: folder created, id=%s name=%s owner=%s", event.FolderID, event.Name, event.OwnerID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnFolderDeleted handles the FolderDeletedEvent.
|
||||||
|
func (h *CQRSHandler) OnFolderDeleted(ctx context.Context, event *FolderDeletedEvent) error {
|
||||||
|
h.log.Infof("event: folder deleted, id=%s owner=%s", event.FolderID, event.OwnerID)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnShareCreated handles the ShareCreatedEvent.
|
||||||
|
func (h *CQRSHandler) OnShareCreated(ctx context.Context, event *ShareCreatedEvent) error {
|
||||||
|
h.log.Infof("event: share created, id=%s token=%s resource=%s/%s by=%s",
|
||||||
|
event.ShareID, event.Token, event.ResourceType, event.ResourceID, event.CreatedBy)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// OnShareDownloaded handles the ShareDownloadedEvent.
|
||||||
|
func (h *CQRSHandler) OnShareDownloaded(ctx context.Context, event *ShareDownloadedEvent) error {
|
||||||
|
h.log.Infof("event: share downloaded, token=%s", event.Token)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|||||||
20
internal/watermark/publisher.go
Normal file
20
internal/watermark/publisher.go
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
package watermark
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
// EventBusPublisher implements biz.EventPublisher using Watermill EventBus.
|
||||||
|
type EventBusPublisher struct {
|
||||||
|
bus *CQRSBus
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewEventBusPublisher creates a new EventBusPublisher from a CQRSBus.
|
||||||
|
func NewEventBusPublisher(bus *CQRSBus) *EventBusPublisher {
|
||||||
|
return &EventBusPublisher{bus: bus}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Publish publishes a domain event via the Watermill EventBus.
|
||||||
|
func (p *EventBusPublisher) Publish(ctx context.Context, event interface{}) error {
|
||||||
|
return p.bus.EventBus.Publish(ctx, event)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user