diff --git a/docs/conventions.md b/docs/conventions.md new file mode 100644 index 0000000..040350d --- /dev/null +++ b/docs/conventions.md @@ -0,0 +1,46 @@ +# Conventions + +## Resolver + +Maps a GraphQL query to a service method. Validates the input values and types, +but no business logic. + +## Command and query + +The only place in the application that may call the repository. + +## Naming conventions + +These names are supposed to help people understand the codebase by recognizing +names from other projects, but are not necessary to follow. + +### Commands + +Action + Subject +: e.g. `SendStudentGrades` + +### Queries + +Action + Quantity + Subject + Filter (optional) +: e.g. `FindOneStudentById`, `FindManyStudents` + +### Events + +TODO + +### Entities + +TODO + +### GraphQL queries and mutations + +Who + Action + Subject + `s` if plural +: e.g. `PrincipalListStudentsGrades` + +### Controllers + +TODO + +- Input: +Action + Subject + Input +: e.g. `ListStudentsGradesInput` diff --git a/docs/cookbook.md b/docs/cookbook.md new file mode 100644 index 0000000..36599f2 --- /dev/null +++ b/docs/cookbook.md @@ -0,0 +1,331 @@ +# Cookbook + +## GraphQL + +### Resolvers + +- Add schema to `/internal/transport-inbound/graphql/.graphql`. Note + that the name of the file is arbitrary and impacts the name of the resolver + file. +- Run `make generate` to generate the resolver file in + `/internal/transport-inbound/graphql/resolvers/.resolvers.go`. +- Write the code for the resolver in the generated file. Usually, this uses + `commands` and `queries` modules from `/internal/features`. + +### Loaders + +```go +// package/internal/features/loaders/module.go + +package loaders + +import "go.uber.org/fx" + +var Module = fx.Provide( + newLoader, +) +``` + +```go +// package/internal/features/loaders/loaders.go + +package loaders + +import ( + "github.com/example/package/internal/generated/ent" + "github.com/graph-gophers/dataloader" +) + +type Loader struct { + ... + FindEntityByIDLoader *dataloader.Loader + ... +} + +func newLoader(repo *ent.Client) *Loader { + FindEntityByIDLoader := dataloader.NewBatchedLoader( + findEntityByIDBatch(repo), + dataloader.WithClearCacheOnBatch(), + ) + + return &Loader{ + ... + FindEntityByIDLoader: FindEntityByIDLoader, + ... + } +} +``` + +```go +// package/internal/features/loaders/findEntityByID.go + +package loaders + +import ( + "context" + + "github.com/TheRafaBonin/roxy" + + "github.com/example/package/internal/generated/ent" + "github.com/example/package/internal/generated/ent/entity" + + "github.com/google/uuid" + + "github.com/graph-gophers/dataloader" +) + +func findEntityByIDBatch(repo *ent.Client) dataloader.BatchFunc { + batchFn := func(ctx context.Context, keys dataloader.Keys) []*dataloader.Result { + // Declares some variables + var entityMap = make(map[string]*ent.Entity) + var errorsMap = make(map[string]error) + + var results []*dataloader.Result + var entityIDs []uuid.UUID + + // Convert keys + for _, key := range keys.Keys() { + uid, err := uuid.Parse(key) + if err != nil { + errorsMap[key] = roxy.Wrap(err, "parsing uuids") + continue + } + + entityIDs = append(entityIDs, uid) + } + + // Finds entities and maps + entities, err := repo.Entity.Query(). + Where(entity.IDIn(entityIDs...)). + All(ctx) + if err != nil { + return findEntityByIDBatchErrorResults(keys, roxy.Wrap(err, "finding entities")) + } + for _, entity := range entities { + entityMap[entity.ID.String()] = entity + } + + // Map the results + for _, key := range keys.Keys() { + err = errorsMap[key] + if err != nil { + results = append(results, &dataloader.Result{ + Data: nil, + Error: err, + }) + continue + } + + p := entityMap[key] + results = append(results, &dataloader.Result{ + Data: p, + }) + } + return results + } + return batchFn +} + +func findEntityByIDBatchErrorResults(keys dataloader.Keys, err error) []*dataloader.Result { + var results []*dataloader.Result + for range keys.Keys() { + results = append(results, &dataloader.Result{ + Data: nil, + Error: err, + }) + } + + return results +} +``` + +```go +// package/internal/features/graphql/resolvers/module.go +package resolvers + +import ( + "github.com/99designs/gqlgen/graphql" + "github.com/example/package/internal/features/loaders" + "go.uber.org/fx" + + generatedGraphql "github.com/example/package/internal/generated/graphql" +) + +func newSchema(..., loaders *loaders.Loader, ...) graphql.ExecutableSchema { + resolver := &Resolver{ + ... + loaders: loaders, + ... + } + + graphqlConfig := generatedGraphql.Config{Resolvers: resolver} + return generatedGraphql.NewExecutableSchema(graphqlConfig) +} + +var Module = fx.Options( + fx.Provide( + newSchema, + ), +) +``` + +```go +// package/internal/features/graphql/resolvers/resolvers.go +package resolvers + +import ( + "github.com/example/package/internal/features/loaders" + + generatedGraphql "github.com/example/package/internal/generated/graphql" +) + +... + +type Resolver struct { + ... + loaders *loaders.Loader + ... +} + +... +``` + +```go +// package/internal/features/graphql/resolvers/entity.resolvers.go + +package resolvers + +import ( + "context" + + "github.com/example/package/internal/generated/ent" + "github.com/example/package/internal/generated/graphql" + "github.com/example/package/internal/transport-inbound/graphql/resolvers/formatters" + "github.com/graph-gophers/dataloader" + "github.com/rotisserie/eris" +) + +func (r *entityResolver) FindEntityByID(ctx context.Context, id string) (*graphql.Entity, error) { + thunk := r.loaders.FindEntityByIDLoader.Load(ctx, dataloader.StringKey(id)) + loadedEntity, err := thunk() + err = eris.Wrap(err, "loaders.FindEntityByIDLoader") + + if err != nil { + return nil, err + } + + entity := loadedEntity.(*ent.Entity) + if entity == nil { + return nil, err + } + + return formatters.FormatEntity(entity), nil +} + +func (r *Resolver) Entity() graphql.EntityResolver { return &entityResolver{r} } + +type entityResolver struct{ *Resolver } +``` + +## Repository Pattern + +### ent + +```go +// package/cmd/entc.go + +//go:build exclude + +package main + +import ( + "log" + + "entgo.io/contrib/entgql" + "entgo.io/ent/entc" + "entgo.io/ent/entc/gen" +) + +func main() { + ex, err := entgql.NewExtension() + if err != nil { + log.Fatalf("creating entgql extension: %v", err) + } + + opts := []entc.Option{ + entc.Extensions(ex), + } + + err = entc.Generate("../internal/entities", &gen.Config{ + Target: "../internal/generated/ent", + Schema: "github.com/example/package/internal/entities", + Package: "github.com/example/package/internal/generated/ent", + Features: []gen.Feature{ + gen.FeatureVersionedMigration, + gen.FeatureUpsert, + gen.FeatureLock, + }, + }, opts...) + if err != nil { + log.Fatalf("running ent codegen: %v", err) + } +} +``` + +## Tests + +### Mock subscribers + +```go +import ( + ... + "github.com/gothunder/thunder/pkg/events/mocks" + ... +) + +... + +var handler *mocks.Handler + +... + +// inside test case + ... + + subscriberCalled := make(chan interface{}) + topic := events.SubscriberTopic + var subscriberReturn events.SubscriberMessage + handler.Mock.On("Handle", + mock.Anything, + topic, + mock.Anything, + ).Once().Return(thunderEvents.Success).Run(func(args mock.Arguments) { + defer GinkgoRecover() + + decoder := args.Get(2).(thunderEvents.EventDecoder) + err := decoder.Decode(&subscriberReturn) + Expect(err).ToNot(HaveOccurred()) + + close(subscriberCalled) + }) + + resp := someCommand() // this publishes an event + + // Assert + Eventually(subscriberCalled, "3s").Should(BeClosed()) + Expect(resp).To(Equal(thunderEvents.Success)) + + ... +``` + +## Generate command + +```go +// package/cmd/generate.go + +package generate + +//go:generate go run entc.go +//go:generate go run github.com/99designs/gqlgen +//go:generate sh -c "protoc --experimental_allow_proto3_optional --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --proto_path=../transport-inbound/grpc/proto --go_out=../../pkg/grpc/ --go-grpc_out=../../pkg/grpc/ ../transport-inbound/grpc/proto/*.proto" +``` diff --git a/docs/getting-started.md b/docs/getting-started.md new file mode 100644 index 0000000..6dfe62e --- /dev/null +++ b/docs/getting-started.md @@ -0,0 +1,49 @@ +# Getting started + +The modules are imported to the root of your app, you can then provide them to +the rest of your project, below we're using [Uber's +fx](https://uber-go.github.io/fx/) to perform the dependency injection + +Install the library: + +```bash +go get github.com/gothunder/thunder +``` + +Then, create a `main.go` file like this: + +```go +// main.go +package main + +import ( + transportinbound "github.com/gothunder/thunder/example/internal/transport-inbound" + transportoutbound "github.com/gothunder/thunder/example/internal/transport-outbound" + thunderEventRabbitmq "github.com/gothunder/thunder/pkg/events/rabbitmq" + thunderLogs "github.com/gothunder/thunder/pkg/log" + + "github.com/rs/zerolog/diode" + "go.uber.org/fx" +) + +func main() { + var w diode.Writer + + app := fx.New( + // The order of these options isn't important. + thunderLogs.Module, + fx.Populate(&w), + + transportinbound.Module, + transportoutbound.Module, + + thunderEventRabbitmq.PublisherModule, + thunderEventRabbitmq.InvokeConsumer, + ) + app.Run() + + // This is required to flush the logs to stdout. + // We only want to do this after the app has exited. + thunderLogs.DiodeShutdown(w) +} +``` diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..6dd1575 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,32 @@ +# Introduction + +Thunder is a collection of libraries and opinionated patterns to build +cloud-native services. The project provides different modules, which can be +used individually and replaced at any time. + +Most of the modules use consolidated projects under the hood, the idea of this +project is to provide wrappers and connect all of these pieces seamlessly. + +## How it works + +This project provides modules and constructors for all of its components, so +one may use a dependency injection framework such as [Uber's +fx](https://uber-go.github.io/fx/), which is used in the docs, or manually +instantiate the components. + +Take a look at the [modules page](./modules/index.md) to see the list of +available modules and the ones planned for the future. + +## Why? + +This project was created to solve some of the problems we faced when building +services at [Alternative Payments](https://www.alternativepayments.io/). + +## Next + +- [Getting started](./getting-started.md): how to use thunder right away. +- [Modules](./modules/index.md): list of available modules. +- [Cookbook](./cookbook.md): common use cases. +- [Conventions](./conventions.md): how to use thunder in your project. +- [Project structure](./project-structure.md): how to structure a thunder + project. diff --git a/docs/modules/chi.md b/docs/modules/chi.md new file mode 100644 index 0000000..fab27cf --- /dev/null +++ b/docs/modules/chi.md @@ -0,0 +1,111 @@ +# Chi + +This module uses [chi](https://go-chi.io/#/) as the HTTP multiplexer, providing +a easy way to add or remove routes. + +## Starting the server + +To start the server, you need to invoke the `thunderChi.StartServer` function. +This is ideally done in the `main` package using [Uber's +fx](https://uber-go.github.io/fx/). The snippet below shows how to do this +using the `fx.Invoke` method. + +```go +// main.go +package main + +import ( + thunderLogs "github.com/gothunder/thunder/pkg/log" + thunderChi "github.com/gothunder/thunder/pkg/router/chi" + transportInbound "github.com/gothunder/thunder/example/email/internal/transport-inbound" + + "github.com/rs/zerolog/diode" + "go.uber.org/fx" +) + +func main() { + var w diode.Writer + + app := fx.New( + // The order of these options isn't important. + thunderLogs.Module, + fx.Populate(&w), + + thunderChi.Module, // This is the module we're interested in. + fx.Invoke(thunderChi.StartServer), // This starts the server. + transportInbound.Module, // This is the module where the routes are defined. + ) + app.Run() + + // This is required to flush the logs to stdout. + // We only want to do this after the app has exited. + thunderLogs.DiodeShutdown(w) +} +``` + +## Defining routes + +To define routes, you need to create a module that implements the +`HTTPHandler` interface, that is: + +- `Method() string` - The HTTP method of the route, e.g. `GET`, `POST`, etc. +- `Pattern() string` - The pattern of the route, e.g. `/users`. +- `ServeHTTP(w http.ResponseWriter, r *http.Request)` - The handler function + that will be called when the route is hit. + +The snippet below shows how to define a route that returns a list of users. + +```go +// transport-inbound/router/handlers.go +package router + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gothunder/thunder/pkg/router" +) + +func NewUsersHandler() router.HandlerOutput { + return router.HandlerOutput{ + Handler: UsersHandler{}, + } +} + +type UsersHandler struct {} + +func (h UsersHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + users := []string{"user1", "user2", "user3"} + + w.Header().Set("Content-Type", "application/json") + w.Write([]byte(fmt.Sprintf("%s", users))) +} + +func (h UsersHandler) Method() string { + return http.MethodGet +} + +func (h UsersHandler) Pattern() string { + return "/users" +} +``` + +## Adding routes + +```go +// transport-inbound/router/module.go +package router + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewUsersHandler, + ), +) +``` + +## TODO + +- Elaborate further on how to use this module. diff --git a/docs/modules/events.md b/docs/modules/events.md new file mode 100644 index 0000000..550a333 --- /dev/null +++ b/docs/modules/events.md @@ -0,0 +1,144 @@ +# Events + +This module is a library with multiple adapters for processing messages. + +We'll assume that you're providing a zerolog logger to the application, a log +entry will be created each time a message is consumed or published. + +When consuming messages, a zerolog context will be provided with some default +fields like the topic name populated. + +Currently, we support: + +- RabbitMQ +- Channel (for testing/mocks) +- more to come in the future... + +## Definitions + +### `pkg/events` + +It's recommended to define the topics and payloads this way you can change them +easily and import them later in other services. + +```go +package events + +const TestTopic = "topic.test" + +type TestPayload struct { + Hello string `json:"hello"` +} +``` + +## RabbitMQ + +- Automatic reconnection with exponential backoffs +- Graceful shutdowns +- Panic recovery for consumers +- Publishing confirmation +- Re-publishing of messages that couldn't get delivered to the exchange +- Re-publishing of messages that would've been lost during a reconnection or + instability +- Smart decoder based on the content type of the message +- Configurable parallel consumption of messages using goroutines +- Simple auto-creation of queues, exchanges, binds, and dead letter queues + +```go +// main.go +package main + +import ( + thunderEventRabbitmq "github.com/gothunder/thunder/pkg/events/rabbitmq" + thunderLogs "github.com/gothunder/thunder/pkg/log" + + "github.com/rs/zerolog/diode" + "go.uber.org/fx" +) + +func main() { + var w diode.Writer + + app := fx.New( + // The order of these options isn't important. + thunderLogs.Module, + fx.Populate(&w), + + thunderEventRabbitmq.PublisherModule, + thunderEventRabbitmq.InvokeConsumer, + ) + app.Run() + + // This is required to flush the logs to stdout. + // We only want to do this after the app has exited. + thunderLogs.DiodeShutdown(w) +} +``` + +### Publisher + +If you're using the fx module, the publisher will automatically be started and +closed. + +When publishing an event, you'll be sending the struct that will be serialized +and sent to the exchange. If there's any error with the serialization, it'll be +returned back to you. + +The message will be published asynchronously, and any errors will be treated +and retried by the module. + +```go +type EventPublisher interface { + // StartPublisher starts the background go routine that will publish messages + // Returns an error if the publisher fails to start or reconnect + StartPublisher(context.Context) error + + // Publish publishes a message to the given topic + // The message is published asynchronously + // The message will be republished if the connection is lost + Publish( + ctx context.Context, + // The name of the event. + topic string, + // The payload of the event. + payload interface{}, + ) error + + // Close gracefully closes the publisher, making sure all messages are published + Close(context.Context) error +} +``` + +### Consumer + +Make sure that you define a single handler that matches the interface below. + +```go +type HandlerResponse int + +const ( + // Default, we remove the message from the queue. + Success HandlerResponse = iota + + // The message will be delivered to a server configured dead-letter queue. + DeadLetter + + // Deliver this message to a different worker. + Retry +) + +type EventDecoder interface { + // Decode decodes the payload into the given interface. + // Returns an error if the payload cannot be decoded. + Decode(v interface{}) error +} + +type Handler interface { + // The function that will be called when a message is received. + Handle(ctx context.Context, topic string, decoder EventDecoder) HandlerResponse + // The topics that will be subscribed to. + Topics() []string +} +``` + +You can find an example of a consumer [here](https://github.com/gothunder/thunder/tree/main/example/internal/transport-inbound/consumers). diff --git a/docs/modules/grpc.md b/docs/modules/grpc.md new file mode 100644 index 0000000..9a1650f --- /dev/null +++ b/docs/modules/grpc.md @@ -0,0 +1,250 @@ +# GRPC + +## Prerequisites + +Install the prerequisites listed in [gRPC Go - Quick Start](https://grpc.io/docs/languages/go/quickstart/#prerequisites). + +## Getting Started + +Add the following code to `cmd/generate/generate.go`: + +``` +//go:generate sh -c "protoc --experimental_allow_proto3_optional --go_opt=paths=source_relative --go-grpc_opt=paths=source_relative --proto_path=../transport-inbound/grpc/proto --go_out=../../pkg/grpc/ --go-grpc_out=../../pkg/grpc/ ../transport-inbound/grpc/proto/*.proto" +``` + +In `main.go`: + +```go + ... + fx.New( + fx.Populate(&w), + + // Adapters + thunderRabbitMQ.PublisherModule, + commonsConnection.Native, + thunderRouter.Module, + thunderLog.Module, + thunderGraphql.Module, + + // Internal Modules + transportoutbound.Module, + transportinbound.Module, + generated.Module, + features.Module, + + // Listeners + fx.Invoke( + commonsMigrate.MigrateUp, + auth.RegisterMiddleware, + thunderRouter.StartListener, + thunderGrpc.StartGrpcListener, <---- + ), + ).Run() + ... +``` + +Inside `internal/transport-inbound/module.go`, add the gRPC module: + +```go +package transportinbound + +import ( + "github.com/example/package/internal/transport-inbound/grpc" + + "go.uber.org/fx" +) + +var Module = fx.Options( + ... + grpc.Module, + ... +) +``` + +Inside the gRPC module, provide the gRPC server: + +```go +// internal/transport-inbound/grpc/server.go +package grpc + +import ( + "context" + "fmt" + "net" + + "github.com/example/package/v3/internal/features/commands" + "github.com/example/package/internal/features/queries" + pb "github.com/example/package/pkg/grpc" + featureserver "github.com/example/package/internal/transport-inbound/grpc/feature-server" + "github.com/rs/zerolog" + + thunderGrpc "github.com/gothunder/thunder/pkg/grpc" + + "google.golang.org/grpc" +) + +type server struct { + FeatureServer featureserver.FeatureServer + grpcServer *grpc.Server +} + +// NewGrpcServer creates a new grpc server +func NewGrpcServer(commands *commands.CommandGroup, queries *queries.QueryGroup, logger *zerolog.Logger) thunderGrpc.GrpcServer { + // Declares the server + server := instantiateNewGrpcServer(commands, queries, logger) + + // Register service servers + pb.RegisterFeatureServiceServer(server.grpcServer, server.FeatureServer) + + return server +} + +func instantiateNewGrpcServer(commands *commands.CommandGroup, queries *queries.QueryGroup, logger *zerolog.Logger) server { + server := server{ + FeatureServer: featureserver.NewFeatureServer(commands, queries), + } + + sv := grpc.NewServer( + grpc.UnaryInterceptor( + grpcLoggerInterceptor(logger), + ), + ) + server.grpcServer = sv + + return server +} + +func (s server) GetListener() (net.Listener, error) { + lis, err := net.Listen("tcp", fmt.Sprintf(":%d", 50051)) + if err != nil { + return nil, err + } + + return lis, nil +} + +func (s server) GetGrpcServer() *grpc.Server { + return s.grpcServer +} + +func grpcLoggerInterceptor(logger *zerolog.Logger) func(context.Context, interface{}, *grpc.UnaryServerInfo, grpc.UnaryHandler) (interface{}, error) { + + return func(ctx context.Context, + req interface{}, + info *grpc.UnaryServerInfo, + handler grpc.UnaryHandler) (interface{}, error) { + + // Add logger to context + lctx := logger.WithContext(ctx) + + // Calls the handler + h, err := handler(lctx, req) + + return h, err + } +} +``` + +In this case, we are using the `FeatureServer` from the `featureserver` +package. This struct implements the `FeatureServiceServer` interface generated +by the protobuf compiler from a `feature.proto` file. + +`commands` and `queries` can be omitted or replaced by any other dependency. + +As for the `feature-server` package, it should look like this: + +```go +// internal/transport-inbound/grpc/feature-server/server.go + +package featureserver + +import ( + "github.com/example/package/internal/features/commands" + "github.com/example/package/internal/features/queries" + pb "github.com/example/package/pkg/grpc" +) + +// FeatureServer defines the feature server +type FeatureServer struct { + pb.UnimplementedFeatureServiceServer + commands *commands.CommandGroup + queries *queries.QueryGroup +} + +// NewFeatureServer creates a new grpc server +func NewFeatureServer(commands *commands.CommandGroup, queries *queries.QueryGroup) FeatureServer { + server := FeatureServer{ + commands: commands, + queries: queries, + } + + return server +} +``` + +```go +// internal/transport-inbound/grpc/feature-server/rpcName.go + +package featureserver + +import ( + "context" + + "github.com/TheRafaBonin/roxy" + thunderGrpc "github.com/gothunder/thunder/pkg/grpc" + + "github.com/example/package/internal/errors" + "github.com/example/package/internal/features/queries/filters" + "github.com/example/package/internal/features/queries/relations" + "github.com/example/package/internal/transport-inbound/grpc/feature-server/formatters" + "github.com/example/package/internal/transport-inbound/grpc/feature-server/parsers" + + pb "github.com/example/package/pkg/grpc" +) + +// ListFeatureEntities ... +func (s FeatureServer) ListFeatureEntities(ctx context.Context, in *pb.ListFeatureEntitiesRequest) (*pb.ListFeatureEntitiesReply, error) { + // Validates input + if in == nil { + statusErr := thunderGrpc.HandleError( + ctx, + roxy.SetDefaultGrpcResponse(roxy.New("Input cannot be nil"), roxy.GrpcResponse{ + Message: "Invalid input provided", + Code: codes.InvalidArgument, + }) + + return nil, statusErr + } + + // Parse variables + parsedInput, err := parsers.ParseEntityFilters(in.EntityFilters) + err = roxy.Wrap(err, "parsers.ParseEntityFilters") + if err != nil { + statusErr := thunderGrpc.HandleError(ctx, err) + return nil, statusErr + } + + // Gets entities + entities, err := s.queries.Entity.FindMany( + ctx, + &filters.EntityFilterInput{ ... }, + ) + err = roxy.Wrap(err, "queries.Entity.FindMany") + if err != nil { + statusErr := thunderGrpc.HandleError(ctx, err) + return nil, statusErr + } + + // Formats response + formattedEntities, err := formatters.FormatEntities(ctx, Entities) + err = roxy.Wrap(err, "formatters.FormatEntities") + if err != nil { + statusErr := thunderGrpc.HandleError(ctx, err) + return nil, statusErr + } + + return &pb.ListFeatureEntitiesReply{ + Entities: formattedEntities, + }, nil +} +``` diff --git a/docs/modules/index.md b/docs/modules/index.md new file mode 100644 index 0000000..532f5f2 --- /dev/null +++ b/docs/modules/index.md @@ -0,0 +1,19 @@ +# Modules + +Here is the list of modules in this project: + +- [log](./logs.md): Provides a [zerolog](https://github.com/rs/zerolog) logger. +- graphql: Provides a GraphQL handler using [gqlgen](https://gqlgen.com/). +- chi: Provides a [chi](https://go-chi.io/#/) multiplexer with the default + middlewares. Also exposes a method to start a server with graceful + shutdown. +- mocks: Provides mocks for consumer and publisher, along with a channel to + receive intercepted messages. +- [rabbitmq](./events.md): Provides RabbitMQ publisher and consumer with + [amqp]("https://github.com/rabbitmq/amqp091-go"). +- [gRPC](./grpc.md): Provides a gRPC server and client. + +## TODO + +- [ent](https://entgo.io/): ORM that can be integrated with GraphQL through + `gqlgen`. diff --git a/docs/modules/logs.md b/docs/modules/logs.md new file mode 100644 index 0000000..8f25532 --- /dev/null +++ b/docs/modules/logs.md @@ -0,0 +1,40 @@ +# Logs + +This module wraps up the [zerolog](https://github.com/rs/zerolog) package. + +The other modules will be expecting you to provide a `zerolog` instance to them. + +Through the wrapper, you'll be able to configure the logging level via +environment variables, as well as enable pretty console output while in +development. + +Additionally, there's a dedicated marshaller for error stack traces. + +The logger is non-blocking, so you should make sure that all logs are flushed +before the app is closed (an example of the complete setup is provided below). + +```go +package main + +import ( + thunderLogs "github.com/gothunder/thunder/pkg/log" + + "github.com/rs/zerolog/diode" + "go.uber.org/fx" +) + +func main() { + var w diode.Writer + + app := fx.New( + // The order of these options isn't important. + thunderLogs.Module, + fx.Populate(&w), + ) + app.Run() + + // This is required to flush the logs to stdout. + // We only want to do this after the app has exited. + thunderLogs.DiodeShutdown(w) +} +``` diff --git a/docs/project-structure.md b/docs/project-structure.md new file mode 100644 index 0000000..a756cbf --- /dev/null +++ b/docs/project-structure.md @@ -0,0 +1,100 @@ +# Structuring your project + +## Directories + +We recommend that you separate each part of the application, isolating the +business logic and following a clean architecture, the idea is that you should +be able to refactor each part individually without affecting other parts of the +app. + +In this section, we'll give you an opinionated structure, it's based on +standards like CQRS, clean architecture, and hexagonal microservices. Feel free +to adapt this structure to your own needs, however, it should serve you well +especially as you scale your app codebase and complexity. + +This will be the structure used for any examples and docs moving forward. + +You can check [example](https://github.com/gothunder/thunder/tree/main/example) +to get a better idea of how this structure looks like. + +### `internal` + +This is a special directory for Go, everything that is inside can't be imported +outside of your app, think about this as similar to an src folder in languages +like JavaScript/Node.JS. + +### `internal/features` + +All domain logic + +### `internal/features/commands` + +Exposes methods that are used to change data, send events, making requests, +etc. You may also perform some validation and query existing data before making +any actions. Think of this as a controller or service layer. + +It's strongly recommended to use database transactions for complex commands +since you may want to roll back your data if there are any failures. + +You should also, always have the assumption that this command may be called +multiple times by your transports, maybe a user refreshed the page resending +the same request, or you received duplicated webhooks or even events were +consumed twice or more due to an instability or bug within your app. + +### `internal/features/queries` + +Exposes methods used to query the database, you may include default filters and +additional logic here before returning data. You shouldn't have anything in +here that changes the data available in the database. + +### `internal/features/repository` + +This is a simple abstraction for interacting with your database or ORM. You +should avoid adding business logic here. You should also avoid interacting with +the database / ORM without the use of your repository. + +### `internal/features/` + +This is where the code for any other feature that does not fit into the above +categories or transport modules should go. + +### `pkg/grpc` + +This is where `protoc` outputs the generated code for your gRPC services. You +shouldn't add any code here manually. + +### Examples + +#### `internal/features/controller` + +Abstraction to use the same logic for resolvers, REST and consumers. Note that +it may not be as useful since the logic may vary a lot between transports. + +#### `internal/features/domains` + +Internal feature to work with domains, including rules, parsers, etc. If used +by other modules, it should be moved to `pkg`. + +### `internal/transport-inbound` + +Every communication that comes into your app, includes API routes, GraphQL +resolvers, event consumers, webhooks, etc. + +This directory may be further split into subdirectories based on the type of +transport, that being: + +- `graphql` for GraphQL resolvers and schemas +- `routes` for HTTP server handlers +- `webhooks` for webhooks +- `consumers` for event consumers + +### `internal/transport-outbound` + +Every communication that your app does, including event publishers, code that +interacts with external APIs, etc. + +### `pkg` + +This is similar to a dist folder in languages like JavaScript/Node.JS, here +you can add any code that is meant to be imported by other services. For +example event definitions. diff --git a/example/bannedDomains/README.md b/example/bannedDomains/README.md new file mode 100644 index 0000000..87cecd3 --- /dev/null +++ b/example/bannedDomains/README.md @@ -0,0 +1,17 @@ +# Banned domains + +This example show how to use thunder to create two services that communicate +with each other using RabbitMQ. The first service is an API that receives an +email and stores it in a database. The second service is a worker that reads +the emails from the database and checks if the domain is banned. If it is, it +publishes a message to a queue that the first service reads and deletes the +email from the database. + +Note that in the real world there is a gap between the time the email is stored +in the database and the time it's checked against the ban list. + +## Setup + +- Spin up a RabbitMQ container +- cd into the `email` directory and run `go run main.go` +- cd into the `ban` directory and run `go run main.go` diff --git a/example/bannedDomains/ban/banned_domains.txt b/example/bannedDomains/ban/banned_domains.txt new file mode 100644 index 0000000..ae49e2b --- /dev/null +++ b/example/bannedDomains/ban/banned_domains.txt @@ -0,0 +1,10 @@ +0-00.usa.cc +animacuratio.ru +citasyencuentros-meet-backup.com +evonse.com +hotmail.com.kids316.com +llerchaougin.gq +nhi9ti90tq5lowtih.tk +realidmichigan.com +tafilfa.ml +webpinoyako.ru diff --git a/example/bannedDomains/ban/go.mod b/example/bannedDomains/ban/go.mod new file mode 100644 index 0000000..6973352 --- /dev/null +++ b/example/bannedDomains/ban/go.mod @@ -0,0 +1,27 @@ +module github.com/gothunder/thunder/example/ban + +go 1.19 + +replace github.com/gothunder/thunder/example/email => ../email + +require ( + github.com/gothunder/thunder v0.5.1 + github.com/gothunder/thunder/example/email v0.0.0-20230102180253-e0b111ffa5c9 + github.com/rs/zerolog v1.28.0 + go.uber.org/fx v1.18.2 +) + +require ( + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/rabbitmq/amqp091-go v1.5.0 // indirect + github.com/rotisserie/eris v0.5.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/dig v1.15.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/sys v0.3.0 // indirect +) diff --git a/example/bannedDomains/ban/go.sum b/example/bannedDomains/ban/go.sum new file mode 100644 index 0000000..698d472 --- /dev/null +++ b/example/bannedDomains/ban/go.sum @@ -0,0 +1,92 @@ +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gothunder/thunder v0.5.1 h1:Ju28dyAeozKXMwoGVV8HLXjDuUfoHwiA2ZbsIlrZPok= +github.com/gothunder/thunder v0.5.1/go.mod h1:iVewZauzVqL0bUMVNPAlsDzx8vdgIB1PGhysWN8PnK8= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/gothunder/thunder/example/email v0.0.0-20230102180253-e0b111ffa5c9 h1:05XVs2BPGm2BFO7GrjrVGAVCKEaV3stSHVpbr1odciQ= +github.com/gothunder/thunder/example/email v0.0.0-20230102180253-e0b111ffa5c9/go.mod h1:wl8sQkUP8L+ECzp7hVfv9vESIDb+Ucjx8FsmyyKA3CQ= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.5.0 h1:VouyHPBu1CrKyJVfteGknGOGCzmOz0zcv/tONLkb7rg= +github.com/rabbitmq/amqp091-go v1.5.0/go.mod h1:JsV0ofX5f1nwOGafb8L5rBItt9GyhfQfcJj+oyz0dGg= +github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= +github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE= +go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= +go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU= +go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/example/bannedDomains/ban/internal/features/commands/commandGroup.go b/example/bannedDomains/ban/internal/features/commands/commandGroup.go new file mode 100644 index 0000000..55c2c05 --- /dev/null +++ b/example/bannedDomains/ban/internal/features/commands/commandGroup.go @@ -0,0 +1,30 @@ +package commands + +import ( + "github.com/gothunder/thunder/example/ban/internal/features/domains" + "github.com/gothunder/thunder/example/ban/internal/transport-outbound/publisher" + "github.com/rs/zerolog" + "go.uber.org/fx" +) + +type CommandGroup struct { + publisherGroup *publisher.PublisherGroup + log *zerolog.Logger + domains *domains.Domains +} + +type CommandGroupInput struct { + fx.In + + PublisherGroup *publisher.PublisherGroup + Log *zerolog.Logger + Domains *domains.Domains +} + +func NewCommandGroup(input CommandGroupInput) *CommandGroup { + return &CommandGroup{ + publisherGroup: input.PublisherGroup, + log: input.Log, + domains: input.Domains, + } +} diff --git a/example/bannedDomains/ban/internal/features/commands/module.go b/example/bannedDomains/ban/internal/features/commands/module.go new file mode 100644 index 0000000..419f2a7 --- /dev/null +++ b/example/bannedDomains/ban/internal/features/commands/module.go @@ -0,0 +1,9 @@ +package commands + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewCommandGroup, + ), +) diff --git a/example/bannedDomains/ban/internal/features/commands/sendDomainResponse.go b/example/bannedDomains/ban/internal/features/commands/sendDomainResponse.go new file mode 100644 index 0000000..31244e3 --- /dev/null +++ b/example/bannedDomains/ban/internal/features/commands/sendDomainResponse.go @@ -0,0 +1,26 @@ +package commands + +import ( + "context" + + "github.com/gothunder/thunder/example/ban/pkg/events" + "github.com/rs/zerolog/log" +) + +// SendDomainResponse sends the result of domain check to RabbitMQ. +func (cg CommandGroup) SendDomainResponse(ctx context.Context, domain string, id int) { + log.Ctx(ctx).Debug().Msgf("Checking ban for '%s'", domain) + isBanned := cg.domains.IsBanned(domain) + + if isBanned { + log.Ctx(ctx).Debug().Msgf("Domain '%s' is banned!", domain) + cg.publisherGroup.SendBanEvent( + ctx, + events.BanPayload{ + ID: id, + }, + ) + } else { + log.Ctx(ctx).Debug().Msgf("Domain '%s' is not banned!", domain) + } +} diff --git a/example/bannedDomains/ban/internal/features/domains/ban.go b/example/bannedDomains/ban/internal/features/domains/ban.go new file mode 100644 index 0000000..20a1baa --- /dev/null +++ b/example/bannedDomains/ban/internal/features/domains/ban.go @@ -0,0 +1,43 @@ +package domains + +import ( + "bufio" + "os" +) + +// Domains is a feature that checks if a domain is banned. +type Domains struct { + bannedDomains map[string]interface{} +} + +// NewDomains creates a new Domains feature. +func NewDomains() *Domains { + d := &Domains{ + bannedDomains: make(map[string]interface{}), + } + + d.readDomains() + + return d +} + +// IsBanned checks if a domain is banned. +func (d Domains) IsBanned(domain string) bool { + _, ok := d.bannedDomains[domain] + + return ok +} + +// readDomains reads a list of banned domains from a file. +func (d Domains) readDomains() { + bannedDomainsSource, err := os.Open("banned_domains.txt") + if err != nil { + panic(err) + } + defer bannedDomainsSource.Close() + + bannedDomains := bufio.NewScanner(bannedDomainsSource) + for bannedDomains.Scan() { + d.bannedDomains[bannedDomains.Text()] = nil + } +} diff --git a/example/bannedDomains/ban/internal/features/domains/module.go b/example/bannedDomains/ban/internal/features/domains/module.go new file mode 100644 index 0000000..f0277e5 --- /dev/null +++ b/example/bannedDomains/ban/internal/features/domains/module.go @@ -0,0 +1,9 @@ +package domains + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewDomains, + ), +) diff --git a/example/bannedDomains/ban/internal/features/module.go b/example/bannedDomains/ban/internal/features/module.go new file mode 100644 index 0000000..2fdf377 --- /dev/null +++ b/example/bannedDomains/ban/internal/features/module.go @@ -0,0 +1,13 @@ +package features + +import ( + "github.com/gothunder/thunder/example/ban/internal/features/commands" + "github.com/gothunder/thunder/example/ban/internal/features/domains" + "go.uber.org/fx" +) + +// Module is a collection of features that can be used in a fx application. +var Module = fx.Options( + domains.Module, + commands.Module, +) diff --git a/example/bannedDomains/ban/internal/transport-inbound/consumers/consumerGroup.go b/example/bannedDomains/ban/internal/transport-inbound/consumers/consumerGroup.go new file mode 100644 index 0000000..acb3328 --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-inbound/consumers/consumerGroup.go @@ -0,0 +1,27 @@ +package consumers + +import ( + "github.com/gothunder/thunder/example/ban/internal/features/domains" + "github.com/gothunder/thunder/example/ban/internal/transport-outbound/publisher" + thunderEvents "github.com/gothunder/thunder/pkg/events" + "go.uber.org/fx" +) + +type ConsumerGroup struct { + domains *domains.Domains + pg *publisher.PublisherGroup +} + +type ConsumerGroupOptions struct { + fx.In + + Domains *domains.Domains + PublisherGroup *publisher.PublisherGroup +} + +func newConsumerGroup(options ConsumerGroupOptions) thunderEvents.Handler { + return &ConsumerGroup{ + domains: options.Domains, + pg: options.PublisherGroup, + } +} diff --git a/example/bannedDomains/ban/internal/transport-inbound/consumers/emailEvent.go b/example/bannedDomains/ban/internal/transport-inbound/consumers/emailEvent.go new file mode 100644 index 0000000..8034155 --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-inbound/consumers/emailEvent.go @@ -0,0 +1,21 @@ +package consumers + +import ( + "context" + "strings" + + "github.com/gothunder/thunder/example/ban/pkg/events" + emailEvents "github.com/gothunder/thunder/example/email/pkg/events" + thunderEvents "github.com/gothunder/thunder/pkg/events" +) + +func (c *ConsumerGroup) emailEvent(ctx context.Context, payload emailEvents.EmailPayload) thunderEvents.HandlerResponse { + domain := strings.Split(payload.Email, "@")[1] + if c.domains.IsBanned(domain) { + c.pg.SendBanEvent(ctx, events.BanPayload{ + ID: payload.ID, + }) + } + + return thunderEvents.Success +} diff --git a/example/bannedDomains/ban/internal/transport-inbound/consumers/handle.go b/example/bannedDomains/ban/internal/transport-inbound/consumers/handle.go new file mode 100644 index 0000000..eb9268d --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-inbound/consumers/handle.go @@ -0,0 +1,26 @@ +package consumers + +import ( + "context" + + thunderEvents "github.com/gothunder/thunder/pkg/events" + "github.com/gothunder/thunder/example/email/pkg/events" + "github.com/rs/zerolog/log" +) + +func (c *ConsumerGroup) Handle(ctx context.Context, topic string, decoder thunderEvents.EventDecoder) thunderEvents.HandlerResponse { + switch { + case topic == events.EmailTopic: + var formattedPayload events.EmailPayload + err := decoder.Decode(&formattedPayload) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("failed to decode payload") + return thunderEvents.DeadLetter + } + log.Ctx(ctx).Debug().Msgf("Got the email '%s' to check for ban", formattedPayload.Email) + + return c.emailEvent(ctx, formattedPayload) + default: + return thunderEvents.DeadLetter + } +} diff --git a/example/internal/transport-inbound/consumers/module.go b/example/bannedDomains/ban/internal/transport-inbound/consumers/module.go similarity index 100% rename from example/internal/transport-inbound/consumers/module.go rename to example/bannedDomains/ban/internal/transport-inbound/consumers/module.go diff --git a/example/bannedDomains/ban/internal/transport-inbound/consumers/topics.go b/example/bannedDomains/ban/internal/transport-inbound/consumers/topics.go new file mode 100644 index 0000000..fcc5abb --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-inbound/consumers/topics.go @@ -0,0 +1,9 @@ +package consumers + +import "github.com/gothunder/thunder/example/email/pkg/events" + +func (c *ConsumerGroup) Topics() []string { + return []string{ + events.EmailTopic, + } +} diff --git a/example/bannedDomains/ban/internal/transport-inbound/module.go b/example/bannedDomains/ban/internal/transport-inbound/module.go new file mode 100644 index 0000000..2ea4150 --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-inbound/module.go @@ -0,0 +1,10 @@ +package transportinbound + +import ( + "github.com/gothunder/thunder/example/ban/internal/transport-inbound/consumers" + "go.uber.org/fx" +) + +var Module = fx.Options( + consumers.Module, +) diff --git a/example/bannedDomains/ban/internal/transport-outbound/module.go b/example/bannedDomains/ban/internal/transport-outbound/module.go new file mode 100644 index 0000000..582562a --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-outbound/module.go @@ -0,0 +1,10 @@ +package transportoutbound + +import ( + "github.com/gothunder/thunder/example/ban/internal/transport-outbound/publisher" + "go.uber.org/fx" +) + +var Module = fx.Options( + publisher.Module, +) diff --git a/example/bannedDomains/ban/internal/transport-outbound/publisher/banEvent.go b/example/bannedDomains/ban/internal/transport-outbound/publisher/banEvent.go new file mode 100644 index 0000000..ed01d6e --- /dev/null +++ b/example/bannedDomains/ban/internal/transport-outbound/publisher/banEvent.go @@ -0,0 +1,11 @@ +package publisher + +import ( + "context" + + "github.com/gothunder/thunder/example/ban/pkg/events" +) + +func (pg *PublisherGroup) SendBanEvent(ctx context.Context, event events.BanPayload) error { + return pg.publisher.Publish(ctx, events.BanTopic, event) +} diff --git a/example/internal/transport-outbound/publisher/module.go b/example/bannedDomains/ban/internal/transport-outbound/publisher/module.go similarity index 100% rename from example/internal/transport-outbound/publisher/module.go rename to example/bannedDomains/ban/internal/transport-outbound/publisher/module.go diff --git a/example/internal/transport-outbound/publisher/publisherGroup.go b/example/bannedDomains/ban/internal/transport-outbound/publisher/publisherGroup.go similarity index 100% rename from example/internal/transport-outbound/publisher/publisherGroup.go rename to example/bannedDomains/ban/internal/transport-outbound/publisher/publisherGroup.go diff --git a/example/bannedDomains/ban/main.go b/example/bannedDomains/ban/main.go new file mode 100644 index 0000000..dea7ed9 --- /dev/null +++ b/example/bannedDomains/ban/main.go @@ -0,0 +1,34 @@ +package main + +import ( + thunderEventRabbitmq "github.com/gothunder/thunder/pkg/events/rabbitmq" + thunderLogs "github.com/gothunder/thunder/pkg/log" + "github.com/gothunder/thunder/example/ban/internal/features" + transportinbound "github.com/gothunder/thunder/example/ban/internal/transport-inbound" + transportoutbound "github.com/gothunder/thunder/example/ban/internal/transport-outbound" + + "github.com/rs/zerolog/diode" + "go.uber.org/fx" +) + +func main() { + var w diode.Writer + + app := fx.New( + // The order of these options isn't important. + thunderLogs.Module, + fx.Populate(&w), + + transportinbound.Module, + transportoutbound.Module, + features.Module, + + thunderEventRabbitmq.PublisherModule, + thunderEventRabbitmq.InvokeConsumer, + ) + app.Run() + + // This is required to flush the logs to stdout. + // We only want to do this after the app has exited. + thunderLogs.DiodeShutdown(w) +} diff --git a/example/bannedDomains/ban/pkg/events/ban.go b/example/bannedDomains/ban/pkg/events/ban.go new file mode 100644 index 0000000..e7858fa --- /dev/null +++ b/example/bannedDomains/ban/pkg/events/ban.go @@ -0,0 +1,7 @@ +package events + +const BanTopic = "topic.ban" + +type BanPayload struct { + ID int `json:"id"` +} diff --git a/example/bannedDomains/email/go.mod b/example/bannedDomains/email/go.mod new file mode 100644 index 0000000..dcba802 --- /dev/null +++ b/example/bannedDomains/email/go.mod @@ -0,0 +1,41 @@ +module github.com/gothunder/thunder/example/email + +go 1.19 + +replace github.com/gothunder/thunder/example/ban => ../ban + +require ( + entgo.io/ent v0.11.4 + github.com/gothunder/thunder v0.5.1 + github.com/gothunder/thunder/example/ban v0.0.0-20230102180253-e0b111ffa5c9 + github.com/rs/zerolog v1.28.0 + go.uber.org/fx v1.18.2 +) + +require ( + ariga.io/atlas v0.7.3-0.20221011160332-3ca609863edd // indirect + github.com/agext/levenshtein v1.2.1 // indirect + github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/go-chi/chi/v5 v5.0.7 // indirect + github.com/go-openapi/inflect v0.19.0 // indirect + github.com/google/go-cmp v0.5.9 // indirect + github.com/google/uuid v1.3.0 // indirect + github.com/hashicorp/hcl/v2 v2.13.0 // indirect + github.com/lib/pq v1.10.7 + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.16 // indirect + github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 // indirect + github.com/rabbitmq/amqp091-go v1.5.0 // indirect + github.com/rotisserie/eris v0.5.4 // indirect + github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect + github.com/zclconf/go-cty v1.8.0 // indirect + go.uber.org/atomic v1.7.0 // indirect + go.uber.org/dig v1.15.0 // indirect + go.uber.org/multierr v1.6.0 // indirect + go.uber.org/zap v1.17.0 // indirect + golang.org/x/mod v0.6.0 // indirect + golang.org/x/sys v0.3.0 // indirect + golang.org/x/text v0.5.0 // indirect +) diff --git a/example/bannedDomains/email/go.sum b/example/bannedDomains/email/go.sum new file mode 100644 index 0000000..3b44b1b --- /dev/null +++ b/example/bannedDomains/email/go.sum @@ -0,0 +1,136 @@ +ariga.io/atlas v0.7.3-0.20221011160332-3ca609863edd h1:c3F2jvvEZzsoH/KUpDNhTsCVeUPnpXaF8kADZvUSiU0= +ariga.io/atlas v0.7.3-0.20221011160332-3ca609863edd/go.mod h1:ft47uSh5hWGDCmQC9DsztZg6Xk+KagM5Ts/mZYKb9JE= +entgo.io/ent v0.11.4 h1:grwVY0fp31BZ6oEo3YrXenAuv8VJmEw7F/Bi6WqeH3Q= +entgo.io/ent v0.11.4/go.mod h1:fnQIXL36RYnCk/9nvG4aE7YHBFZhCycfh7wMjY5p7SE= +github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60= +github.com/agext/levenshtein v1.2.1 h1:QmvMAjj2aEICytGiWzmxoE0x2KZvE0fvmqMOfy2tjT8= +github.com/agext/levenshtein v1.2.1/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= +github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA= +github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= +github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= +github.com/benbjohnson/clock v1.3.0 h1:ip6w0uFQkncKQ979AypyG0ER7mqUSBdKLOgAle/AT8A= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/coreos/go-systemd/v22 v22.3.3-0.20220203105225-a9a7ef127534/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/go-chi/chi/v5 v5.0.7 h1:rDTPXLDHGATaeHvVlLcR4Qe0zftYethFucbjVQ1PxU8= +github.com/go-chi/chi/v5 v5.0.7/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= +github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4= +github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4= +github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= +github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gothunder/thunder v0.5.1 h1:Ju28dyAeozKXMwoGVV8HLXjDuUfoHwiA2ZbsIlrZPok= +github.com/gothunder/thunder v0.5.1/go.mod h1:iVewZauzVqL0bUMVNPAlsDzx8vdgIB1PGhysWN8PnK8= +github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= +github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348 h1:MtvEpTB6LX3vkb4ax0b5D2DHbNAUsen0Gx5wZoq3lV4= +github.com/lib/pq v1.10.7 h1:p7ZhMD+KsSRozJr34udlUrhboJwWAgCg34+/ZZNvZZw= +github.com/lib/pq v1.10.7/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16 h1:bq3VjFmv/sOjHtdEhmkEV4x1AJtvUvOJ2PFAZ5+peKQ= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7 h1:DpOJ2HYzCv8LZP15IdmG+YdwD2luVPHITV96TkirNBM= +github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rabbitmq/amqp091-go v1.5.0 h1:VouyHPBu1CrKyJVfteGknGOGCzmOz0zcv/tONLkb7rg= +github.com/rabbitmq/amqp091-go v1.5.0/go.mod h1:JsV0ofX5f1nwOGafb8L5rBItt9GyhfQfcJj+oyz0dGg= +github.com/rotisserie/eris v0.5.4 h1:Il6IvLdAapsMhvuOahHWiBnl1G++Q0/L5UIkI5mARSk= +github.com/rotisserie/eris v0.5.4/go.mod h1:Z/kgYTJiJtocxCbFfvRmO+QejApzG6zpyky9G1A4g9s= +github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.28.0 h1:MirSo27VyNi7RJYP3078AA1+Cyzd2GB66qy3aUHvsWY= +github.com/rs/zerolog v1.28.0/go.mod h1:NILgTygv/Uej1ra5XxGf82ZFSLk58MFGAUS2o6usyD0= +github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/vmihailenco/msgpack/v4 v4.3.12/go.mod h1:gborTTJjAo/GWTqqRjrLCn9pgNN+NXzzngzBKDPIqw4= +github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= +github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/tagparser v0.1.1/go.mod h1:OeAg3pn3UbLjkWt+rN9oFYB6u/cQgqMEUPoW2WPyhdI= +github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= +github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= +github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= +github.com/zclconf/go-cty v1.8.0 h1:s4AvqaeQzJIu3ndv4gVIhplVD0krU+bgrcLSVUnaWuA= +github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= +go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/dig v1.15.0 h1:vq3YWr8zRj1eFGC7Gvf907hE0eRjPTZ1d3xHadD6liE= +go.uber.org/dig v1.15.0/go.mod h1:pKHs0wMynzL6brANhB2hLMro+zalv1osARTviTcqHLM= +go.uber.org/fx v1.18.2 h1:bUNI6oShr+OVFQeU8cDNbnN7VFsu+SsjHzUF51V/GAU= +go.uber.org/fx v1.18.2/go.mod h1:g0V1KMQ66zIRk8bLu3Ea5Jt2w/cHlOIp4wdRsgh0JaY= +go.uber.org/goleak v1.1.12 h1:gZAh5/EyT/HQwlpkCy6wTpqfH9H8Lz8zbm3dZh+OyzA= +go.uber.org/goleak v1.1.12/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= +go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= +go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= +go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= +go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= +golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/mod v0.6.0 h1:b9gGHsz9/HhJ3HF5DHQytPpuwocVTChQJK3AvoLRD5I= +golang.org/x/mod v0.6.0/go.mod h1:4mET923SAdbXp2ki8ey+zGs1SLqsuM2Y0uvdZR/fUNI= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.3.0 h1:w8ZOecv6NaNa/zC8944JTU3vz4u6Lagfk4RPQxv92NQ= +golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.5.0 h1:OLmvp0KP+FVG99Ct/qFiL/Fhk4zp4QQnZ7b2U+5piUM= +golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/example/bannedDomains/email/internal/features/email.go b/example/bannedDomains/email/internal/features/email.go new file mode 100644 index 0000000..50a4a9c --- /dev/null +++ b/example/bannedDomains/email/internal/features/email.go @@ -0,0 +1,45 @@ +package features + +import ( + "context" + "regexp" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent" + "github.com/rs/zerolog/log" +) + +type EmailService struct { + client *ent.Client +} + +func NewEmailService(client *ent.Client) EmailService { + return EmailService{ + client: client, + } +} + +func (e EmailService) Create(ctx context.Context, email string) (*ent.Email, error) { + emailRegistry, err := e.client.Email.Create().SetEmail(email).Save(ctx) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("failed creating email") + return nil, err + } + log.Ctx(ctx).Debug().Msg("email created") + return emailRegistry, nil +} + +func (e EmailService) Delete(ctx context.Context, id int) error { + err := e.client.Email.DeleteOneID(id).Exec(ctx) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("failed deleting email") + return err + } + log.Ctx(ctx).Debug().Msg("email deleted") + return nil +} + +// IsValidEmail checks if the email is valid using a regex pattern +func IsValidEmail(email string) bool { + emailRegex := regexp.MustCompile(`^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`) + return emailRegex.MatchString(email) +} diff --git a/example/bannedDomains/email/internal/features/module.go b/example/bannedDomains/email/internal/features/module.go new file mode 100644 index 0000000..df5f06d --- /dev/null +++ b/example/bannedDomains/email/internal/features/module.go @@ -0,0 +1,9 @@ +package features + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewEmailService, + ), +) diff --git a/example/bannedDomains/email/internal/features/repository/ent/client.go b/example/bannedDomains/email/internal/features/repository/ent/client.go new file mode 100644 index 0000000..19a8592 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/client.go @@ -0,0 +1,213 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "log" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/migrate" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql" +) + +// Client is the client that holds all ent builders. +type Client struct { + config + // Schema is the client for creating, migrating and dropping schema. + Schema *migrate.Schema + // Email is the client for interacting with the Email builders. + Email *EmailClient +} + +// NewClient creates a new client configured with the given options. +func NewClient(opts ...Option) *Client { + cfg := config{log: log.Println, hooks: &hooks{}} + cfg.options(opts...) + client := &Client{config: cfg} + client.init() + return client +} + +func (c *Client) init() { + c.Schema = migrate.NewSchema(c.driver) + c.Email = NewEmailClient(c.config) +} + +// Open opens a database/sql.DB specified by the driver name and +// the data source name, and returns a new client attached to it. +// Optional parameters can be added for configuring the client. +func Open(driverName, dataSourceName string, options ...Option) (*Client, error) { + switch driverName { + case dialect.MySQL, dialect.Postgres, dialect.SQLite: + drv, err := sql.Open(driverName, dataSourceName) + if err != nil { + return nil, err + } + return NewClient(append(options, Driver(drv))...), nil + default: + return nil, fmt.Errorf("unsupported driver: %q", driverName) + } +} + +// Tx returns a new transactional client. The provided context +// is used until the transaction is committed or rolled back. +func (c *Client) Tx(ctx context.Context) (*Tx, error) { + if _, ok := c.driver.(*txDriver); ok { + return nil, errors.New("ent: cannot start a transaction within a transaction") + } + tx, err := newTx(ctx, c.driver) + if err != nil { + return nil, fmt.Errorf("ent: starting a transaction: %w", err) + } + cfg := c.config + cfg.driver = tx + return &Tx{ + ctx: ctx, + config: cfg, + Email: NewEmailClient(cfg), + }, nil +} + +// BeginTx returns a transactional client with specified options. +func (c *Client) BeginTx(ctx context.Context, opts *sql.TxOptions) (*Tx, error) { + if _, ok := c.driver.(*txDriver); ok { + return nil, errors.New("ent: cannot start a transaction within a transaction") + } + tx, err := c.driver.(interface { + BeginTx(context.Context, *sql.TxOptions) (dialect.Tx, error) + }).BeginTx(ctx, opts) + if err != nil { + return nil, fmt.Errorf("ent: starting a transaction: %w", err) + } + cfg := c.config + cfg.driver = &txDriver{tx: tx, drv: c.driver} + return &Tx{ + ctx: ctx, + config: cfg, + Email: NewEmailClient(cfg), + }, nil +} + +// Debug returns a new debug-client. It's used to get verbose logging on specific operations. +// +// client.Debug(). +// Email. +// Query(). +// Count(ctx) +func (c *Client) Debug() *Client { + if c.debug { + return c + } + cfg := c.config + cfg.driver = dialect.Debug(c.driver, c.log) + client := &Client{config: cfg} + client.init() + return client +} + +// Close closes the database connection and prevents new queries from starting. +func (c *Client) Close() error { + return c.driver.Close() +} + +// Use adds the mutation hooks to all the entity clients. +// In order to add hooks to a specific client, call: `client.Node.Use(...)`. +func (c *Client) Use(hooks ...Hook) { + c.Email.Use(hooks...) +} + +// EmailClient is a client for the Email schema. +type EmailClient struct { + config +} + +// NewEmailClient returns a client for the Email from the given config. +func NewEmailClient(c config) *EmailClient { + return &EmailClient{config: c} +} + +// Use adds a list of mutation hooks to the hooks stack. +// A call to `Use(f, g, h)` equals to `email.Hooks(f(g(h())))`. +func (c *EmailClient) Use(hooks ...Hook) { + c.hooks.Email = append(c.hooks.Email, hooks...) +} + +// Create returns a builder for creating a Email entity. +func (c *EmailClient) Create() *EmailCreate { + mutation := newEmailMutation(c.config, OpCreate) + return &EmailCreate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// CreateBulk returns a builder for creating a bulk of Email entities. +func (c *EmailClient) CreateBulk(builders ...*EmailCreate) *EmailCreateBulk { + return &EmailCreateBulk{config: c.config, builders: builders} +} + +// Update returns an update builder for Email. +func (c *EmailClient) Update() *EmailUpdate { + mutation := newEmailMutation(c.config, OpUpdate) + return &EmailUpdate{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOne returns an update builder for the given entity. +func (c *EmailClient) UpdateOne(e *Email) *EmailUpdateOne { + mutation := newEmailMutation(c.config, OpUpdateOne, withEmail(e)) + return &EmailUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// UpdateOneID returns an update builder for the given id. +func (c *EmailClient) UpdateOneID(id int) *EmailUpdateOne { + mutation := newEmailMutation(c.config, OpUpdateOne, withEmailID(id)) + return &EmailUpdateOne{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// Delete returns a delete builder for Email. +func (c *EmailClient) Delete() *EmailDelete { + mutation := newEmailMutation(c.config, OpDelete) + return &EmailDelete{config: c.config, hooks: c.Hooks(), mutation: mutation} +} + +// DeleteOne returns a builder for deleting the given entity. +func (c *EmailClient) DeleteOne(e *Email) *EmailDeleteOne { + return c.DeleteOneID(e.ID) +} + +// DeleteOneID returns a builder for deleting the given entity by its id. +func (c *EmailClient) DeleteOneID(id int) *EmailDeleteOne { + builder := c.Delete().Where(email.ID(id)) + builder.mutation.id = &id + builder.mutation.op = OpDeleteOne + return &EmailDeleteOne{builder} +} + +// Query returns a query builder for Email. +func (c *EmailClient) Query() *EmailQuery { + return &EmailQuery{ + config: c.config, + } +} + +// Get returns a Email entity by its id. +func (c *EmailClient) Get(ctx context.Context, id int) (*Email, error) { + return c.Query().Where(email.ID(id)).Only(ctx) +} + +// GetX is like Get, but panics if an error occurs. +func (c *EmailClient) GetX(ctx context.Context, id int) *Email { + obj, err := c.Get(ctx, id) + if err != nil { + panic(err) + } + return obj +} + +// Hooks returns the client hooks. +func (c *EmailClient) Hooks() []Hook { + return c.hooks.Email +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/config.go b/example/bannedDomains/email/internal/features/repository/ent/config.go new file mode 100644 index 0000000..9f2e383 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/config.go @@ -0,0 +1,59 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "entgo.io/ent" + "entgo.io/ent/dialect" +) + +// Option function to configure the client. +type Option func(*config) + +// Config is the configuration for the client and its builder. +type config struct { + // driver used for executing database requests. + driver dialect.Driver + // debug enable a debug logging. + debug bool + // log used for logging on debug mode. + log func(...any) + // hooks to execute on mutations. + hooks *hooks +} + +// hooks per client, for fast access. +type hooks struct { + Email []ent.Hook +} + +// Options applies the options on the config object. +func (c *config) options(opts ...Option) { + for _, opt := range opts { + opt(c) + } + if c.debug { + c.driver = dialect.Debug(c.driver, c.log) + } +} + +// Debug enables debug logging on the ent.Driver. +func Debug() Option { + return func(c *config) { + c.debug = true + } +} + +// Log sets the logging function for debug mode. +func Log(fn func(...any)) Option { + return func(c *config) { + c.log = fn + } +} + +// Driver configures the client driver. +func Driver(driver dialect.Driver) Option { + return func(c *config) { + c.driver = driver + } +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/context.go b/example/bannedDomains/email/internal/features/repository/ent/context.go new file mode 100644 index 0000000..7811bfa --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/context.go @@ -0,0 +1,33 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" +) + +type clientCtxKey struct{} + +// FromContext returns a Client stored inside a context, or nil if there isn't one. +func FromContext(ctx context.Context) *Client { + c, _ := ctx.Value(clientCtxKey{}).(*Client) + return c +} + +// NewContext returns a new context with the given Client attached. +func NewContext(parent context.Context, c *Client) context.Context { + return context.WithValue(parent, clientCtxKey{}, c) +} + +type txCtxKey struct{} + +// TxFromContext returns a Tx stored inside a context, or nil if there isn't one. +func TxFromContext(ctx context.Context) *Tx { + tx, _ := ctx.Value(txCtxKey{}).(*Tx) + return tx +} + +// NewTxContext returns a new context with the given Tx attached. +func NewTxContext(parent context.Context, tx *Tx) context.Context { + return context.WithValue(parent, txCtxKey{}, tx) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/email.go b/example/bannedDomains/email/internal/features/repository/ent/email.go new file mode 100644 index 0000000..bb81711 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email.go @@ -0,0 +1,124 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "fmt" + "strings" + "time" + + "entgo.io/ent/dialect/sql" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" +) + +// Email is the model entity for the Email schema. +type Email struct { + config `json:"-"` + // ID of the ent. + ID int `json:"id,omitempty"` + // Email holds the value of the "email" field. + Email string `json:"email,omitempty"` + // CreatedAt holds the value of the "createdAt" field. + CreatedAt time.Time `json:"createdAt,omitempty"` + // UpdatedAt holds the value of the "updatedAt" field. + UpdatedAt time.Time `json:"updatedAt,omitempty"` +} + +// scanValues returns the types for scanning values from sql.Rows. +func (*Email) scanValues(columns []string) ([]any, error) { + values := make([]any, len(columns)) + for i := range columns { + switch columns[i] { + case email.FieldID: + values[i] = new(sql.NullInt64) + case email.FieldEmail: + values[i] = new(sql.NullString) + case email.FieldCreatedAt, email.FieldUpdatedAt: + values[i] = new(sql.NullTime) + default: + return nil, fmt.Errorf("unexpected column %q for type Email", columns[i]) + } + } + return values, nil +} + +// assignValues assigns the values that were returned from sql.Rows (after scanning) +// to the Email fields. +func (e *Email) assignValues(columns []string, values []any) error { + if m, n := len(values), len(columns); m < n { + return fmt.Errorf("mismatch number of scan values: %d != %d", m, n) + } + for i := range columns { + switch columns[i] { + case email.FieldID: + value, ok := values[i].(*sql.NullInt64) + if !ok { + return fmt.Errorf("unexpected type %T for field id", value) + } + e.ID = int(value.Int64) + case email.FieldEmail: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field email", values[i]) + } else if value.Valid { + e.Email = value.String + } + case email.FieldCreatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field createdAt", values[i]) + } else if value.Valid { + e.CreatedAt = value.Time + } + case email.FieldUpdatedAt: + if value, ok := values[i].(*sql.NullTime); !ok { + return fmt.Errorf("unexpected type %T for field updatedAt", values[i]) + } else if value.Valid { + e.UpdatedAt = value.Time + } + } + } + return nil +} + +// Update returns a builder for updating this Email. +// Note that you need to call Email.Unwrap() before calling this method if this Email +// was returned from a transaction, and the transaction was committed or rolled back. +func (e *Email) Update() *EmailUpdateOne { + return (&EmailClient{config: e.config}).UpdateOne(e) +} + +// Unwrap unwraps the Email entity that was returned from a transaction after it was closed, +// so that all future queries will be executed through the driver which created the transaction. +func (e *Email) Unwrap() *Email { + _tx, ok := e.config.driver.(*txDriver) + if !ok { + panic("ent: Email is not a transactional entity") + } + e.config.driver = _tx.drv + return e +} + +// String implements the fmt.Stringer. +func (e *Email) String() string { + var builder strings.Builder + builder.WriteString("Email(") + builder.WriteString(fmt.Sprintf("id=%v, ", e.ID)) + builder.WriteString("email=") + builder.WriteString(e.Email) + builder.WriteString(", ") + builder.WriteString("createdAt=") + builder.WriteString(e.CreatedAt.Format(time.ANSIC)) + builder.WriteString(", ") + builder.WriteString("updatedAt=") + builder.WriteString(e.UpdatedAt.Format(time.ANSIC)) + builder.WriteByte(')') + return builder.String() +} + +// Emails is a parsable slice of Email. +type Emails []*Email + +func (e Emails) config(cfg config) { + for _i := range e { + e[_i].config = cfg + } +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/email/email.go b/example/bannedDomains/email/internal/features/repository/ent/email/email.go new file mode 100644 index 0000000..3e75f63 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email/email.go @@ -0,0 +1,49 @@ +// Code generated by ent, DO NOT EDIT. + +package email + +import ( + "time" +) + +const ( + // Label holds the string label denoting the email type in the database. + Label = "email" + // FieldID holds the string denoting the id field in the database. + FieldID = "id" + // FieldEmail holds the string denoting the email field in the database. + FieldEmail = "email" + // FieldCreatedAt holds the string denoting the createdat field in the database. + FieldCreatedAt = "created_at" + // FieldUpdatedAt holds the string denoting the updatedat field in the database. + FieldUpdatedAt = "updated_at" + // Table holds the table name of the email in the database. + Table = "emails" +) + +// Columns holds all SQL columns for email fields. +var Columns = []string{ + FieldID, + FieldEmail, + FieldCreatedAt, + FieldUpdatedAt, +} + +// ValidColumn reports if the column name is valid (part of the table columns). +func ValidColumn(column string) bool { + for i := range Columns { + if column == Columns[i] { + return true + } + } + return false +} + +var ( + // DefaultCreatedAt holds the default value on creation for the "createdAt" field. + DefaultCreatedAt func() time.Time + // DefaultUpdatedAt holds the default value on creation for the "updatedAt" field. + DefaultUpdatedAt func() time.Time + // UpdateDefaultUpdatedAt holds the default value on update for the "updatedAt" field. + UpdateDefaultUpdatedAt func() time.Time +) diff --git a/example/bannedDomains/email/internal/features/repository/ent/email/where.go b/example/bannedDomains/email/internal/features/repository/ent/email/where.go new file mode 100644 index 0000000..6a4a12d --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email/where.go @@ -0,0 +1,361 @@ +// Code generated by ent, DO NOT EDIT. + +package email + +import ( + "time" + + "entgo.io/ent/dialect/sql" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/predicate" +) + +// ID filters vertices based on their ID field. +func ID(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldID), id)) + }) +} + +// IDEQ applies the EQ predicate on the ID field. +func IDEQ(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldID), id)) + }) +} + +// IDNEQ applies the NEQ predicate on the ID field. +func IDNEQ(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldID), id)) + }) +} + +// IDIn applies the In predicate on the ID field. +func IDIn(ids ...int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + v := make([]any, len(ids)) + for i := range v { + v[i] = ids[i] + } + s.Where(sql.In(s.C(FieldID), v...)) + }) +} + +// IDNotIn applies the NotIn predicate on the ID field. +func IDNotIn(ids ...int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + v := make([]any, len(ids)) + for i := range v { + v[i] = ids[i] + } + s.Where(sql.NotIn(s.C(FieldID), v...)) + }) +} + +// IDGT applies the GT predicate on the ID field. +func IDGT(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldID), id)) + }) +} + +// IDGTE applies the GTE predicate on the ID field. +func IDGTE(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldID), id)) + }) +} + +// IDLT applies the LT predicate on the ID field. +func IDLT(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldID), id)) + }) +} + +// IDLTE applies the LTE predicate on the ID field. +func IDLTE(id int) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldID), id)) + }) +} + +// Email applies equality check predicate on the "email" field. It's identical to EmailEQ. +func Email(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldEmail), v)) + }) +} + +// CreatedAt applies equality check predicate on the "createdAt" field. It's identical to CreatedAtEQ. +func CreatedAt(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldCreatedAt), v)) + }) +} + +// UpdatedAt applies equality check predicate on the "updatedAt" field. It's identical to UpdatedAtEQ. +func UpdatedAt(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldUpdatedAt), v)) + }) +} + +// EmailEQ applies the EQ predicate on the "email" field. +func EmailEQ(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldEmail), v)) + }) +} + +// EmailNEQ applies the NEQ predicate on the "email" field. +func EmailNEQ(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldEmail), v)) + }) +} + +// EmailIn applies the In predicate on the "email" field. +func EmailIn(vs ...string) predicate.Email { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.In(s.C(FieldEmail), v...)) + }) +} + +// EmailNotIn applies the NotIn predicate on the "email" field. +func EmailNotIn(vs ...string) predicate.Email { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NotIn(s.C(FieldEmail), v...)) + }) +} + +// EmailGT applies the GT predicate on the "email" field. +func EmailGT(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldEmail), v)) + }) +} + +// EmailGTE applies the GTE predicate on the "email" field. +func EmailGTE(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldEmail), v)) + }) +} + +// EmailLT applies the LT predicate on the "email" field. +func EmailLT(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldEmail), v)) + }) +} + +// EmailLTE applies the LTE predicate on the "email" field. +func EmailLTE(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldEmail), v)) + }) +} + +// EmailContains applies the Contains predicate on the "email" field. +func EmailContains(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.Contains(s.C(FieldEmail), v)) + }) +} + +// EmailHasPrefix applies the HasPrefix predicate on the "email" field. +func EmailHasPrefix(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.HasPrefix(s.C(FieldEmail), v)) + }) +} + +// EmailHasSuffix applies the HasSuffix predicate on the "email" field. +func EmailHasSuffix(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.HasSuffix(s.C(FieldEmail), v)) + }) +} + +// EmailEqualFold applies the EqualFold predicate on the "email" field. +func EmailEqualFold(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EqualFold(s.C(FieldEmail), v)) + }) +} + +// EmailContainsFold applies the ContainsFold predicate on the "email" field. +func EmailContainsFold(v string) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.ContainsFold(s.C(FieldEmail), v)) + }) +} + +// CreatedAtEQ applies the EQ predicate on the "createdAt" field. +func CreatedAtEQ(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldCreatedAt), v)) + }) +} + +// CreatedAtNEQ applies the NEQ predicate on the "createdAt" field. +func CreatedAtNEQ(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldCreatedAt), v)) + }) +} + +// CreatedAtIn applies the In predicate on the "createdAt" field. +func CreatedAtIn(vs ...time.Time) predicate.Email { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.In(s.C(FieldCreatedAt), v...)) + }) +} + +// CreatedAtNotIn applies the NotIn predicate on the "createdAt" field. +func CreatedAtNotIn(vs ...time.Time) predicate.Email { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NotIn(s.C(FieldCreatedAt), v...)) + }) +} + +// CreatedAtGT applies the GT predicate on the "createdAt" field. +func CreatedAtGT(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldCreatedAt), v)) + }) +} + +// CreatedAtGTE applies the GTE predicate on the "createdAt" field. +func CreatedAtGTE(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldCreatedAt), v)) + }) +} + +// CreatedAtLT applies the LT predicate on the "createdAt" field. +func CreatedAtLT(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldCreatedAt), v)) + }) +} + +// CreatedAtLTE applies the LTE predicate on the "createdAt" field. +func CreatedAtLTE(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldCreatedAt), v)) + }) +} + +// UpdatedAtEQ applies the EQ predicate on the "updatedAt" field. +func UpdatedAtEQ(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.EQ(s.C(FieldUpdatedAt), v)) + }) +} + +// UpdatedAtNEQ applies the NEQ predicate on the "updatedAt" field. +func UpdatedAtNEQ(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NEQ(s.C(FieldUpdatedAt), v)) + }) +} + +// UpdatedAtIn applies the In predicate on the "updatedAt" field. +func UpdatedAtIn(vs ...time.Time) predicate.Email { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.In(s.C(FieldUpdatedAt), v...)) + }) +} + +// UpdatedAtNotIn applies the NotIn predicate on the "updatedAt" field. +func UpdatedAtNotIn(vs ...time.Time) predicate.Email { + v := make([]any, len(vs)) + for i := range v { + v[i] = vs[i] + } + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.NotIn(s.C(FieldUpdatedAt), v...)) + }) +} + +// UpdatedAtGT applies the GT predicate on the "updatedAt" field. +func UpdatedAtGT(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GT(s.C(FieldUpdatedAt), v)) + }) +} + +// UpdatedAtGTE applies the GTE predicate on the "updatedAt" field. +func UpdatedAtGTE(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.GTE(s.C(FieldUpdatedAt), v)) + }) +} + +// UpdatedAtLT applies the LT predicate on the "updatedAt" field. +func UpdatedAtLT(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LT(s.C(FieldUpdatedAt), v)) + }) +} + +// UpdatedAtLTE applies the LTE predicate on the "updatedAt" field. +func UpdatedAtLTE(v time.Time) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s.Where(sql.LTE(s.C(FieldUpdatedAt), v)) + }) +} + +// And groups predicates with the AND operator between them. +func And(predicates ...predicate.Email) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s1 := s.Clone().SetP(nil) + for _, p := range predicates { + p(s1) + } + s.Where(s1.P()) + }) +} + +// Or groups predicates with the OR operator between them. +func Or(predicates ...predicate.Email) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + s1 := s.Clone().SetP(nil) + for i, p := range predicates { + if i > 0 { + s1.Or() + } + p(s1) + } + s.Where(s1.P()) + }) +} + +// Not applies the not operator on the given predicate. +func Not(p predicate.Email) predicate.Email { + return predicate.Email(func(s *sql.Selector) { + p(s.Not()) + }) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/email_create.go b/example/bannedDomains/email/internal/features/repository/ent/email_create.go new file mode 100644 index 0000000..5b03386 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email_create.go @@ -0,0 +1,279 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" +) + +// EmailCreate is the builder for creating a Email entity. +type EmailCreate struct { + config + mutation *EmailMutation + hooks []Hook +} + +// SetEmail sets the "email" field. +func (ec *EmailCreate) SetEmail(s string) *EmailCreate { + ec.mutation.SetEmail(s) + return ec +} + +// SetCreatedAt sets the "createdAt" field. +func (ec *EmailCreate) SetCreatedAt(t time.Time) *EmailCreate { + ec.mutation.SetCreatedAt(t) + return ec +} + +// SetNillableCreatedAt sets the "createdAt" field if the given value is not nil. +func (ec *EmailCreate) SetNillableCreatedAt(t *time.Time) *EmailCreate { + if t != nil { + ec.SetCreatedAt(*t) + } + return ec +} + +// SetUpdatedAt sets the "updatedAt" field. +func (ec *EmailCreate) SetUpdatedAt(t time.Time) *EmailCreate { + ec.mutation.SetUpdatedAt(t) + return ec +} + +// SetNillableUpdatedAt sets the "updatedAt" field if the given value is not nil. +func (ec *EmailCreate) SetNillableUpdatedAt(t *time.Time) *EmailCreate { + if t != nil { + ec.SetUpdatedAt(*t) + } + return ec +} + +// Mutation returns the EmailMutation object of the builder. +func (ec *EmailCreate) Mutation() *EmailMutation { + return ec.mutation +} + +// Save creates the Email in the database. +func (ec *EmailCreate) Save(ctx context.Context) (*Email, error) { + var ( + err error + node *Email + ) + ec.defaults() + if len(ec.hooks) == 0 { + if err = ec.check(); err != nil { + return nil, err + } + node, err = ec.sqlSave(ctx) + } else { + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*EmailMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err = ec.check(); err != nil { + return nil, err + } + ec.mutation = mutation + if node, err = ec.sqlSave(ctx); err != nil { + return nil, err + } + mutation.id = &node.ID + mutation.done = true + return node, err + }) + for i := len(ec.hooks) - 1; i >= 0; i-- { + if ec.hooks[i] == nil { + return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") + } + mut = ec.hooks[i](mut) + } + v, err := mut.Mutate(ctx, ec.mutation) + if err != nil { + return nil, err + } + nv, ok := v.(*Email) + if !ok { + return nil, fmt.Errorf("unexpected node type %T returned from EmailMutation", v) + } + node = nv + } + return node, err +} + +// SaveX calls Save and panics if Save returns an error. +func (ec *EmailCreate) SaveX(ctx context.Context) *Email { + v, err := ec.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (ec *EmailCreate) Exec(ctx context.Context) error { + _, err := ec.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (ec *EmailCreate) ExecX(ctx context.Context) { + if err := ec.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (ec *EmailCreate) defaults() { + if _, ok := ec.mutation.CreatedAt(); !ok { + v := email.DefaultCreatedAt() + ec.mutation.SetCreatedAt(v) + } + if _, ok := ec.mutation.UpdatedAt(); !ok { + v := email.DefaultUpdatedAt() + ec.mutation.SetUpdatedAt(v) + } +} + +// check runs all checks and user-defined validators on the builder. +func (ec *EmailCreate) check() error { + if _, ok := ec.mutation.Email(); !ok { + return &ValidationError{Name: "email", err: errors.New(`ent: missing required field "Email.email"`)} + } + if _, ok := ec.mutation.CreatedAt(); !ok { + return &ValidationError{Name: "createdAt", err: errors.New(`ent: missing required field "Email.createdAt"`)} + } + if _, ok := ec.mutation.UpdatedAt(); !ok { + return &ValidationError{Name: "updatedAt", err: errors.New(`ent: missing required field "Email.updatedAt"`)} + } + return nil +} + +func (ec *EmailCreate) sqlSave(ctx context.Context) (*Email, error) { + _node, _spec := ec.createSpec() + if err := sqlgraph.CreateNode(ctx, ec.driver, _spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + id := _spec.ID.Value.(int64) + _node.ID = int(id) + return _node, nil +} + +func (ec *EmailCreate) createSpec() (*Email, *sqlgraph.CreateSpec) { + var ( + _node = &Email{config: ec.config} + _spec = &sqlgraph.CreateSpec{ + Table: email.Table, + ID: &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Column: email.FieldID, + }, + } + ) + if value, ok := ec.mutation.Email(); ok { + _spec.SetField(email.FieldEmail, field.TypeString, value) + _node.Email = value + } + if value, ok := ec.mutation.CreatedAt(); ok { + _spec.SetField(email.FieldCreatedAt, field.TypeTime, value) + _node.CreatedAt = value + } + if value, ok := ec.mutation.UpdatedAt(); ok { + _spec.SetField(email.FieldUpdatedAt, field.TypeTime, value) + _node.UpdatedAt = value + } + return _node, _spec +} + +// EmailCreateBulk is the builder for creating many Email entities in bulk. +type EmailCreateBulk struct { + config + builders []*EmailCreate +} + +// Save creates the Email entities in the database. +func (ecb *EmailCreateBulk) Save(ctx context.Context) ([]*Email, error) { + specs := make([]*sqlgraph.CreateSpec, len(ecb.builders)) + nodes := make([]*Email, len(ecb.builders)) + mutators := make([]Mutator, len(ecb.builders)) + for i := range ecb.builders { + func(i int, root context.Context) { + builder := ecb.builders[i] + builder.defaults() + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*EmailMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + if err := builder.check(); err != nil { + return nil, err + } + builder.mutation = mutation + nodes[i], specs[i] = builder.createSpec() + var err error + if i < len(mutators)-1 { + _, err = mutators[i+1].Mutate(root, ecb.builders[i+1].mutation) + } else { + spec := &sqlgraph.BatchCreateSpec{Nodes: specs} + // Invoke the actual operation on the latest mutation in the chain. + if err = sqlgraph.BatchCreate(ctx, ecb.driver, spec); err != nil { + if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + } + } + if err != nil { + return nil, err + } + mutation.id = &nodes[i].ID + if specs[i].ID.Value != nil { + id := specs[i].ID.Value.(int64) + nodes[i].ID = int(id) + } + mutation.done = true + return nodes[i], nil + }) + for i := len(builder.hooks) - 1; i >= 0; i-- { + mut = builder.hooks[i](mut) + } + mutators[i] = mut + }(i, ctx) + } + if len(mutators) > 0 { + if _, err := mutators[0].Mutate(ctx, ecb.builders[0].mutation); err != nil { + return nil, err + } + } + return nodes, nil +} + +// SaveX is like Save, but panics if an error occurs. +func (ecb *EmailCreateBulk) SaveX(ctx context.Context) []*Email { + v, err := ecb.Save(ctx) + if err != nil { + panic(err) + } + return v +} + +// Exec executes the query. +func (ecb *EmailCreateBulk) Exec(ctx context.Context) error { + _, err := ecb.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (ecb *EmailCreateBulk) ExecX(ctx context.Context) { + if err := ecb.Exec(ctx); err != nil { + panic(err) + } +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/email_delete.go b/example/bannedDomains/email/internal/features/repository/ent/email_delete.go new file mode 100644 index 0000000..76dcfdd --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email_delete.go @@ -0,0 +1,115 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/predicate" +) + +// EmailDelete is the builder for deleting a Email entity. +type EmailDelete struct { + config + hooks []Hook + mutation *EmailMutation +} + +// Where appends a list predicates to the EmailDelete builder. +func (ed *EmailDelete) Where(ps ...predicate.Email) *EmailDelete { + ed.mutation.Where(ps...) + return ed +} + +// Exec executes the deletion query and returns how many vertices were deleted. +func (ed *EmailDelete) Exec(ctx context.Context) (int, error) { + var ( + err error + affected int + ) + if len(ed.hooks) == 0 { + affected, err = ed.sqlExec(ctx) + } else { + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*EmailMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + ed.mutation = mutation + affected, err = ed.sqlExec(ctx) + mutation.done = true + return affected, err + }) + for i := len(ed.hooks) - 1; i >= 0; i-- { + if ed.hooks[i] == nil { + return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") + } + mut = ed.hooks[i](mut) + } + if _, err := mut.Mutate(ctx, ed.mutation); err != nil { + return 0, err + } + } + return affected, err +} + +// ExecX is like Exec, but panics if an error occurs. +func (ed *EmailDelete) ExecX(ctx context.Context) int { + n, err := ed.Exec(ctx) + if err != nil { + panic(err) + } + return n +} + +func (ed *EmailDelete) sqlExec(ctx context.Context) (int, error) { + _spec := &sqlgraph.DeleteSpec{ + Node: &sqlgraph.NodeSpec{ + Table: email.Table, + ID: &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Column: email.FieldID, + }, + }, + } + if ps := ed.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + affected, err := sqlgraph.DeleteNodes(ctx, ed.driver, _spec) + if err != nil && sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return affected, err +} + +// EmailDeleteOne is the builder for deleting a single Email entity. +type EmailDeleteOne struct { + ed *EmailDelete +} + +// Exec executes the deletion query. +func (edo *EmailDeleteOne) Exec(ctx context.Context) error { + n, err := edo.ed.Exec(ctx) + switch { + case err != nil: + return err + case n == 0: + return &NotFoundError{email.Label} + default: + return nil + } +} + +// ExecX is like Exec, but panics if an error occurs. +func (edo *EmailDeleteOne) ExecX(ctx context.Context) { + edo.ed.ExecX(ctx) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/email_query.go b/example/bannedDomains/email/internal/features/repository/ent/email_query.go new file mode 100644 index 0000000..e6bdf9d --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email_query.go @@ -0,0 +1,548 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "fmt" + "math" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/predicate" +) + +// EmailQuery is the builder for querying Email entities. +type EmailQuery struct { + config + limit *int + offset *int + unique *bool + order []OrderFunc + fields []string + predicates []predicate.Email + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Where adds a new predicate for the EmailQuery builder. +func (eq *EmailQuery) Where(ps ...predicate.Email) *EmailQuery { + eq.predicates = append(eq.predicates, ps...) + return eq +} + +// Limit adds a limit step to the query. +func (eq *EmailQuery) Limit(limit int) *EmailQuery { + eq.limit = &limit + return eq +} + +// Offset adds an offset step to the query. +func (eq *EmailQuery) Offset(offset int) *EmailQuery { + eq.offset = &offset + return eq +} + +// Unique configures the query builder to filter duplicate records on query. +// By default, unique is set to true, and can be disabled using this method. +func (eq *EmailQuery) Unique(unique bool) *EmailQuery { + eq.unique = &unique + return eq +} + +// Order adds an order step to the query. +func (eq *EmailQuery) Order(o ...OrderFunc) *EmailQuery { + eq.order = append(eq.order, o...) + return eq +} + +// First returns the first Email entity from the query. +// Returns a *NotFoundError when no Email was found. +func (eq *EmailQuery) First(ctx context.Context) (*Email, error) { + nodes, err := eq.Limit(1).All(ctx) + if err != nil { + return nil, err + } + if len(nodes) == 0 { + return nil, &NotFoundError{email.Label} + } + return nodes[0], nil +} + +// FirstX is like First, but panics if an error occurs. +func (eq *EmailQuery) FirstX(ctx context.Context) *Email { + node, err := eq.First(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return node +} + +// FirstID returns the first Email ID from the query. +// Returns a *NotFoundError when no Email ID was found. +func (eq *EmailQuery) FirstID(ctx context.Context) (id int, err error) { + var ids []int + if ids, err = eq.Limit(1).IDs(ctx); err != nil { + return + } + if len(ids) == 0 { + err = &NotFoundError{email.Label} + return + } + return ids[0], nil +} + +// FirstIDX is like FirstID, but panics if an error occurs. +func (eq *EmailQuery) FirstIDX(ctx context.Context) int { + id, err := eq.FirstID(ctx) + if err != nil && !IsNotFound(err) { + panic(err) + } + return id +} + +// Only returns a single Email entity found by the query, ensuring it only returns one. +// Returns a *NotSingularError when more than one Email entity is found. +// Returns a *NotFoundError when no Email entities are found. +func (eq *EmailQuery) Only(ctx context.Context) (*Email, error) { + nodes, err := eq.Limit(2).All(ctx) + if err != nil { + return nil, err + } + switch len(nodes) { + case 1: + return nodes[0], nil + case 0: + return nil, &NotFoundError{email.Label} + default: + return nil, &NotSingularError{email.Label} + } +} + +// OnlyX is like Only, but panics if an error occurs. +func (eq *EmailQuery) OnlyX(ctx context.Context) *Email { + node, err := eq.Only(ctx) + if err != nil { + panic(err) + } + return node +} + +// OnlyID is like Only, but returns the only Email ID in the query. +// Returns a *NotSingularError when more than one Email ID is found. +// Returns a *NotFoundError when no entities are found. +func (eq *EmailQuery) OnlyID(ctx context.Context) (id int, err error) { + var ids []int + if ids, err = eq.Limit(2).IDs(ctx); err != nil { + return + } + switch len(ids) { + case 1: + id = ids[0] + case 0: + err = &NotFoundError{email.Label} + default: + err = &NotSingularError{email.Label} + } + return +} + +// OnlyIDX is like OnlyID, but panics if an error occurs. +func (eq *EmailQuery) OnlyIDX(ctx context.Context) int { + id, err := eq.OnlyID(ctx) + if err != nil { + panic(err) + } + return id +} + +// All executes the query and returns a list of Emails. +func (eq *EmailQuery) All(ctx context.Context) ([]*Email, error) { + if err := eq.prepareQuery(ctx); err != nil { + return nil, err + } + return eq.sqlAll(ctx) +} + +// AllX is like All, but panics if an error occurs. +func (eq *EmailQuery) AllX(ctx context.Context) []*Email { + nodes, err := eq.All(ctx) + if err != nil { + panic(err) + } + return nodes +} + +// IDs executes the query and returns a list of Email IDs. +func (eq *EmailQuery) IDs(ctx context.Context) ([]int, error) { + var ids []int + if err := eq.Select(email.FieldID).Scan(ctx, &ids); err != nil { + return nil, err + } + return ids, nil +} + +// IDsX is like IDs, but panics if an error occurs. +func (eq *EmailQuery) IDsX(ctx context.Context) []int { + ids, err := eq.IDs(ctx) + if err != nil { + panic(err) + } + return ids +} + +// Count returns the count of the given query. +func (eq *EmailQuery) Count(ctx context.Context) (int, error) { + if err := eq.prepareQuery(ctx); err != nil { + return 0, err + } + return eq.sqlCount(ctx) +} + +// CountX is like Count, but panics if an error occurs. +func (eq *EmailQuery) CountX(ctx context.Context) int { + count, err := eq.Count(ctx) + if err != nil { + panic(err) + } + return count +} + +// Exist returns true if the query has elements in the graph. +func (eq *EmailQuery) Exist(ctx context.Context) (bool, error) { + if err := eq.prepareQuery(ctx); err != nil { + return false, err + } + return eq.sqlExist(ctx) +} + +// ExistX is like Exist, but panics if an error occurs. +func (eq *EmailQuery) ExistX(ctx context.Context) bool { + exist, err := eq.Exist(ctx) + if err != nil { + panic(err) + } + return exist +} + +// Clone returns a duplicate of the EmailQuery builder, including all associated steps. It can be +// used to prepare common query builders and use them differently after the clone is made. +func (eq *EmailQuery) Clone() *EmailQuery { + if eq == nil { + return nil + } + return &EmailQuery{ + config: eq.config, + limit: eq.limit, + offset: eq.offset, + order: append([]OrderFunc{}, eq.order...), + predicates: append([]predicate.Email{}, eq.predicates...), + // clone intermediate query. + sql: eq.sql.Clone(), + path: eq.path, + unique: eq.unique, + } +} + +// GroupBy is used to group vertices by one or more fields/columns. +// It is often used with aggregate functions, like: count, max, mean, min, sum. +// +// Example: +// +// var v []struct { +// Email string `json:"email,omitempty"` +// Count int `json:"count,omitempty"` +// } +// +// client.Email.Query(). +// GroupBy(email.FieldEmail). +// Aggregate(ent.Count()). +// Scan(ctx, &v) +func (eq *EmailQuery) GroupBy(field string, fields ...string) *EmailGroupBy { + grbuild := &EmailGroupBy{config: eq.config} + grbuild.fields = append([]string{field}, fields...) + grbuild.path = func(ctx context.Context) (prev *sql.Selector, err error) { + if err := eq.prepareQuery(ctx); err != nil { + return nil, err + } + return eq.sqlQuery(ctx), nil + } + grbuild.label = email.Label + grbuild.flds, grbuild.scan = &grbuild.fields, grbuild.Scan + return grbuild +} + +// Select allows the selection one or more fields/columns for the given query, +// instead of selecting all fields in the entity. +// +// Example: +// +// var v []struct { +// Email string `json:"email,omitempty"` +// } +// +// client.Email.Query(). +// Select(email.FieldEmail). +// Scan(ctx, &v) +func (eq *EmailQuery) Select(fields ...string) *EmailSelect { + eq.fields = append(eq.fields, fields...) + selbuild := &EmailSelect{EmailQuery: eq} + selbuild.label = email.Label + selbuild.flds, selbuild.scan = &eq.fields, selbuild.Scan + return selbuild +} + +// Aggregate returns a EmailSelect configured with the given aggregations. +func (eq *EmailQuery) Aggregate(fns ...AggregateFunc) *EmailSelect { + return eq.Select().Aggregate(fns...) +} + +func (eq *EmailQuery) prepareQuery(ctx context.Context) error { + for _, f := range eq.fields { + if !email.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + } + if eq.path != nil { + prev, err := eq.path(ctx) + if err != nil { + return err + } + eq.sql = prev + } + return nil +} + +func (eq *EmailQuery) sqlAll(ctx context.Context, hooks ...queryHook) ([]*Email, error) { + var ( + nodes = []*Email{} + _spec = eq.querySpec() + ) + _spec.ScanValues = func(columns []string) ([]any, error) { + return (*Email).scanValues(nil, columns) + } + _spec.Assign = func(columns []string, values []any) error { + node := &Email{config: eq.config} + nodes = append(nodes, node) + return node.assignValues(columns, values) + } + for i := range hooks { + hooks[i](ctx, _spec) + } + if err := sqlgraph.QueryNodes(ctx, eq.driver, _spec); err != nil { + return nil, err + } + if len(nodes) == 0 { + return nodes, nil + } + return nodes, nil +} + +func (eq *EmailQuery) sqlCount(ctx context.Context) (int, error) { + _spec := eq.querySpec() + _spec.Node.Columns = eq.fields + if len(eq.fields) > 0 { + _spec.Unique = eq.unique != nil && *eq.unique + } + return sqlgraph.CountNodes(ctx, eq.driver, _spec) +} + +func (eq *EmailQuery) sqlExist(ctx context.Context) (bool, error) { + switch _, err := eq.FirstID(ctx); { + case IsNotFound(err): + return false, nil + case err != nil: + return false, fmt.Errorf("ent: check existence: %w", err) + default: + return true, nil + } +} + +func (eq *EmailQuery) querySpec() *sqlgraph.QuerySpec { + _spec := &sqlgraph.QuerySpec{ + Node: &sqlgraph.NodeSpec{ + Table: email.Table, + Columns: email.Columns, + ID: &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Column: email.FieldID, + }, + }, + From: eq.sql, + Unique: true, + } + if unique := eq.unique; unique != nil { + _spec.Unique = *unique + } + if fields := eq.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, email.FieldID) + for i := range fields { + if fields[i] != email.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, fields[i]) + } + } + } + if ps := eq.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if limit := eq.limit; limit != nil { + _spec.Limit = *limit + } + if offset := eq.offset; offset != nil { + _spec.Offset = *offset + } + if ps := eq.order; len(ps) > 0 { + _spec.Order = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + return _spec +} + +func (eq *EmailQuery) sqlQuery(ctx context.Context) *sql.Selector { + builder := sql.Dialect(eq.driver.Dialect()) + t1 := builder.Table(email.Table) + columns := eq.fields + if len(columns) == 0 { + columns = email.Columns + } + selector := builder.Select(t1.Columns(columns...)...).From(t1) + if eq.sql != nil { + selector = eq.sql + selector.Select(selector.Columns(columns...)...) + } + if eq.unique != nil && *eq.unique { + selector.Distinct() + } + for _, p := range eq.predicates { + p(selector) + } + for _, p := range eq.order { + p(selector) + } + if offset := eq.offset; offset != nil { + // limit is mandatory for offset clause. We start + // with default value, and override it below if needed. + selector.Offset(*offset).Limit(math.MaxInt32) + } + if limit := eq.limit; limit != nil { + selector.Limit(*limit) + } + return selector +} + +// EmailGroupBy is the group-by builder for Email entities. +type EmailGroupBy struct { + config + selector + fields []string + fns []AggregateFunc + // intermediate query (i.e. traversal path). + sql *sql.Selector + path func(context.Context) (*sql.Selector, error) +} + +// Aggregate adds the given aggregation functions to the group-by query. +func (egb *EmailGroupBy) Aggregate(fns ...AggregateFunc) *EmailGroupBy { + egb.fns = append(egb.fns, fns...) + return egb +} + +// Scan applies the group-by query and scans the result into the given value. +func (egb *EmailGroupBy) Scan(ctx context.Context, v any) error { + query, err := egb.path(ctx) + if err != nil { + return err + } + egb.sql = query + return egb.sqlScan(ctx, v) +} + +func (egb *EmailGroupBy) sqlScan(ctx context.Context, v any) error { + for _, f := range egb.fields { + if !email.ValidColumn(f) { + return &ValidationError{Name: f, err: fmt.Errorf("invalid field %q for group-by", f)} + } + } + selector := egb.sqlQuery() + if err := selector.Err(); err != nil { + return err + } + rows := &sql.Rows{} + query, args := selector.Query() + if err := egb.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} + +func (egb *EmailGroupBy) sqlQuery() *sql.Selector { + selector := egb.sql.Select() + aggregation := make([]string, 0, len(egb.fns)) + for _, fn := range egb.fns { + aggregation = append(aggregation, fn(selector)) + } + if len(selector.SelectedColumns()) == 0 { + columns := make([]string, 0, len(egb.fields)+len(egb.fns)) + for _, f := range egb.fields { + columns = append(columns, selector.C(f)) + } + columns = append(columns, aggregation...) + selector.Select(columns...) + } + return selector.GroupBy(selector.Columns(egb.fields...)...) +} + +// EmailSelect is the builder for selecting fields of Email entities. +type EmailSelect struct { + *EmailQuery + selector + // intermediate query (i.e. traversal path). + sql *sql.Selector +} + +// Aggregate adds the given aggregation functions to the selector query. +func (es *EmailSelect) Aggregate(fns ...AggregateFunc) *EmailSelect { + es.fns = append(es.fns, fns...) + return es +} + +// Scan applies the selector query and scans the result into the given value. +func (es *EmailSelect) Scan(ctx context.Context, v any) error { + if err := es.prepareQuery(ctx); err != nil { + return err + } + es.sql = es.EmailQuery.sqlQuery(ctx) + return es.sqlScan(ctx, v) +} + +func (es *EmailSelect) sqlScan(ctx context.Context, v any) error { + aggregation := make([]string, 0, len(es.fns)) + for _, fn := range es.fns { + aggregation = append(aggregation, fn(es.sql)) + } + switch n := len(*es.selector.flds); { + case n == 0 && len(aggregation) > 0: + es.sql.Select(aggregation...) + case n != 0 && len(aggregation) > 0: + es.sql.AppendSelect(aggregation...) + } + rows := &sql.Rows{} + query, args := es.sql.Query() + if err := es.driver.Query(ctx, query, args, rows); err != nil { + return err + } + defer rows.Close() + return sql.ScanSlice(rows, v) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/email_update.go b/example/bannedDomains/email/internal/features/repository/ent/email_update.go new file mode 100644 index 0000000..7c53635 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/email_update.go @@ -0,0 +1,334 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "time" + + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "entgo.io/ent/schema/field" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/predicate" +) + +// EmailUpdate is the builder for updating Email entities. +type EmailUpdate struct { + config + hooks []Hook + mutation *EmailMutation +} + +// Where appends a list predicates to the EmailUpdate builder. +func (eu *EmailUpdate) Where(ps ...predicate.Email) *EmailUpdate { + eu.mutation.Where(ps...) + return eu +} + +// SetEmail sets the "email" field. +func (eu *EmailUpdate) SetEmail(s string) *EmailUpdate { + eu.mutation.SetEmail(s) + return eu +} + +// SetCreatedAt sets the "createdAt" field. +func (eu *EmailUpdate) SetCreatedAt(t time.Time) *EmailUpdate { + eu.mutation.SetCreatedAt(t) + return eu +} + +// SetNillableCreatedAt sets the "createdAt" field if the given value is not nil. +func (eu *EmailUpdate) SetNillableCreatedAt(t *time.Time) *EmailUpdate { + if t != nil { + eu.SetCreatedAt(*t) + } + return eu +} + +// SetUpdatedAt sets the "updatedAt" field. +func (eu *EmailUpdate) SetUpdatedAt(t time.Time) *EmailUpdate { + eu.mutation.SetUpdatedAt(t) + return eu +} + +// Mutation returns the EmailMutation object of the builder. +func (eu *EmailUpdate) Mutation() *EmailMutation { + return eu.mutation +} + +// Save executes the query and returns the number of nodes affected by the update operation. +func (eu *EmailUpdate) Save(ctx context.Context) (int, error) { + var ( + err error + affected int + ) + eu.defaults() + if len(eu.hooks) == 0 { + affected, err = eu.sqlSave(ctx) + } else { + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*EmailMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + eu.mutation = mutation + affected, err = eu.sqlSave(ctx) + mutation.done = true + return affected, err + }) + for i := len(eu.hooks) - 1; i >= 0; i-- { + if eu.hooks[i] == nil { + return 0, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") + } + mut = eu.hooks[i](mut) + } + if _, err := mut.Mutate(ctx, eu.mutation); err != nil { + return 0, err + } + } + return affected, err +} + +// SaveX is like Save, but panics if an error occurs. +func (eu *EmailUpdate) SaveX(ctx context.Context) int { + affected, err := eu.Save(ctx) + if err != nil { + panic(err) + } + return affected +} + +// Exec executes the query. +func (eu *EmailUpdate) Exec(ctx context.Context) error { + _, err := eu.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (eu *EmailUpdate) ExecX(ctx context.Context) { + if err := eu.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (eu *EmailUpdate) defaults() { + if _, ok := eu.mutation.UpdatedAt(); !ok { + v := email.UpdateDefaultUpdatedAt() + eu.mutation.SetUpdatedAt(v) + } +} + +func (eu *EmailUpdate) sqlSave(ctx context.Context) (n int, err error) { + _spec := &sqlgraph.UpdateSpec{ + Node: &sqlgraph.NodeSpec{ + Table: email.Table, + Columns: email.Columns, + ID: &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Column: email.FieldID, + }, + }, + } + if ps := eu.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := eu.mutation.Email(); ok { + _spec.SetField(email.FieldEmail, field.TypeString, value) + } + if value, ok := eu.mutation.CreatedAt(); ok { + _spec.SetField(email.FieldCreatedAt, field.TypeTime, value) + } + if value, ok := eu.mutation.UpdatedAt(); ok { + _spec.SetField(email.FieldUpdatedAt, field.TypeTime, value) + } + if n, err = sqlgraph.UpdateNodes(ctx, eu.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{email.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return 0, err + } + return n, nil +} + +// EmailUpdateOne is the builder for updating a single Email entity. +type EmailUpdateOne struct { + config + fields []string + hooks []Hook + mutation *EmailMutation +} + +// SetEmail sets the "email" field. +func (euo *EmailUpdateOne) SetEmail(s string) *EmailUpdateOne { + euo.mutation.SetEmail(s) + return euo +} + +// SetCreatedAt sets the "createdAt" field. +func (euo *EmailUpdateOne) SetCreatedAt(t time.Time) *EmailUpdateOne { + euo.mutation.SetCreatedAt(t) + return euo +} + +// SetNillableCreatedAt sets the "createdAt" field if the given value is not nil. +func (euo *EmailUpdateOne) SetNillableCreatedAt(t *time.Time) *EmailUpdateOne { + if t != nil { + euo.SetCreatedAt(*t) + } + return euo +} + +// SetUpdatedAt sets the "updatedAt" field. +func (euo *EmailUpdateOne) SetUpdatedAt(t time.Time) *EmailUpdateOne { + euo.mutation.SetUpdatedAt(t) + return euo +} + +// Mutation returns the EmailMutation object of the builder. +func (euo *EmailUpdateOne) Mutation() *EmailMutation { + return euo.mutation +} + +// Select allows selecting one or more fields (columns) of the returned entity. +// The default is selecting all fields defined in the entity schema. +func (euo *EmailUpdateOne) Select(field string, fields ...string) *EmailUpdateOne { + euo.fields = append([]string{field}, fields...) + return euo +} + +// Save executes the query and returns the updated Email entity. +func (euo *EmailUpdateOne) Save(ctx context.Context) (*Email, error) { + var ( + err error + node *Email + ) + euo.defaults() + if len(euo.hooks) == 0 { + node, err = euo.sqlSave(ctx) + } else { + var mut Mutator = MutateFunc(func(ctx context.Context, m Mutation) (Value, error) { + mutation, ok := m.(*EmailMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T", m) + } + euo.mutation = mutation + node, err = euo.sqlSave(ctx) + mutation.done = true + return node, err + }) + for i := len(euo.hooks) - 1; i >= 0; i-- { + if euo.hooks[i] == nil { + return nil, fmt.Errorf("ent: uninitialized hook (forgotten import ent/runtime?)") + } + mut = euo.hooks[i](mut) + } + v, err := mut.Mutate(ctx, euo.mutation) + if err != nil { + return nil, err + } + nv, ok := v.(*Email) + if !ok { + return nil, fmt.Errorf("unexpected node type %T returned from EmailMutation", v) + } + node = nv + } + return node, err +} + +// SaveX is like Save, but panics if an error occurs. +func (euo *EmailUpdateOne) SaveX(ctx context.Context) *Email { + node, err := euo.Save(ctx) + if err != nil { + panic(err) + } + return node +} + +// Exec executes the query on the entity. +func (euo *EmailUpdateOne) Exec(ctx context.Context) error { + _, err := euo.Save(ctx) + return err +} + +// ExecX is like Exec, but panics if an error occurs. +func (euo *EmailUpdateOne) ExecX(ctx context.Context) { + if err := euo.Exec(ctx); err != nil { + panic(err) + } +} + +// defaults sets the default values of the builder before save. +func (euo *EmailUpdateOne) defaults() { + if _, ok := euo.mutation.UpdatedAt(); !ok { + v := email.UpdateDefaultUpdatedAt() + euo.mutation.SetUpdatedAt(v) + } +} + +func (euo *EmailUpdateOne) sqlSave(ctx context.Context) (_node *Email, err error) { + _spec := &sqlgraph.UpdateSpec{ + Node: &sqlgraph.NodeSpec{ + Table: email.Table, + Columns: email.Columns, + ID: &sqlgraph.FieldSpec{ + Type: field.TypeInt, + Column: email.FieldID, + }, + }, + } + id, ok := euo.mutation.ID() + if !ok { + return nil, &ValidationError{Name: "id", err: errors.New(`ent: missing "Email.id" for update`)} + } + _spec.Node.ID.Value = id + if fields := euo.fields; len(fields) > 0 { + _spec.Node.Columns = make([]string, 0, len(fields)) + _spec.Node.Columns = append(_spec.Node.Columns, email.FieldID) + for _, f := range fields { + if !email.ValidColumn(f) { + return nil, &ValidationError{Name: f, err: fmt.Errorf("ent: invalid field %q for query", f)} + } + if f != email.FieldID { + _spec.Node.Columns = append(_spec.Node.Columns, f) + } + } + } + if ps := euo.mutation.predicates; len(ps) > 0 { + _spec.Predicate = func(selector *sql.Selector) { + for i := range ps { + ps[i](selector) + } + } + } + if value, ok := euo.mutation.Email(); ok { + _spec.SetField(email.FieldEmail, field.TypeString, value) + } + if value, ok := euo.mutation.CreatedAt(); ok { + _spec.SetField(email.FieldCreatedAt, field.TypeTime, value) + } + if value, ok := euo.mutation.UpdatedAt(); ok { + _spec.SetField(email.FieldUpdatedAt, field.TypeTime, value) + } + _node = &Email{config: euo.config} + _spec.Assign = _node.assignValues + _spec.ScanValues = _node.scanValues + if err = sqlgraph.UpdateNode(ctx, euo.driver, _spec); err != nil { + if _, ok := err.(*sqlgraph.NotFoundError); ok { + err = &NotFoundError{email.Label} + } else if sqlgraph.IsConstraintError(err) { + err = &ConstraintError{msg: err.Error(), wrap: err} + } + return nil, err + } + return _node, nil +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/ent.go b/example/bannedDomains/email/internal/features/repository/ent/ent.go new file mode 100644 index 0000000..3419b46 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/ent.go @@ -0,0 +1,466 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + + "entgo.io/ent" + "entgo.io/ent/dialect/sql" + "entgo.io/ent/dialect/sql/sqlgraph" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" +) + +// ent aliases to avoid import conflicts in user's code. +type ( + Op = ent.Op + Hook = ent.Hook + Value = ent.Value + Query = ent.Query + Policy = ent.Policy + Mutator = ent.Mutator + Mutation = ent.Mutation + MutateFunc = ent.MutateFunc +) + +// OrderFunc applies an ordering on the sql selector. +type OrderFunc func(*sql.Selector) + +// columnChecker returns a function indicates if the column exists in the given column. +func columnChecker(table string) func(string) error { + checks := map[string]func(string) bool{ + email.Table: email.ValidColumn, + } + check, ok := checks[table] + if !ok { + return func(string) error { + return fmt.Errorf("unknown table %q", table) + } + } + return func(column string) error { + if !check(column) { + return fmt.Errorf("unknown column %q for table %q", column, table) + } + return nil + } +} + +// Asc applies the given fields in ASC order. +func Asc(fields ...string) OrderFunc { + return func(s *sql.Selector) { + check := columnChecker(s.TableName()) + for _, f := range fields { + if err := check(f); err != nil { + s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) + } + s.OrderBy(sql.Asc(s.C(f))) + } + } +} + +// Desc applies the given fields in DESC order. +func Desc(fields ...string) OrderFunc { + return func(s *sql.Selector) { + check := columnChecker(s.TableName()) + for _, f := range fields { + if err := check(f); err != nil { + s.AddError(&ValidationError{Name: f, err: fmt.Errorf("ent: %w", err)}) + } + s.OrderBy(sql.Desc(s.C(f))) + } + } +} + +// AggregateFunc applies an aggregation step on the group-by traversal/selector. +type AggregateFunc func(*sql.Selector) string + +// As is a pseudo aggregation function for renaming another other functions with custom names. For example: +// +// GroupBy(field1, field2). +// Aggregate(ent.As(ent.Sum(field1), "sum_field1"), (ent.As(ent.Sum(field2), "sum_field2")). +// Scan(ctx, &v) +func As(fn AggregateFunc, end string) AggregateFunc { + return func(s *sql.Selector) string { + return sql.As(fn(s), end) + } +} + +// Count applies the "count" aggregation function on each group. +func Count() AggregateFunc { + return func(s *sql.Selector) string { + return sql.Count("*") + } +} + +// Max applies the "max" aggregation function on the given field of each group. +func Max(field string) AggregateFunc { + return func(s *sql.Selector) string { + check := columnChecker(s.TableName()) + if err := check(field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Max(s.C(field)) + } +} + +// Mean applies the "mean" aggregation function on the given field of each group. +func Mean(field string) AggregateFunc { + return func(s *sql.Selector) string { + check := columnChecker(s.TableName()) + if err := check(field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Avg(s.C(field)) + } +} + +// Min applies the "min" aggregation function on the given field of each group. +func Min(field string) AggregateFunc { + return func(s *sql.Selector) string { + check := columnChecker(s.TableName()) + if err := check(field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Min(s.C(field)) + } +} + +// Sum applies the "sum" aggregation function on the given field of each group. +func Sum(field string) AggregateFunc { + return func(s *sql.Selector) string { + check := columnChecker(s.TableName()) + if err := check(field); err != nil { + s.AddError(&ValidationError{Name: field, err: fmt.Errorf("ent: %w", err)}) + return "" + } + return sql.Sum(s.C(field)) + } +} + +// ValidationError returns when validating a field or edge fails. +type ValidationError struct { + Name string // Field or edge name. + err error +} + +// Error implements the error interface. +func (e *ValidationError) Error() string { + return e.err.Error() +} + +// Unwrap implements the errors.Wrapper interface. +func (e *ValidationError) Unwrap() error { + return e.err +} + +// IsValidationError returns a boolean indicating whether the error is a validation error. +func IsValidationError(err error) bool { + if err == nil { + return false + } + var e *ValidationError + return errors.As(err, &e) +} + +// NotFoundError returns when trying to fetch a specific entity and it was not found in the database. +type NotFoundError struct { + label string +} + +// Error implements the error interface. +func (e *NotFoundError) Error() string { + return "ent: " + e.label + " not found" +} + +// IsNotFound returns a boolean indicating whether the error is a not found error. +func IsNotFound(err error) bool { + if err == nil { + return false + } + var e *NotFoundError + return errors.As(err, &e) +} + +// MaskNotFound masks not found error. +func MaskNotFound(err error) error { + if IsNotFound(err) { + return nil + } + return err +} + +// NotSingularError returns when trying to fetch a singular entity and more then one was found in the database. +type NotSingularError struct { + label string +} + +// Error implements the error interface. +func (e *NotSingularError) Error() string { + return "ent: " + e.label + " not singular" +} + +// IsNotSingular returns a boolean indicating whether the error is a not singular error. +func IsNotSingular(err error) bool { + if err == nil { + return false + } + var e *NotSingularError + return errors.As(err, &e) +} + +// NotLoadedError returns when trying to get a node that was not loaded by the query. +type NotLoadedError struct { + edge string +} + +// Error implements the error interface. +func (e *NotLoadedError) Error() string { + return "ent: " + e.edge + " edge was not loaded" +} + +// IsNotLoaded returns a boolean indicating whether the error is a not loaded error. +func IsNotLoaded(err error) bool { + if err == nil { + return false + } + var e *NotLoadedError + return errors.As(err, &e) +} + +// ConstraintError returns when trying to create/update one or more entities and +// one or more of their constraints failed. For example, violation of edge or +// field uniqueness. +type ConstraintError struct { + msg string + wrap error +} + +// Error implements the error interface. +func (e ConstraintError) Error() string { + return "ent: constraint failed: " + e.msg +} + +// Unwrap implements the errors.Wrapper interface. +func (e *ConstraintError) Unwrap() error { + return e.wrap +} + +// IsConstraintError returns a boolean indicating whether the error is a constraint failure. +func IsConstraintError(err error) bool { + if err == nil { + return false + } + var e *ConstraintError + return errors.As(err, &e) +} + +// selector embedded by the different Select/GroupBy builders. +type selector struct { + label string + flds *[]string + fns []AggregateFunc + scan func(context.Context, any) error +} + +// ScanX is like Scan, but panics if an error occurs. +func (s *selector) ScanX(ctx context.Context, v any) { + if err := s.scan(ctx, v); err != nil { + panic(err) + } +} + +// Strings returns list of strings from a selector. It is only allowed when selecting one field. +func (s *selector) Strings(ctx context.Context) ([]string, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Strings is not achievable when selecting more than 1 field") + } + var v []string + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// StringsX is like Strings, but panics if an error occurs. +func (s *selector) StringsX(ctx context.Context) []string { + v, err := s.Strings(ctx) + if err != nil { + panic(err) + } + return v +} + +// String returns a single string from a selector. It is only allowed when selecting one field. +func (s *selector) String(ctx context.Context) (_ string, err error) { + var v []string + if v, err = s.Strings(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Strings returned %d results when one was expected", len(v)) + } + return +} + +// StringX is like String, but panics if an error occurs. +func (s *selector) StringX(ctx context.Context) string { + v, err := s.String(ctx) + if err != nil { + panic(err) + } + return v +} + +// Ints returns list of ints from a selector. It is only allowed when selecting one field. +func (s *selector) Ints(ctx context.Context) ([]int, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Ints is not achievable when selecting more than 1 field") + } + var v []int + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// IntsX is like Ints, but panics if an error occurs. +func (s *selector) IntsX(ctx context.Context) []int { + v, err := s.Ints(ctx) + if err != nil { + panic(err) + } + return v +} + +// Int returns a single int from a selector. It is only allowed when selecting one field. +func (s *selector) Int(ctx context.Context) (_ int, err error) { + var v []int + if v, err = s.Ints(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Ints returned %d results when one was expected", len(v)) + } + return +} + +// IntX is like Int, but panics if an error occurs. +func (s *selector) IntX(ctx context.Context) int { + v, err := s.Int(ctx) + if err != nil { + panic(err) + } + return v +} + +// Float64s returns list of float64s from a selector. It is only allowed when selecting one field. +func (s *selector) Float64s(ctx context.Context) ([]float64, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Float64s is not achievable when selecting more than 1 field") + } + var v []float64 + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// Float64sX is like Float64s, but panics if an error occurs. +func (s *selector) Float64sX(ctx context.Context) []float64 { + v, err := s.Float64s(ctx) + if err != nil { + panic(err) + } + return v +} + +// Float64 returns a single float64 from a selector. It is only allowed when selecting one field. +func (s *selector) Float64(ctx context.Context) (_ float64, err error) { + var v []float64 + if v, err = s.Float64s(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Float64s returned %d results when one was expected", len(v)) + } + return +} + +// Float64X is like Float64, but panics if an error occurs. +func (s *selector) Float64X(ctx context.Context) float64 { + v, err := s.Float64(ctx) + if err != nil { + panic(err) + } + return v +} + +// Bools returns list of bools from a selector. It is only allowed when selecting one field. +func (s *selector) Bools(ctx context.Context) ([]bool, error) { + if len(*s.flds) > 1 { + return nil, errors.New("ent: Bools is not achievable when selecting more than 1 field") + } + var v []bool + if err := s.scan(ctx, &v); err != nil { + return nil, err + } + return v, nil +} + +// BoolsX is like Bools, but panics if an error occurs. +func (s *selector) BoolsX(ctx context.Context) []bool { + v, err := s.Bools(ctx) + if err != nil { + panic(err) + } + return v +} + +// Bool returns a single bool from a selector. It is only allowed when selecting one field. +func (s *selector) Bool(ctx context.Context) (_ bool, err error) { + var v []bool + if v, err = s.Bools(ctx); err != nil { + return + } + switch len(v) { + case 1: + return v[0], nil + case 0: + err = &NotFoundError{s.label} + default: + err = fmt.Errorf("ent: Bools returned %d results when one was expected", len(v)) + } + return +} + +// BoolX is like Bool, but panics if an error occurs. +func (s *selector) BoolX(ctx context.Context) bool { + v, err := s.Bool(ctx) + if err != nil { + panic(err) + } + return v +} + +// queryHook describes an internal hook for the different sqlAll methods. +type queryHook func(context.Context, *sqlgraph.QuerySpec) diff --git a/example/bannedDomains/email/internal/features/repository/ent/enttest/enttest.go b/example/bannedDomains/email/internal/features/repository/ent/enttest/enttest.go new file mode 100644 index 0000000..9a76777 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/enttest/enttest.go @@ -0,0 +1,84 @@ +// Code generated by ent, DO NOT EDIT. + +package enttest + +import ( + "context" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent" + // required by schema hooks. + _ "github.com/gothunder/thunder/example/email/internal/features/repository/ent/runtime" + + "entgo.io/ent/dialect/sql/schema" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/migrate" +) + +type ( + // TestingT is the interface that is shared between + // testing.T and testing.B and used by enttest. + TestingT interface { + FailNow() + Error(...any) + } + + // Option configures client creation. + Option func(*options) + + options struct { + opts []ent.Option + migrateOpts []schema.MigrateOption + } +) + +// WithOptions forwards options to client creation. +func WithOptions(opts ...ent.Option) Option { + return func(o *options) { + o.opts = append(o.opts, opts...) + } +} + +// WithMigrateOptions forwards options to auto migration. +func WithMigrateOptions(opts ...schema.MigrateOption) Option { + return func(o *options) { + o.migrateOpts = append(o.migrateOpts, opts...) + } +} + +func newOptions(opts []Option) *options { + o := &options{} + for _, opt := range opts { + opt(o) + } + return o +} + +// Open calls ent.Open and auto-run migration. +func Open(t TestingT, driverName, dataSourceName string, opts ...Option) *ent.Client { + o := newOptions(opts) + c, err := ent.Open(driverName, dataSourceName, o.opts...) + if err != nil { + t.Error(err) + t.FailNow() + } + migrateSchema(t, c, o) + return c +} + +// NewClient calls ent.NewClient and auto-run migration. +func NewClient(t TestingT, opts ...Option) *ent.Client { + o := newOptions(opts) + c := ent.NewClient(o.opts...) + migrateSchema(t, c, o) + return c +} +func migrateSchema(t TestingT, c *ent.Client, o *options) { + tables, err := schema.CopyTables(migrate.Tables) + if err != nil { + t.Error(err) + t.FailNow() + } + if err := migrate.Create(context.Background(), c.Schema, tables, o.migrateOpts...); err != nil { + t.Error(err) + t.FailNow() + } +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/generate.go b/example/bannedDomains/email/internal/features/repository/ent/generate.go new file mode 100644 index 0000000..8d3fdfd --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/generate.go @@ -0,0 +1,3 @@ +package ent + +//go:generate go run -mod=mod entgo.io/ent/cmd/ent generate ./schema diff --git a/example/bannedDomains/email/internal/features/repository/ent/hook/hook.go b/example/bannedDomains/email/internal/features/repository/ent/hook/hook.go new file mode 100644 index 0000000..5d5fa72 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/hook/hook.go @@ -0,0 +1,200 @@ +// Code generated by ent, DO NOT EDIT. + +package hook + +import ( + "context" + "fmt" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent" +) + +// The EmailFunc type is an adapter to allow the use of ordinary +// function as Email mutator. +type EmailFunc func(context.Context, *ent.EmailMutation) (ent.Value, error) + +// Mutate calls f(ctx, m). +func (f EmailFunc) Mutate(ctx context.Context, m ent.Mutation) (ent.Value, error) { + mv, ok := m.(*ent.EmailMutation) + if !ok { + return nil, fmt.Errorf("unexpected mutation type %T. expect *ent.EmailMutation", m) + } + return f(ctx, mv) +} + +// Condition is a hook condition function. +type Condition func(context.Context, ent.Mutation) bool + +// And groups conditions with the AND operator. +func And(first, second Condition, rest ...Condition) Condition { + return func(ctx context.Context, m ent.Mutation) bool { + if !first(ctx, m) || !second(ctx, m) { + return false + } + for _, cond := range rest { + if !cond(ctx, m) { + return false + } + } + return true + } +} + +// Or groups conditions with the OR operator. +func Or(first, second Condition, rest ...Condition) Condition { + return func(ctx context.Context, m ent.Mutation) bool { + if first(ctx, m) || second(ctx, m) { + return true + } + for _, cond := range rest { + if cond(ctx, m) { + return true + } + } + return false + } +} + +// Not negates a given condition. +func Not(cond Condition) Condition { + return func(ctx context.Context, m ent.Mutation) bool { + return !cond(ctx, m) + } +} + +// HasOp is a condition testing mutation operation. +func HasOp(op ent.Op) Condition { + return func(_ context.Context, m ent.Mutation) bool { + return m.Op().Is(op) + } +} + +// HasAddedFields is a condition validating `.AddedField` on fields. +func HasAddedFields(field string, fields ...string) Condition { + return func(_ context.Context, m ent.Mutation) bool { + if _, exists := m.AddedField(field); !exists { + return false + } + for _, field := range fields { + if _, exists := m.AddedField(field); !exists { + return false + } + } + return true + } +} + +// HasClearedFields is a condition validating `.FieldCleared` on fields. +func HasClearedFields(field string, fields ...string) Condition { + return func(_ context.Context, m ent.Mutation) bool { + if exists := m.FieldCleared(field); !exists { + return false + } + for _, field := range fields { + if exists := m.FieldCleared(field); !exists { + return false + } + } + return true + } +} + +// HasFields is a condition validating `.Field` on fields. +func HasFields(field string, fields ...string) Condition { + return func(_ context.Context, m ent.Mutation) bool { + if _, exists := m.Field(field); !exists { + return false + } + for _, field := range fields { + if _, exists := m.Field(field); !exists { + return false + } + } + return true + } +} + +// If executes the given hook under condition. +// +// hook.If(ComputeAverage, And(HasFields(...), HasAddedFields(...))) +func If(hk ent.Hook, cond Condition) ent.Hook { + return func(next ent.Mutator) ent.Mutator { + return ent.MutateFunc(func(ctx context.Context, m ent.Mutation) (ent.Value, error) { + if cond(ctx, m) { + return hk(next).Mutate(ctx, m) + } + return next.Mutate(ctx, m) + }) + } +} + +// On executes the given hook only for the given operation. +// +// hook.On(Log, ent.Delete|ent.Create) +func On(hk ent.Hook, op ent.Op) ent.Hook { + return If(hk, HasOp(op)) +} + +// Unless skips the given hook only for the given operation. +// +// hook.Unless(Log, ent.Update|ent.UpdateOne) +func Unless(hk ent.Hook, op ent.Op) ent.Hook { + return If(hk, Not(HasOp(op))) +} + +// FixedError is a hook returning a fixed error. +func FixedError(err error) ent.Hook { + return func(ent.Mutator) ent.Mutator { + return ent.MutateFunc(func(context.Context, ent.Mutation) (ent.Value, error) { + return nil, err + }) + } +} + +// Reject returns a hook that rejects all operations that match op. +// +// func (T) Hooks() []ent.Hook { +// return []ent.Hook{ +// Reject(ent.Delete|ent.Update), +// } +// } +func Reject(op ent.Op) ent.Hook { + hk := FixedError(fmt.Errorf("%s operation is not allowed", op)) + return On(hk, op) +} + +// Chain acts as a list of hooks and is effectively immutable. +// Once created, it will always hold the same set of hooks in the same order. +type Chain struct { + hooks []ent.Hook +} + +// NewChain creates a new chain of hooks. +func NewChain(hooks ...ent.Hook) Chain { + return Chain{append([]ent.Hook(nil), hooks...)} +} + +// Hook chains the list of hooks and returns the final hook. +func (c Chain) Hook() ent.Hook { + return func(mutator ent.Mutator) ent.Mutator { + for i := len(c.hooks) - 1; i >= 0; i-- { + mutator = c.hooks[i](mutator) + } + return mutator + } +} + +// Append extends a chain, adding the specified hook +// as the last ones in the mutation flow. +func (c Chain) Append(hooks ...ent.Hook) Chain { + newHooks := make([]ent.Hook, 0, len(c.hooks)+len(hooks)) + newHooks = append(newHooks, c.hooks...) + newHooks = append(newHooks, hooks...) + return Chain{newHooks} +} + +// Extend extends a chain, adding the specified chain +// as the last ones in the mutation flow. +func (c Chain) Extend(chain Chain) Chain { + return c.Append(chain.hooks...) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/migrate/migrate.go b/example/bannedDomains/email/internal/features/repository/ent/migrate/migrate.go new file mode 100644 index 0000000..1956a6b --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/migrate/migrate.go @@ -0,0 +1,64 @@ +// Code generated by ent, DO NOT EDIT. + +package migrate + +import ( + "context" + "fmt" + "io" + + "entgo.io/ent/dialect" + "entgo.io/ent/dialect/sql/schema" +) + +var ( + // WithGlobalUniqueID sets the universal ids options to the migration. + // If this option is enabled, ent migration will allocate a 1<<32 range + // for the ids of each entity (table). + // Note that this option cannot be applied on tables that already exist. + WithGlobalUniqueID = schema.WithGlobalUniqueID + // WithDropColumn sets the drop column option to the migration. + // If this option is enabled, ent migration will drop old columns + // that were used for both fields and edges. This defaults to false. + WithDropColumn = schema.WithDropColumn + // WithDropIndex sets the drop index option to the migration. + // If this option is enabled, ent migration will drop old indexes + // that were defined in the schema. This defaults to false. + // Note that unique constraints are defined using `UNIQUE INDEX`, + // and therefore, it's recommended to enable this option to get more + // flexibility in the schema changes. + WithDropIndex = schema.WithDropIndex + // WithForeignKeys enables creating foreign-key in schema DDL. This defaults to true. + WithForeignKeys = schema.WithForeignKeys +) + +// Schema is the API for creating, migrating and dropping a schema. +type Schema struct { + drv dialect.Driver +} + +// NewSchema creates a new schema client. +func NewSchema(drv dialect.Driver) *Schema { return &Schema{drv: drv} } + +// Create creates all schema resources. +func (s *Schema) Create(ctx context.Context, opts ...schema.MigrateOption) error { + return Create(ctx, s, Tables, opts...) +} + +// Create creates all table resources using the given schema driver. +func Create(ctx context.Context, s *Schema, tables []*schema.Table, opts ...schema.MigrateOption) error { + migrate, err := schema.NewMigrate(s.drv, opts...) + if err != nil { + return fmt.Errorf("ent/migrate: %w", err) + } + return migrate.Create(ctx, tables...) +} + +// WriteTo writes the schema changes to w instead of running them against the database. +// +// if err := client.Schema.WriteTo(context.Background(), os.Stdout); err != nil { +// log.Fatal(err) +// } +func (s *Schema) WriteTo(ctx context.Context, w io.Writer, opts ...schema.MigrateOption) error { + return Create(ctx, &Schema{drv: &schema.WriteDriver{Writer: w, Driver: s.drv}}, Tables, opts...) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/migrate/schema.go b/example/bannedDomains/email/internal/features/repository/ent/migrate/schema.go new file mode 100644 index 0000000..8d3b40a --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/migrate/schema.go @@ -0,0 +1,31 @@ +// Code generated by ent, DO NOT EDIT. + +package migrate + +import ( + "entgo.io/ent/dialect/sql/schema" + "entgo.io/ent/schema/field" +) + +var ( + // EmailsColumns holds the columns for the "emails" table. + EmailsColumns = []*schema.Column{ + {Name: "id", Type: field.TypeInt, Increment: true}, + {Name: "email", Type: field.TypeString}, + {Name: "created_at", Type: field.TypeTime}, + {Name: "updated_at", Type: field.TypeTime}, + } + // EmailsTable holds the schema information for the "emails" table. + EmailsTable = &schema.Table{ + Name: "emails", + Columns: EmailsColumns, + PrimaryKey: []*schema.Column{EmailsColumns[0]}, + } + // Tables holds all the tables in the schema. + Tables = []*schema.Table{ + EmailsTable, + } +) + +func init() { +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/mutation.go b/example/bannedDomains/email/internal/features/repository/ent/mutation.go new file mode 100644 index 0000000..7d48bf6 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/mutation.go @@ -0,0 +1,447 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "errors" + "fmt" + "sync" + "time" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/predicate" + + "entgo.io/ent" +) + +const ( + // Operation types. + OpCreate = ent.OpCreate + OpDelete = ent.OpDelete + OpDeleteOne = ent.OpDeleteOne + OpUpdate = ent.OpUpdate + OpUpdateOne = ent.OpUpdateOne + + // Node types. + TypeEmail = "Email" +) + +// EmailMutation represents an operation that mutates the Email nodes in the graph. +type EmailMutation struct { + config + op Op + typ string + id *int + email *string + createdAt *time.Time + updatedAt *time.Time + clearedFields map[string]struct{} + done bool + oldValue func(context.Context) (*Email, error) + predicates []predicate.Email +} + +var _ ent.Mutation = (*EmailMutation)(nil) + +// emailOption allows management of the mutation configuration using functional options. +type emailOption func(*EmailMutation) + +// newEmailMutation creates new mutation for the Email entity. +func newEmailMutation(c config, op Op, opts ...emailOption) *EmailMutation { + m := &EmailMutation{ + config: c, + op: op, + typ: TypeEmail, + clearedFields: make(map[string]struct{}), + } + for _, opt := range opts { + opt(m) + } + return m +} + +// withEmailID sets the ID field of the mutation. +func withEmailID(id int) emailOption { + return func(m *EmailMutation) { + var ( + err error + once sync.Once + value *Email + ) + m.oldValue = func(ctx context.Context) (*Email, error) { + once.Do(func() { + if m.done { + err = errors.New("querying old values post mutation is not allowed") + } else { + value, err = m.Client().Email.Get(ctx, id) + } + }) + return value, err + } + m.id = &id + } +} + +// withEmail sets the old Email of the mutation. +func withEmail(node *Email) emailOption { + return func(m *EmailMutation) { + m.oldValue = func(context.Context) (*Email, error) { + return node, nil + } + m.id = &node.ID + } +} + +// Client returns a new `ent.Client` from the mutation. If the mutation was +// executed in a transaction (ent.Tx), a transactional client is returned. +func (m EmailMutation) Client() *Client { + client := &Client{config: m.config} + client.init() + return client +} + +// Tx returns an `ent.Tx` for mutations that were executed in transactions; +// it returns an error otherwise. +func (m EmailMutation) Tx() (*Tx, error) { + if _, ok := m.driver.(*txDriver); !ok { + return nil, errors.New("ent: mutation is not running in a transaction") + } + tx := &Tx{config: m.config} + tx.init() + return tx, nil +} + +// ID returns the ID value in the mutation. Note that the ID is only available +// if it was provided to the builder or after it was returned from the database. +func (m *EmailMutation) ID() (id int, exists bool) { + if m.id == nil { + return + } + return *m.id, true +} + +// IDs queries the database and returns the entity ids that match the mutation's predicate. +// That means, if the mutation is applied within a transaction with an isolation level such +// as sql.LevelSerializable, the returned ids match the ids of the rows that will be updated +// or updated by the mutation. +func (m *EmailMutation) IDs(ctx context.Context) ([]int, error) { + switch { + case m.op.Is(OpUpdateOne | OpDeleteOne): + id, exists := m.ID() + if exists { + return []int{id}, nil + } + fallthrough + case m.op.Is(OpUpdate | OpDelete): + return m.Client().Email.Query().Where(m.predicates...).IDs(ctx) + default: + return nil, fmt.Errorf("IDs is not allowed on %s operations", m.op) + } +} + +// SetEmail sets the "email" field. +func (m *EmailMutation) SetEmail(s string) { + m.email = &s +} + +// Email returns the value of the "email" field in the mutation. +func (m *EmailMutation) Email() (r string, exists bool) { + v := m.email + if v == nil { + return + } + return *v, true +} + +// OldEmail returns the old "email" field's value of the Email entity. +// If the Email object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EmailMutation) OldEmail(ctx context.Context) (v string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldEmail is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldEmail requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldEmail: %w", err) + } + return oldValue.Email, nil +} + +// ResetEmail resets all changes to the "email" field. +func (m *EmailMutation) ResetEmail() { + m.email = nil +} + +// SetCreatedAt sets the "createdAt" field. +func (m *EmailMutation) SetCreatedAt(t time.Time) { + m.createdAt = &t +} + +// CreatedAt returns the value of the "createdAt" field in the mutation. +func (m *EmailMutation) CreatedAt() (r time.Time, exists bool) { + v := m.createdAt + if v == nil { + return + } + return *v, true +} + +// OldCreatedAt returns the old "createdAt" field's value of the Email entity. +// If the Email object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EmailMutation) OldCreatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldCreatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldCreatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldCreatedAt: %w", err) + } + return oldValue.CreatedAt, nil +} + +// ResetCreatedAt resets all changes to the "createdAt" field. +func (m *EmailMutation) ResetCreatedAt() { + m.createdAt = nil +} + +// SetUpdatedAt sets the "updatedAt" field. +func (m *EmailMutation) SetUpdatedAt(t time.Time) { + m.updatedAt = &t +} + +// UpdatedAt returns the value of the "updatedAt" field in the mutation. +func (m *EmailMutation) UpdatedAt() (r time.Time, exists bool) { + v := m.updatedAt + if v == nil { + return + } + return *v, true +} + +// OldUpdatedAt returns the old "updatedAt" field's value of the Email entity. +// If the Email object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *EmailMutation) OldUpdatedAt(ctx context.Context) (v time.Time, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldUpdatedAt is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldUpdatedAt requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldUpdatedAt: %w", err) + } + return oldValue.UpdatedAt, nil +} + +// ResetUpdatedAt resets all changes to the "updatedAt" field. +func (m *EmailMutation) ResetUpdatedAt() { + m.updatedAt = nil +} + +// Where appends a list predicates to the EmailMutation builder. +func (m *EmailMutation) Where(ps ...predicate.Email) { + m.predicates = append(m.predicates, ps...) +} + +// Op returns the operation name. +func (m *EmailMutation) Op() Op { + return m.op +} + +// Type returns the node type of this mutation (Email). +func (m *EmailMutation) Type() string { + return m.typ +} + +// Fields returns all fields that were changed during this mutation. Note that in +// order to get all numeric fields that were incremented/decremented, call +// AddedFields(). +func (m *EmailMutation) Fields() []string { + fields := make([]string, 0, 3) + if m.email != nil { + fields = append(fields, email.FieldEmail) + } + if m.createdAt != nil { + fields = append(fields, email.FieldCreatedAt) + } + if m.updatedAt != nil { + fields = append(fields, email.FieldUpdatedAt) + } + return fields +} + +// Field returns the value of a field with the given name. The second boolean +// return value indicates that this field was not set, or was not defined in the +// schema. +func (m *EmailMutation) Field(name string) (ent.Value, bool) { + switch name { + case email.FieldEmail: + return m.Email() + case email.FieldCreatedAt: + return m.CreatedAt() + case email.FieldUpdatedAt: + return m.UpdatedAt() + } + return nil, false +} + +// OldField returns the old value of the field from the database. An error is +// returned if the mutation operation is not UpdateOne, or the query to the +// database failed. +func (m *EmailMutation) OldField(ctx context.Context, name string) (ent.Value, error) { + switch name { + case email.FieldEmail: + return m.OldEmail(ctx) + case email.FieldCreatedAt: + return m.OldCreatedAt(ctx) + case email.FieldUpdatedAt: + return m.OldUpdatedAt(ctx) + } + return nil, fmt.Errorf("unknown Email field %s", name) +} + +// SetField sets the value of a field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *EmailMutation) SetField(name string, value ent.Value) error { + switch name { + case email.FieldEmail: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetEmail(v) + return nil + case email.FieldCreatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetCreatedAt(v) + return nil + case email.FieldUpdatedAt: + v, ok := value.(time.Time) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetUpdatedAt(v) + return nil + } + return fmt.Errorf("unknown Email field %s", name) +} + +// AddedFields returns all numeric fields that were incremented/decremented during +// this mutation. +func (m *EmailMutation) AddedFields() []string { + return nil +} + +// AddedField returns the numeric value that was incremented/decremented on a field +// with the given name. The second boolean return value indicates that this field +// was not set, or was not defined in the schema. +func (m *EmailMutation) AddedField(name string) (ent.Value, bool) { + return nil, false +} + +// AddField adds the value to the field with the given name. It returns an error if +// the field is not defined in the schema, or if the type mismatched the field +// type. +func (m *EmailMutation) AddField(name string, value ent.Value) error { + switch name { + } + return fmt.Errorf("unknown Email numeric field %s", name) +} + +// ClearedFields returns all nullable fields that were cleared during this +// mutation. +func (m *EmailMutation) ClearedFields() []string { + return nil +} + +// FieldCleared returns a boolean indicating if a field with the given name was +// cleared in this mutation. +func (m *EmailMutation) FieldCleared(name string) bool { + _, ok := m.clearedFields[name] + return ok +} + +// ClearField clears the value of the field with the given name. It returns an +// error if the field is not defined in the schema. +func (m *EmailMutation) ClearField(name string) error { + return fmt.Errorf("unknown Email nullable field %s", name) +} + +// ResetField resets all changes in the mutation for the field with the given name. +// It returns an error if the field is not defined in the schema. +func (m *EmailMutation) ResetField(name string) error { + switch name { + case email.FieldEmail: + m.ResetEmail() + return nil + case email.FieldCreatedAt: + m.ResetCreatedAt() + return nil + case email.FieldUpdatedAt: + m.ResetUpdatedAt() + return nil + } + return fmt.Errorf("unknown Email field %s", name) +} + +// AddedEdges returns all edge names that were set/added in this mutation. +func (m *EmailMutation) AddedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// AddedIDs returns all IDs (to other nodes) that were added for the given edge +// name in this mutation. +func (m *EmailMutation) AddedIDs(name string) []ent.Value { + return nil +} + +// RemovedEdges returns all edge names that were removed in this mutation. +func (m *EmailMutation) RemovedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// RemovedIDs returns all IDs (to other nodes) that were removed for the edge with +// the given name in this mutation. +func (m *EmailMutation) RemovedIDs(name string) []ent.Value { + return nil +} + +// ClearedEdges returns all edge names that were cleared in this mutation. +func (m *EmailMutation) ClearedEdges() []string { + edges := make([]string, 0, 0) + return edges +} + +// EdgeCleared returns a boolean which indicates if the edge with the given name +// was cleared in this mutation. +func (m *EmailMutation) EdgeCleared(name string) bool { + return false +} + +// ClearEdge clears the value of the edge with the given name. It returns an error +// if that edge is not defined in the schema. +func (m *EmailMutation) ClearEdge(name string) error { + return fmt.Errorf("unknown Email unique edge %s", name) +} + +// ResetEdge resets all changes to the edge with the given name in this mutation. +// It returns an error if the edge is not defined in the schema. +func (m *EmailMutation) ResetEdge(name string) error { + return fmt.Errorf("unknown Email edge %s", name) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/predicate/predicate.go b/example/bannedDomains/email/internal/features/repository/ent/predicate/predicate.go new file mode 100644 index 0000000..a59bb3a --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/predicate/predicate.go @@ -0,0 +1,10 @@ +// Code generated by ent, DO NOT EDIT. + +package predicate + +import ( + "entgo.io/ent/dialect/sql" +) + +// Email is the predicate function for email builders. +type Email func(*sql.Selector) diff --git a/example/bannedDomains/email/internal/features/repository/ent/runtime.go b/example/bannedDomains/email/internal/features/repository/ent/runtime.go new file mode 100644 index 0000000..e834c02 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/runtime.go @@ -0,0 +1,28 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "time" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/email" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent/schema" +) + +// The init function reads all schema descriptors with runtime code +// (default values, validators, hooks and policies) and stitches it +// to their package variables. +func init() { + emailFields := schema.Email{}.Fields() + _ = emailFields + // emailDescCreatedAt is the schema descriptor for createdAt field. + emailDescCreatedAt := emailFields[1].Descriptor() + // email.DefaultCreatedAt holds the default value on creation for the createdAt field. + email.DefaultCreatedAt = emailDescCreatedAt.Default.(func() time.Time) + // emailDescUpdatedAt is the schema descriptor for updatedAt field. + emailDescUpdatedAt := emailFields[2].Descriptor() + // email.DefaultUpdatedAt holds the default value on creation for the updatedAt field. + email.DefaultUpdatedAt = emailDescUpdatedAt.Default.(func() time.Time) + // email.UpdateDefaultUpdatedAt holds the default value on update for the updatedAt field. + email.UpdateDefaultUpdatedAt = emailDescUpdatedAt.UpdateDefault.(func() time.Time) +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/runtime/runtime.go b/example/bannedDomains/email/internal/features/repository/ent/runtime/runtime.go new file mode 100644 index 0000000..e22513e --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/runtime/runtime.go @@ -0,0 +1,10 @@ +// Code generated by ent, DO NOT EDIT. + +package runtime + +// The schema-stitching logic is generated in github.com/gothunder/thunder/example/email/internal/features/repository/ent/runtime.go + +const ( + Version = "v0.11.4" // Version of ent codegen. + Sum = "h1:grwVY0fp31BZ6oEo3YrXenAuv8VJmEw7F/Bi6WqeH3Q=" // Sum of ent codegen. +) diff --git a/example/bannedDomains/email/internal/features/repository/ent/schema/email.go b/example/bannedDomains/email/internal/features/repository/ent/schema/email.go new file mode 100644 index 0000000..3bdbe42 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/schema/email.go @@ -0,0 +1,27 @@ +package schema + +import ( + "time" + + "entgo.io/ent" + "entgo.io/ent/schema/field" +) + +// Email holds the schema definition for the Email entity. +type Email struct { + ent.Schema +} + +// Fields of the Email. +func (Email) Fields() []ent.Field { + return []ent.Field{ + field.String("email"), + field.Time("createdAt").Default(time.Now), + field.Time("updatedAt").Default(time.Now).UpdateDefault(time.Now), + } +} + +// Edges of the Email. +func (Email) Edges() []ent.Edge { + return nil +} diff --git a/example/bannedDomains/email/internal/features/repository/ent/tx.go b/example/bannedDomains/email/internal/features/repository/ent/tx.go new file mode 100644 index 0000000..32756b4 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/ent/tx.go @@ -0,0 +1,210 @@ +// Code generated by ent, DO NOT EDIT. + +package ent + +import ( + "context" + "sync" + + "entgo.io/ent/dialect" +) + +// Tx is a transactional client that is created by calling Client.Tx(). +type Tx struct { + config + // Email is the client for interacting with the Email builders. + Email *EmailClient + + // lazily loaded. + client *Client + clientOnce sync.Once + // ctx lives for the life of the transaction. It is + // the same context used by the underlying connection. + ctx context.Context +} + +type ( + // Committer is the interface that wraps the Commit method. + Committer interface { + Commit(context.Context, *Tx) error + } + + // The CommitFunc type is an adapter to allow the use of ordinary + // function as a Committer. If f is a function with the appropriate + // signature, CommitFunc(f) is a Committer that calls f. + CommitFunc func(context.Context, *Tx) error + + // CommitHook defines the "commit middleware". A function that gets a Committer + // and returns a Committer. For example: + // + // hook := func(next ent.Committer) ent.Committer { + // return ent.CommitFunc(func(ctx context.Context, tx *ent.Tx) error { + // // Do some stuff before. + // if err := next.Commit(ctx, tx); err != nil { + // return err + // } + // // Do some stuff after. + // return nil + // }) + // } + // + CommitHook func(Committer) Committer +) + +// Commit calls f(ctx, m). +func (f CommitFunc) Commit(ctx context.Context, tx *Tx) error { + return f(ctx, tx) +} + +// Commit commits the transaction. +func (tx *Tx) Commit() error { + txDriver := tx.config.driver.(*txDriver) + var fn Committer = CommitFunc(func(context.Context, *Tx) error { + return txDriver.tx.Commit() + }) + txDriver.mu.Lock() + hooks := append([]CommitHook(nil), txDriver.onCommit...) + txDriver.mu.Unlock() + for i := len(hooks) - 1; i >= 0; i-- { + fn = hooks[i](fn) + } + return fn.Commit(tx.ctx, tx) +} + +// OnCommit adds a hook to call on commit. +func (tx *Tx) OnCommit(f CommitHook) { + txDriver := tx.config.driver.(*txDriver) + txDriver.mu.Lock() + txDriver.onCommit = append(txDriver.onCommit, f) + txDriver.mu.Unlock() +} + +type ( + // Rollbacker is the interface that wraps the Rollback method. + Rollbacker interface { + Rollback(context.Context, *Tx) error + } + + // The RollbackFunc type is an adapter to allow the use of ordinary + // function as a Rollbacker. If f is a function with the appropriate + // signature, RollbackFunc(f) is a Rollbacker that calls f. + RollbackFunc func(context.Context, *Tx) error + + // RollbackHook defines the "rollback middleware". A function that gets a Rollbacker + // and returns a Rollbacker. For example: + // + // hook := func(next ent.Rollbacker) ent.Rollbacker { + // return ent.RollbackFunc(func(ctx context.Context, tx *ent.Tx) error { + // // Do some stuff before. + // if err := next.Rollback(ctx, tx); err != nil { + // return err + // } + // // Do some stuff after. + // return nil + // }) + // } + // + RollbackHook func(Rollbacker) Rollbacker +) + +// Rollback calls f(ctx, m). +func (f RollbackFunc) Rollback(ctx context.Context, tx *Tx) error { + return f(ctx, tx) +} + +// Rollback rollbacks the transaction. +func (tx *Tx) Rollback() error { + txDriver := tx.config.driver.(*txDriver) + var fn Rollbacker = RollbackFunc(func(context.Context, *Tx) error { + return txDriver.tx.Rollback() + }) + txDriver.mu.Lock() + hooks := append([]RollbackHook(nil), txDriver.onRollback...) + txDriver.mu.Unlock() + for i := len(hooks) - 1; i >= 0; i-- { + fn = hooks[i](fn) + } + return fn.Rollback(tx.ctx, tx) +} + +// OnRollback adds a hook to call on rollback. +func (tx *Tx) OnRollback(f RollbackHook) { + txDriver := tx.config.driver.(*txDriver) + txDriver.mu.Lock() + txDriver.onRollback = append(txDriver.onRollback, f) + txDriver.mu.Unlock() +} + +// Client returns a Client that binds to current transaction. +func (tx *Tx) Client() *Client { + tx.clientOnce.Do(func() { + tx.client = &Client{config: tx.config} + tx.client.init() + }) + return tx.client +} + +func (tx *Tx) init() { + tx.Email = NewEmailClient(tx.config) +} + +// txDriver wraps the given dialect.Tx with a nop dialect.Driver implementation. +// The idea is to support transactions without adding any extra code to the builders. +// When a builder calls to driver.Tx(), it gets the same dialect.Tx instance. +// Commit and Rollback are nop for the internal builders and the user must call one +// of them in order to commit or rollback the transaction. +// +// If a closed transaction is embedded in one of the generated entities, and the entity +// applies a query, for example: Email.QueryXXX(), the query will be executed +// through the driver which created this transaction. +// +// Note that txDriver is not goroutine safe. +type txDriver struct { + // the driver we started the transaction from. + drv dialect.Driver + // tx is the underlying transaction. + tx dialect.Tx + // completion hooks. + mu sync.Mutex + onCommit []CommitHook + onRollback []RollbackHook +} + +// newTx creates a new transactional driver. +func newTx(ctx context.Context, drv dialect.Driver) (*txDriver, error) { + tx, err := drv.Tx(ctx) + if err != nil { + return nil, err + } + return &txDriver{tx: tx, drv: drv}, nil +} + +// Tx returns the transaction wrapper (txDriver) to avoid Commit or Rollback calls +// from the internal builders. Should be called only by the internal builders. +func (tx *txDriver) Tx(context.Context) (dialect.Tx, error) { return tx, nil } + +// Dialect returns the dialect of the driver we started the transaction from. +func (tx *txDriver) Dialect() string { return tx.drv.Dialect() } + +// Close is a nop close. +func (*txDriver) Close() error { return nil } + +// Commit is a nop commit for the internal builders. +// User must call `Tx.Commit` in order to commit the transaction. +func (*txDriver) Commit() error { return nil } + +// Rollback is a nop rollback for the internal builders. +// User must call `Tx.Rollback` in order to rollback the transaction. +func (*txDriver) Rollback() error { return nil } + +// Exec calls tx.Exec. +func (tx *txDriver) Exec(ctx context.Context, query string, args, v any) error { + return tx.tx.Exec(ctx, query, args, v) +} + +// Query calls tx.Query. +func (tx *txDriver) Query(ctx context.Context, query string, args, v any) error { + return tx.tx.Query(ctx, query, args, v) +} + +var _ dialect.Driver = (*txDriver)(nil) diff --git a/example/bannedDomains/email/internal/features/repository/module.go b/example/bannedDomains/email/internal/features/repository/module.go new file mode 100644 index 0000000..9604b47 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/module.go @@ -0,0 +1,9 @@ +package repository + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewClient, + ), +) diff --git a/example/bannedDomains/email/internal/features/repository/orm.go b/example/bannedDomains/email/internal/features/repository/orm.go new file mode 100644 index 0000000..d302276 --- /dev/null +++ b/example/bannedDomains/email/internal/features/repository/orm.go @@ -0,0 +1,38 @@ +package repository + +import ( + "context" + + "github.com/gothunder/thunder/example/email/internal/features/repository/ent" + "github.com/rs/zerolog" + "go.uber.org/fx" + + _ "github.com/lib/pq" +) + +func NewClient(logger *zerolog.Logger, lc fx.Lifecycle) *ent.Client { + client := Connect(logger) + + lc.Append(fx.Hook{ + OnStop: func(ctx context.Context) error { + return client.Close() + }, + }) + + return client +} + +func Connect(logger *zerolog.Logger) *ent.Client { + client, err := ent.Open("postgres", "host=db port=5432 user=postgres dbname=email sslmode=disable password=password") + if err != nil { + logger.Error().Err(err).Msg("failed opening connection to postgres") + } + + // Run the auto migration tool. + if err := client.Schema.Create(context.Background()); err != nil { + logger.Error().Err(err).Msg("failed creating schema resources") + } + + logger.Info().Msg("connected to postgres") + return client +} diff --git a/example/bannedDomains/email/internal/transport-inbound/consumers/banEvent.go b/example/bannedDomains/email/internal/transport-inbound/consumers/banEvent.go new file mode 100644 index 0000000..955c55f --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/consumers/banEvent.go @@ -0,0 +1,18 @@ +package consumers + +import ( + "context" + + thunderEvents "github.com/gothunder/thunder/pkg/events" + "github.com/gothunder/thunder/example/ban/pkg/events" +) + +func (c *ConsumerGroup) banEvent(ctx context.Context, payload events.BanPayload) thunderEvents.HandlerResponse { + id := payload.ID + err := c.emailService.Delete(ctx, id) + if err != nil { + return thunderEvents.DeadLetter + } + + return thunderEvents.Success +} diff --git a/example/bannedDomains/email/internal/transport-inbound/consumers/consumerGroup.go b/example/bannedDomains/email/internal/transport-inbound/consumers/consumerGroup.go new file mode 100644 index 0000000..69c98a1 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/consumers/consumerGroup.go @@ -0,0 +1,16 @@ +package consumers + +import ( + thunderEvents "github.com/gothunder/thunder/pkg/events" + "github.com/gothunder/thunder/example/email/internal/features" +) + +type ConsumerGroup struct { + emailService features.EmailService +} + +func newConsumerGroup(emailService features.EmailService) thunderEvents.Handler { + return &ConsumerGroup{ + emailService: emailService, + } +} diff --git a/example/bannedDomains/email/internal/transport-inbound/consumers/handle.go b/example/bannedDomains/email/internal/transport-inbound/consumers/handle.go new file mode 100644 index 0000000..6a42091 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/consumers/handle.go @@ -0,0 +1,30 @@ +package consumers + +import ( + "context" + + thunderEvents "github.com/gothunder/thunder/pkg/events" + "github.com/gothunder/thunder/example/ban/pkg/events" + emailEvents "github.com/gothunder/thunder/example/email/pkg/events" + "github.com/rs/zerolog/log" +) + +func (c *ConsumerGroup) Handle(ctx context.Context, topic string, decoder thunderEvents.EventDecoder) thunderEvents.HandlerResponse { + switch { + case topic == events.BanTopic: + var formattedPayload events.BanPayload + err := decoder.Decode(&formattedPayload) + if err != nil { + log.Ctx(ctx).Error().Err(err).Msg("failed to decode payload") + return thunderEvents.DeadLetter + } + log.Ctx(ctx).Debug().Msgf("Got ban request for domain ID '%d'", formattedPayload.ID) + + return c.banEvent(ctx, formattedPayload) + case topic == emailEvents.EmailTopic: + log.Ctx(ctx).Debug().Msg("got email event") + return thunderEvents.Retry + default: + return thunderEvents.DeadLetter + } +} diff --git a/example/bannedDomains/email/internal/transport-inbound/consumers/module.go b/example/bannedDomains/email/internal/transport-inbound/consumers/module.go new file mode 100644 index 0000000..8b1f81b --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/consumers/module.go @@ -0,0 +1,9 @@ +package consumers + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + newConsumerGroup, + ), +) diff --git a/example/bannedDomains/email/internal/transport-inbound/consumers/topics.go b/example/bannedDomains/email/internal/transport-inbound/consumers/topics.go new file mode 100644 index 0000000..fb3cff7 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/consumers/topics.go @@ -0,0 +1,9 @@ +package consumers + +import "github.com/gothunder/thunder/example/ban/pkg/events" + +func (c *ConsumerGroup) Topics() []string { + return []string{ + events.BanTopic, + } +} diff --git a/example/bannedDomains/email/internal/transport-inbound/module.go b/example/bannedDomains/email/internal/transport-inbound/module.go new file mode 100644 index 0000000..bc4a447 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/module.go @@ -0,0 +1,12 @@ +package transportinbound + +import ( + "github.com/gothunder/thunder/example/email/internal/transport-inbound/consumers" + "github.com/gothunder/thunder/example/email/internal/transport-inbound/router" + "go.uber.org/fx" +) + +var Module = fx.Options( + consumers.Module, + router.Module, +) diff --git a/example/bannedDomains/email/internal/transport-inbound/router/handlers.go b/example/bannedDomains/email/internal/transport-inbound/router/handlers.go new file mode 100644 index 0000000..94ccc4f --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/router/handlers.go @@ -0,0 +1,85 @@ +package router + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/gothunder/thunder/pkg/router" + "github.com/gothunder/thunder/example/email/internal/features" + "github.com/gothunder/thunder/example/email/internal/features/repository/ent" + "github.com/gothunder/thunder/example/email/internal/transport-outbound/publisher" + "github.com/gothunder/thunder/example/email/pkg/events" +) + +func NewEmailHandler(emailService features.EmailService, publisher *publisher.PublisherGroup) router.HandlerOutput { + return router.HandlerOutput{ + Handler: emailHandler{ + emailService, + publisher, + }, + } +} + +type emailHandler struct { + service features.EmailService + publisher *publisher.PublisherGroup +} + +type EmailRequest struct { + Email string `json:"email"` +} + +func (h emailHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var emailRequest EmailRequest + err := json.NewDecoder(r.Body).Decode(&emailRequest) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + + if emailRequest.Email == "" { + http.Error(w, "no email provided", http.StatusBadRequest) + return + } + + if !features.IsValidEmail(emailRequest.Email) { + http.Error(w, "invalid email provided", http.StatusBadRequest) + return + } + + ctx := r.Context() + emailRegistry, err := h.service.Create(ctx, emailRequest.Email) + h.publisher.SendEmailEvent(ctx, createEmailEvent(emailRegistry)) + + if err != nil { + http.Error(w, fmt.Sprintf("error creating email: %s", err), http.StatusInternalServerError) + return + } + + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusCreated) + + jsonEmailRegistry, err := json.Marshal(emailRegistry) + if err != nil { + http.Error(w, fmt.Sprintf("error marshalling emailRegistry: %s", err), http.StatusInternalServerError) + return + } + + w.Write(jsonEmailRegistry) +} + +func (h emailHandler) Method() string { + return http.MethodPost +} + +func (h emailHandler) Pattern() string { + return "/email" +} + +func createEmailEvent(email *ent.Email) events.EmailPayload { + return events.EmailPayload{ + ID: email.ID, + Email: email.Email, + } +} diff --git a/example/bannedDomains/email/internal/transport-inbound/router/module.go b/example/bannedDomains/email/internal/transport-inbound/router/module.go new file mode 100644 index 0000000..fb75936 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-inbound/router/module.go @@ -0,0 +1,9 @@ +package router + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + NewEmailHandler, + ), +) diff --git a/example/bannedDomains/email/internal/transport-outbound/module.go b/example/bannedDomains/email/internal/transport-outbound/module.go new file mode 100644 index 0000000..02ea392 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-outbound/module.go @@ -0,0 +1,10 @@ +package transportoutbound + +import ( + "github.com/gothunder/thunder/example/email/internal/transport-outbound/publisher" + "go.uber.org/fx" +) + +var Module = fx.Options( + publisher.Module, +) diff --git a/example/bannedDomains/email/internal/transport-outbound/publisher/emailEvent.go b/example/bannedDomains/email/internal/transport-outbound/publisher/emailEvent.go new file mode 100644 index 0000000..11978bd --- /dev/null +++ b/example/bannedDomains/email/internal/transport-outbound/publisher/emailEvent.go @@ -0,0 +1,11 @@ +package publisher + +import ( + "context" + + "github.com/gothunder/thunder/example/email/pkg/events" +) + +func (pg *PublisherGroup) SendEmailEvent(ctx context.Context, event events.EmailPayload) error { + return pg.publisher.Publish(ctx, events.EmailTopic, event) +} diff --git a/example/bannedDomains/email/internal/transport-outbound/publisher/module.go b/example/bannedDomains/email/internal/transport-outbound/publisher/module.go new file mode 100644 index 0000000..6f6d099 --- /dev/null +++ b/example/bannedDomains/email/internal/transport-outbound/publisher/module.go @@ -0,0 +1,9 @@ +package publisher + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + newPublisherGroup, + ), +) diff --git a/example/bannedDomains/email/internal/transport-outbound/publisher/publisherGroup.go b/example/bannedDomains/email/internal/transport-outbound/publisher/publisherGroup.go new file mode 100644 index 0000000..cbdc71a --- /dev/null +++ b/example/bannedDomains/email/internal/transport-outbound/publisher/publisherGroup.go @@ -0,0 +1,15 @@ +package publisher + +import ( + thunderEvents "github.com/gothunder/thunder/pkg/events" +) + +type PublisherGroup struct { + publisher thunderEvents.EventPublisher +} + +func newPublisherGroup(publisher thunderEvents.EventPublisher) *PublisherGroup { + return &PublisherGroup{ + publisher: publisher, + } +} diff --git a/example/bannedDomains/email/main.go b/example/bannedDomains/email/main.go new file mode 100644 index 0000000..1a446c5 --- /dev/null +++ b/example/bannedDomains/email/main.go @@ -0,0 +1,36 @@ +package main + +import ( + thunderEventRabbitmq "github.com/gothunder/thunder/pkg/events/rabbitmq" + thunderLogs "github.com/gothunder/thunder/pkg/log" + thunderChi "github.com/gothunder/thunder/pkg/router/chi" + "github.com/gothunder/thunder/example/email/internal/features" + "github.com/gothunder/thunder/example/email/internal/features/repository" + transportinbound "github.com/gothunder/thunder/example/email/internal/transport-inbound" + transportoutbound "github.com/gothunder/thunder/example/email/internal/transport-outbound" + + "github.com/rs/zerolog/diode" + "go.uber.org/fx" +) + +func main() { + var w diode.Writer + + app := fx.New( + fx.Populate(&w), + thunderLogs.Module, + thunderChi.Module, + fx.Invoke(thunderChi.StartListener), + + transportinbound.Module, + transportoutbound.Module, + repository.Module, + features.Module, + + thunderEventRabbitmq.PublisherModule, + thunderEventRabbitmq.InvokeConsumer, + ) + app.Run() + + thunderLogs.DiodeShutdown(w) +} diff --git a/example/bannedDomains/email/pkg/events/email.go b/example/bannedDomains/email/pkg/events/email.go new file mode 100644 index 0000000..4b94c86 --- /dev/null +++ b/example/bannedDomains/email/pkg/events/email.go @@ -0,0 +1,8 @@ +package events + +const EmailTopic = "topic.email" + +type EmailPayload struct { + ID int `json:"id"` + Email string `json:"email"` +} diff --git a/example/docker-compose.yml b/example/gettingStarted/docker-compose.yml similarity index 100% rename from example/docker-compose.yml rename to example/gettingStarted/docker-compose.yml diff --git a/example/internal/transport-inbound/consumers/consumerGroup.go b/example/gettingStarted/internal/transport-inbound/consumers/consumerGroup.go similarity index 100% rename from example/internal/transport-inbound/consumers/consumerGroup.go rename to example/gettingStarted/internal/transport-inbound/consumers/consumerGroup.go diff --git a/example/internal/transport-inbound/consumers/handle.go b/example/gettingStarted/internal/transport-inbound/consumers/handle.go similarity index 100% rename from example/internal/transport-inbound/consumers/handle.go rename to example/gettingStarted/internal/transport-inbound/consumers/handle.go diff --git a/example/gettingStarted/internal/transport-inbound/consumers/module.go b/example/gettingStarted/internal/transport-inbound/consumers/module.go new file mode 100644 index 0000000..8b1f81b --- /dev/null +++ b/example/gettingStarted/internal/transport-inbound/consumers/module.go @@ -0,0 +1,9 @@ +package consumers + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + newConsumerGroup, + ), +) diff --git a/example/internal/transport-inbound/consumers/testEvent.go b/example/gettingStarted/internal/transport-inbound/consumers/testEvent.go similarity index 100% rename from example/internal/transport-inbound/consumers/testEvent.go rename to example/gettingStarted/internal/transport-inbound/consumers/testEvent.go diff --git a/example/internal/transport-inbound/consumers/topics.go b/example/gettingStarted/internal/transport-inbound/consumers/topics.go similarity index 100% rename from example/internal/transport-inbound/consumers/topics.go rename to example/gettingStarted/internal/transport-inbound/consumers/topics.go diff --git a/example/internal/transport-inbound/module.go b/example/gettingStarted/internal/transport-inbound/module.go similarity index 100% rename from example/internal/transport-inbound/module.go rename to example/gettingStarted/internal/transport-inbound/module.go diff --git a/example/internal/transport-outbound/module.go b/example/gettingStarted/internal/transport-outbound/module.go similarity index 100% rename from example/internal/transport-outbound/module.go rename to example/gettingStarted/internal/transport-outbound/module.go diff --git a/example/gettingStarted/internal/transport-outbound/publisher/module.go b/example/gettingStarted/internal/transport-outbound/publisher/module.go new file mode 100644 index 0000000..6f6d099 --- /dev/null +++ b/example/gettingStarted/internal/transport-outbound/publisher/module.go @@ -0,0 +1,9 @@ +package publisher + +import "go.uber.org/fx" + +var Module = fx.Options( + fx.Provide( + newPublisherGroup, + ), +) diff --git a/example/gettingStarted/internal/transport-outbound/publisher/publisherGroup.go b/example/gettingStarted/internal/transport-outbound/publisher/publisherGroup.go new file mode 100644 index 0000000..cbdc71a --- /dev/null +++ b/example/gettingStarted/internal/transport-outbound/publisher/publisherGroup.go @@ -0,0 +1,15 @@ +package publisher + +import ( + thunderEvents "github.com/gothunder/thunder/pkg/events" +) + +type PublisherGroup struct { + publisher thunderEvents.EventPublisher +} + +func newPublisherGroup(publisher thunderEvents.EventPublisher) *PublisherGroup { + return &PublisherGroup{ + publisher: publisher, + } +} diff --git a/example/internal/transport-outbound/publisher/testEvent.go b/example/gettingStarted/internal/transport-outbound/publisher/testEvent.go similarity index 100% rename from example/internal/transport-outbound/publisher/testEvent.go rename to example/gettingStarted/internal/transport-outbound/publisher/testEvent.go diff --git a/example/main.go b/example/gettingStarted/main.go similarity index 100% rename from example/main.go rename to example/gettingStarted/main.go diff --git a/example/pkg/events/test.go b/example/gettingStarted/pkg/events/test.go similarity index 100% rename from example/pkg/events/test.go rename to example/gettingStarted/pkg/events/test.go diff --git a/example/tests/example_test.go b/example/gettingStarted/tests/example_test.go similarity index 100% rename from example/tests/example_test.go rename to example/gettingStarted/tests/example_test.go diff --git a/example/tests/suite_test.go b/example/gettingStarted/tests/suite_test.go similarity index 100% rename from example/tests/suite_test.go rename to example/gettingStarted/tests/suite_test.go