From 4144c36e2dc466160f7f946745def949f0784dd0 Mon Sep 17 00:00:00 2001 From: Zig Blathazar <42387185+ZigBalthazar@users.noreply.github.com> Date: Mon, 13 Jan 2025 17:08:17 +0330 Subject: [PATCH] feat(domain): add get all route (#1) --- .gitignore | 4 + cmd/daemon/daemon.go | 9 +- deliveries/grpc/gen/health.pb.go | 5 +- deliveries/grpc/gen/health_grpc.pb.go | 1 + .../http/handlers/domain/domain_create.go | 53 +++++ deliveries/http/handlers/domain/domain_get.go | 43 ++++ .../http/handlers/domain/domain_handler.go | 13 ++ .../http/handlers/domain/domain_request.go | 8 + .../http/handlers/domain/domain_response.go | 12 ++ .../http/handlers/domain/domain_router.go | 12 ++ .../handlers/domain_handler/domain_create.go | 53 ----- .../domain_handler/dto/domain_request.go | 8 - .../domain_handler/dto/domain_response.go | 5 - .../http/handlers/domain_handler/handler.go | 13 -- .../http/handlers/domain_handler/router.go | 11 - deliveries/http/http_handlers.go | 12 +- deliveries/http/http_server.go | 12 +- docs/docs.go | 59 ++++++ docs/swagger.json | 59 ++++++ docs/swagger.yaml | 36 ++++ documents/.keep | 0 infrastructures/grpc_client/gen/example.pb.go | 5 +- .../grpc_client/gen/example_grpc.pb.go | 1 + pkg/validator/validator.go | 22 +- repositories/base_repository.go | 22 +- repositories/domain/domain_repository.go | 57 ----- repositories/domain_repository.go | 62 ++++++ schema.md | 194 ++++++++++-------- schemas/{domain.go => domain_schema.go} | 2 +- services/domain/domain_create.go | 33 ++- services/domain/domain_error.go | 2 +- services/domain/domain_get.go | 27 +++ services/domain/domain_service.go | 18 +- services/domain/domain_validator.go | 7 - 34 files changed, 566 insertions(+), 314 deletions(-) create mode 100644 deliveries/http/handlers/domain/domain_create.go create mode 100644 deliveries/http/handlers/domain/domain_get.go create mode 100644 deliveries/http/handlers/domain/domain_handler.go create mode 100644 deliveries/http/handlers/domain/domain_request.go create mode 100644 deliveries/http/handlers/domain/domain_response.go create mode 100644 deliveries/http/handlers/domain/domain_router.go delete mode 100644 deliveries/http/handlers/domain_handler/domain_create.go delete mode 100644 deliveries/http/handlers/domain_handler/dto/domain_request.go delete mode 100644 deliveries/http/handlers/domain_handler/dto/domain_response.go delete mode 100644 deliveries/http/handlers/domain_handler/handler.go delete mode 100644 deliveries/http/handlers/domain_handler/router.go delete mode 100644 documents/.keep delete mode 100644 repositories/domain/domain_repository.go create mode 100644 repositories/domain_repository.go rename schemas/{domain.go => domain_schema.go} (87%) create mode 100644 services/domain/domain_get.go delete mode 100644 services/domain/domain_validator.go diff --git a/.gitignore b/.gitignore index 378eac2..a11b4aa 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,5 @@ build +**/.env +**/config.yml +.vscode +**/*.log \ No newline at end of file diff --git a/cmd/daemon/daemon.go b/cmd/daemon/daemon.go index 204ce7d..fc46e28 100644 --- a/cmd/daemon/daemon.go +++ b/cmd/daemon/daemon.go @@ -10,8 +10,8 @@ import ( grpcClient "github.com/dezh-tech/panda/infrastructures/grpc_client" "github.com/dezh-tech/panda/infrastructures/redis" "github.com/dezh-tech/panda/pkg/logger" - domainRepo "github.com/dezh-tech/panda/repositories/domain" - domainService "github.com/dezh-tech/panda/services/domain" + "github.com/dezh-tech/panda/repositories" + service "github.com/dezh-tech/panda/services/domain" ) type Daemon struct { @@ -38,9 +38,10 @@ func New(cfg *config.Config) (*Daemon, error) { return nil, err } - domainRepo := domainRepo.New(db.Client, cfg.Database.DBName, time.Duration(cfg.Database.QueryTimeout)) + domainRepo := repositories.NewDomainRepository(db.Client, cfg.Database.DBName, + time.Duration(cfg.Database.QueryTimeout)*time.Millisecond) - hs := http.New(cfg.HTTPServer, domainService.New(domainRepo)) + hs := http.New(cfg.HTTPServer, service.NewDomainService(domainRepo)) gs := grpc.New(&cfg.GRPCServer, r, db, time.Now()) return &Daemon{ diff --git a/deliveries/grpc/gen/health.pb.go b/deliveries/grpc/gen/health.pb.go index 406d0c5..a5f941a 100644 --- a/deliveries/grpc/gen/health.pb.go +++ b/deliveries/grpc/gen/health.pb.go @@ -7,10 +7,11 @@ package grpc_client import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/deliveries/grpc/gen/health_grpc.pb.go b/deliveries/grpc/gen/health_grpc.pb.go index fff7749..5f4c5f3 100644 --- a/deliveries/grpc/gen/health_grpc.pb.go +++ b/deliveries/grpc/gen/health_grpc.pb.go @@ -8,6 +8,7 @@ package grpc_client import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/deliveries/http/handlers/domain/domain_create.go b/deliveries/http/handlers/domain/domain_create.go new file mode 100644 index 0000000..996ce7e --- /dev/null +++ b/deliveries/http/handlers/domain/domain_create.go @@ -0,0 +1,53 @@ +package handlers + +import ( + "net/http" + + "github.com/dezh-tech/panda/pkg" + "github.com/dezh-tech/panda/pkg/validator" + "github.com/labstack/echo/v4" +) + +// CreateDomain creates a new domain. +// +// @Summary Create a new domain +// @Description Create a new domain with the specified attributes. +// @Tags domains +// @Accept json +// @Produce json +// @Param domain body DomainCreateRequest true "Domain creation payload" +// @Success 200 {object} pkg.ResponseDto{data=DomainCreateResponse} "Domain created successfully" +// @Failure 400 {object} pkg.ResponseDto[validator.Varror] "Bad Request - Validation error" +// @Failure 500 {object} pkg.ResponseDto[string] "Internal Server Error" +// @Router /domains [post] +func (dh Domain) create(c echo.Context) error { + req := new(DomainCreateRequest) + if err := c.Bind(req); err != nil { + return echo.NewHTTPError(http.StatusBadRequest, pkg.ResponseDto{ + Success: false, + Error: validator.Varror{Error: "invalid input"}, + }) + } + + // Validate the request payload + v := validator.NewValidator() + validationErrors := v.Validate(req) + if validationErrors != nil { + return echo.NewHTTPError(http.StatusBadRequest, pkg.ResponseDto{ + Success: false, + Error: validator.Varror{ValidationErrors: validationErrors}, + }) + } + + // Call the domain service to create the domain + ctx := c.Request().Context() + resp, err := dh.service.Create(ctx, req.Domain, req.Status, req.BasePricePerIdentifier, req.DefaultTTL) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, pkg.ResponseDto{ + Success: false, + Error: validator.Varror{Error: echo.ErrInternalServerError.Error()}, + }) + } + + return c.JSON(http.StatusOK, pkg.ResponseDto{Success: true, Data: DomainCreateResponse{ID: resp}}) +} diff --git a/deliveries/http/handlers/domain/domain_get.go b/deliveries/http/handlers/domain/domain_get.go new file mode 100644 index 0000000..69ba824 --- /dev/null +++ b/deliveries/http/handlers/domain/domain_get.go @@ -0,0 +1,43 @@ +package handlers + +import ( + "net/http" + + "github.com/dezh-tech/panda/pkg" + "github.com/dezh-tech/panda/pkg/validator" + "github.com/labstack/echo/v4" +) + +// DomainGetAll retrieves all domains. +// +// @Summary Retrieve all domains +// @Description Get a list of all domains with their attributes. +// @Tags domains +// @Accept json +// @Produce json +// @Success 200 {object} pkg.ResponseDto{data=[]DomainGetResponse} "Domains retrieved successfully" +// @Failure 500 {object} pkg.ResponseDto[string] "Internal Server Error" +// @Router /domains [get] +func (dh Domain) getAll(c echo.Context) error { + ctx := c.Request().Context() + domains, err := dh.service.GetAllWithoutFilter(ctx) + if err != nil { + return echo.NewHTTPError(http.StatusInternalServerError, pkg.ResponseDto{ + Success: false, + Error: validator.Varror{Error: echo.ErrInternalServerError.Error()}, + }) + } + + domainsRes := make([]DomainGetResponse, 0) + for _, d := range *domains { + domainsRes = append(domainsRes, DomainGetResponse{ + Domain: d.Domain, + BasePricePerIdentifier: d.BasePricePerIdentifier, + DefaultTTL: d.DefaultTTL, + Status: d.Status, + }) + } + + // Respond with the created domain's ID + return c.JSON(http.StatusOK, pkg.ResponseDto{Success: true, Data: domainsRes}) +} diff --git a/deliveries/http/handlers/domain/domain_handler.go b/deliveries/http/handlers/domain/domain_handler.go new file mode 100644 index 0000000..715f23b --- /dev/null +++ b/deliveries/http/handlers/domain/domain_handler.go @@ -0,0 +1,13 @@ +package handlers + +import service "github.com/dezh-tech/panda/services/domain" + +type Domain struct { + service service.Domain +} + +func NewDomainService(domainSvc service.Domain) Domain { + return Domain{ + service: domainSvc, + } +} diff --git a/deliveries/http/handlers/domain/domain_request.go b/deliveries/http/handlers/domain/domain_request.go new file mode 100644 index 0000000..ffa0ef0 --- /dev/null +++ b/deliveries/http/handlers/domain/domain_request.go @@ -0,0 +1,8 @@ +package handlers + +type DomainCreateRequest struct { + Domain string `json:"domain" validate:"required,hostname"` + BasePricePerIdentifier uint `json:"base_price_per_identifier" validate:"required,min=1"` + DefaultTTL uint32 `json:"default_ttl" validate:"required,min=1"` + Status string `json:"status" validate:"required,oneof=active inactive"` +} diff --git a/deliveries/http/handlers/domain/domain_response.go b/deliveries/http/handlers/domain/domain_response.go new file mode 100644 index 0000000..2721627 --- /dev/null +++ b/deliveries/http/handlers/domain/domain_response.go @@ -0,0 +1,12 @@ +package handlers + +type DomainCreateResponse struct { + ID interface{} `json:"id"` +} + +type DomainGetResponse struct { + Domain string `json:"domain"` + BasePricePerIdentifier uint `json:"base_price_per_identifier"` + DefaultTTL uint32 `json:"default_ttl"` + Status string `json:"status"` +} diff --git a/deliveries/http/handlers/domain/domain_router.go b/deliveries/http/handlers/domain/domain_router.go new file mode 100644 index 0000000..1ae64ab --- /dev/null +++ b/deliveries/http/handlers/domain/domain_router.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "github.com/labstack/echo/v4" +) + +func (dh Domain) SetDomainRoutes(e *echo.Echo) { + userGroup := e.Group("/domains") + + userGroup.POST("", dh.create) + userGroup.GET("", dh.getAll) +} diff --git a/deliveries/http/handlers/domain_handler/domain_create.go b/deliveries/http/handlers/domain_handler/domain_create.go deleted file mode 100644 index 1a53750..0000000 --- a/deliveries/http/handlers/domain_handler/domain_create.go +++ /dev/null @@ -1,53 +0,0 @@ -package domainhandler - -import ( - "net/http" - - domainhandler "github.com/dezh-tech/panda/deliveries/http/handlers/domain_handler/dto" - "github.com/dezh-tech/panda/pkg" - "github.com/dezh-tech/panda/pkg/validator" - domainService "github.com/dezh-tech/panda/services/domain" - "github.com/labstack/echo/v4" -) - -// CreateDomain creates a new domain. -// -// @Summary Create a new domain -// @Description Create a new domain with the specified attributes. -// @Tags domains -// @Accept json -// @Produce json -// @Param domain body domainhandler.DomainCreateRequest true "Domain creation payload" -// @Success 200 {object} pkg.ResponseDto{data=domainhandler.DomainCreateResponse} "Domain created successfully" -// @Failure 400 {object} pkg.ResponseDto[validator.Varror] "Bad Request - Validation error" -// @Failure 500 {object} pkg.ResponseDto[string] "Internal Server Error" -// @Router /domains [post] -func (h Handler) domainCreate(c echo.Context) error { - // Parse the request body into the DTO - req := new(domainhandler.DomainCreateRequest) - if err := c.Bind(req); err != nil { - return echo.NewHTTPError(http.StatusBadRequest, map[string]string{"error": "invalid input"}) - } - - // Validate the request payload - v := validator.NewValidator() - validationErrors := v.Validate(req) - if validationErrors != nil { - return echo.NewHTTPError(http.StatusBadRequest, pkg.ResponseDto{Success: false, Error: validator.Varror{ValidationErrors: validationErrors}}) - } - - // Call the domain service to create the domain - ctx := c.Request().Context() // Extract context from Echo - resp, err := h.domainSvc.Create(ctx, domainService.DomainInsertArgs{ - Domain: req.Domain, - BasePricePerIdentifier: req.BasePricePerIdentifier, - DefaultTTL: req.DefaultTTL, - Status: req.Status, - }) - if err != nil { - return echo.NewHTTPError(http.StatusInternalServerError, pkg.ResponseDto{Success: false, Error: validator.Varror{Error: echo.ErrInternalServerError.Error()}}) - } - - // Respond with the created domain's ID - return c.JSON(http.StatusOK, pkg.ResponseDto{Success: true, Data: domainhandler.DomainCreateResponse{ID: resp.ID}}) -} diff --git a/deliveries/http/handlers/domain_handler/dto/domain_request.go b/deliveries/http/handlers/domain_handler/dto/domain_request.go deleted file mode 100644 index ed5fcf0..0000000 --- a/deliveries/http/handlers/domain_handler/dto/domain_request.go +++ /dev/null @@ -1,8 +0,0 @@ -package domainhandler - -type DomainCreateRequest struct { - Domain string `json:"domain" validate:"required,hostname" form:"domain" query:"domain"` - BasePricePerIdentifier uint `json:"base_price_per_identifier" validate:"required,min=1" form:"base_price_per_identifier" query:"base_price_per_identifier"` - DefaultTTL uint32 `json:"default_ttl" validate:"required,min=1" form:"default_ttl" query:"default_ttl"` - Status string `json:"status" validate:"required,oneof=active inactive" form:"status" query:"status"` -} diff --git a/deliveries/http/handlers/domain_handler/dto/domain_response.go b/deliveries/http/handlers/domain_handler/dto/domain_response.go deleted file mode 100644 index 6024018..0000000 --- a/deliveries/http/handlers/domain_handler/dto/domain_response.go +++ /dev/null @@ -1,5 +0,0 @@ -package domainhandler - -type DomainCreateResponse struct { - ID interface{} `json:"id"` -} diff --git a/deliveries/http/handlers/domain_handler/handler.go b/deliveries/http/handlers/domain_handler/handler.go deleted file mode 100644 index ff9b84e..0000000 --- a/deliveries/http/handlers/domain_handler/handler.go +++ /dev/null @@ -1,13 +0,0 @@ -package domainhandler - -import domainService "github.com/dezh-tech/panda/services/domain" - -type Handler struct { - domainSvc domainService.DomainService -} - -func New(domainSvc domainService.DomainService) Handler { - return Handler{ - domainSvc: domainSvc, - } -} diff --git a/deliveries/http/handlers/domain_handler/router.go b/deliveries/http/handlers/domain_handler/router.go deleted file mode 100644 index 6ea7f5c..0000000 --- a/deliveries/http/handlers/domain_handler/router.go +++ /dev/null @@ -1,11 +0,0 @@ -package domainhandler - -import ( - "github.com/labstack/echo/v4" -) - -func (h Handler) SetRoutes(e *echo.Echo) { - userGroup := e.Group("/domains") - - userGroup.POST("", h.domainCreate) -} diff --git a/deliveries/http/http_handlers.go b/deliveries/http/http_handlers.go index 5f137e6..6bab7c0 100644 --- a/deliveries/http/http_handlers.go +++ b/deliveries/http/http_handlers.go @@ -1,8 +1,8 @@ package http import ( - domainhandler "github.com/dezh-tech/panda/deliveries/http/handlers/domain_handler" - _ "github.com/dezh-tech/panda/docs" + handlers "github.com/dezh-tech/panda/deliveries/http/handlers/domain" + _ "github.com/dezh-tech/panda/docs" // revive:disable-line:blank-imports Justification: Required for Swagger documentation "github.com/labstack/echo/v4" echoSwagger "github.com/swaggo/echo-swagger" ) @@ -22,12 +22,12 @@ import ( // @host localhost:8080 // @BasePath / -type HttpHandlers struct { - user domainhandler.Handler +type Handlers struct { + domain handlers.Domain } -func (h *HttpHandlers) Start(r *echo.Echo) { - h.user.SetRoutes(r) +func (h *Handlers) Start(r *echo.Echo) { + h.domain.SetDomainRoutes(r) r.GET("/swagger/*", echoSwagger.WrapHandler) } diff --git a/deliveries/http/http_server.go b/deliveries/http/http_server.go index 4f1696d..96009b6 100644 --- a/deliveries/http/http_server.go +++ b/deliveries/http/http_server.go @@ -3,24 +3,24 @@ package http import ( "fmt" - domainhandler "github.com/dezh-tech/panda/deliveries/http/handlers/domain_handler" - domainService "github.com/dezh-tech/panda/services/domain" + handlers "github.com/dezh-tech/panda/deliveries/http/handlers/domain" + service "github.com/dezh-tech/panda/services/domain" "github.com/labstack/echo/v4" ) type Server struct { Router *echo.Echo config Config - handlers HttpHandlers + handlers Handlers } -func New(config Config, userSvc domainService.DomainService) Server { +func New(config Config, userSvc service.Domain) Server { return Server{ Router: echo.New(), config: config, - handlers: HttpHandlers{ - user: domainhandler.New(userSvc), + handlers: Handlers{ + domain: handlers.NewDomainService(userSvc), }, } } diff --git a/docs/docs.go b/docs/docs.go index 7bf8d31..bc122dd 100644 --- a/docs/docs.go +++ b/docs/docs.go @@ -25,6 +25,48 @@ const docTemplate = `{ "basePath": "{{.BasePath}}", "paths": { "/domains": { + "get": { + "description": "Get a list of all domains with their attributes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "domains" + ], + "summary": "Retrieve all domains", + "responses": { + "200": { + "description": "Domains retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/pkg.ResponseDto" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domainhandler.DomainGetResponse" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/pkg.ResponseDto" + } + } + } + }, "post": { "description": "Create a new domain with the specified attributes.", "consumes": [ @@ -119,6 +161,23 @@ const docTemplate = `{ "id": {} } }, + "domainhandler.DomainGetResponse": { + "type": "object", + "properties": { + "base_price_per_identifier": { + "type": "integer" + }, + "default_ttl": { + "type": "integer" + }, + "domain": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "pkg.ResponseDto": { "type": "object", "properties": { diff --git a/docs/swagger.json b/docs/swagger.json index f46b3be..6fedaf2 100644 --- a/docs/swagger.json +++ b/docs/swagger.json @@ -19,6 +19,48 @@ "basePath": "/", "paths": { "/domains": { + "get": { + "description": "Get a list of all domains with their attributes.", + "consumes": [ + "application/json" + ], + "produces": [ + "application/json" + ], + "tags": [ + "domains" + ], + "summary": "Retrieve all domains", + "responses": { + "200": { + "description": "Domains retrieved successfully", + "schema": { + "allOf": [ + { + "$ref": "#/definitions/pkg.ResponseDto" + }, + { + "type": "object", + "properties": { + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/domainhandler.DomainGetResponse" + } + } + } + } + ] + } + }, + "500": { + "description": "Internal Server Error", + "schema": { + "$ref": "#/definitions/pkg.ResponseDto" + } + } + } + }, "post": { "description": "Create a new domain with the specified attributes.", "consumes": [ @@ -113,6 +155,23 @@ "id": {} } }, + "domainhandler.DomainGetResponse": { + "type": "object", + "properties": { + "base_price_per_identifier": { + "type": "integer" + }, + "default_ttl": { + "type": "integer" + }, + "domain": { + "type": "string" + }, + "status": { + "type": "string" + } + } + }, "pkg.ResponseDto": { "type": "object", "properties": { diff --git a/docs/swagger.yaml b/docs/swagger.yaml index e4ccb6f..438f0e5 100644 --- a/docs/swagger.yaml +++ b/docs/swagger.yaml @@ -25,6 +25,17 @@ definitions: properties: id: {} type: object + domainhandler.DomainGetResponse: + properties: + base_price_per_identifier: + type: integer + default_ttl: + type: integer + domain: + type: string + status: + type: string + type: object pkg.ResponseDto: properties: data: {} @@ -67,6 +78,31 @@ info: version: "1.0" paths: /domains: + get: + consumes: + - application/json + description: Get a list of all domains with their attributes. + produces: + - application/json + responses: + "200": + description: Domains retrieved successfully + schema: + allOf: + - $ref: '#/definitions/pkg.ResponseDto' + - properties: + data: + items: + $ref: '#/definitions/domainhandler.DomainGetResponse' + type: array + type: object + "500": + description: Internal Server Error + schema: + $ref: '#/definitions/pkg.ResponseDto' + summary: Retrieve all domains + tags: + - domains post: consumes: - application/json diff --git a/documents/.keep b/documents/.keep deleted file mode 100644 index e69de29..0000000 diff --git a/infrastructures/grpc_client/gen/example.pb.go b/infrastructures/grpc_client/gen/example.pb.go index 0803a6a..83e0b5b 100644 --- a/infrastructures/grpc_client/gen/example.pb.go +++ b/infrastructures/grpc_client/gen/example.pb.go @@ -7,10 +7,11 @@ package grpc_client import ( - protoreflect "google.golang.org/protobuf/reflect/protoreflect" - protoimpl "google.golang.org/protobuf/runtime/protoimpl" reflect "reflect" sync "sync" + + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" ) const ( diff --git a/infrastructures/grpc_client/gen/example_grpc.pb.go b/infrastructures/grpc_client/gen/example_grpc.pb.go index d9a7a73..644dff4 100644 --- a/infrastructures/grpc_client/gen/example_grpc.pb.go +++ b/infrastructures/grpc_client/gen/example_grpc.pb.go @@ -8,6 +8,7 @@ package grpc_client import ( context "context" + grpc "google.golang.org/grpc" codes "google.golang.org/grpc/codes" status "google.golang.org/grpc/status" diff --git a/pkg/validator/validator.go b/pkg/validator/validator.go index c3155ac..258fbc7 100644 --- a/pkg/validator/validator.go +++ b/pkg/validator/validator.go @@ -1,6 +1,7 @@ package validator import ( + "errors" "sync" "github.com/go-playground/locales/en" @@ -29,7 +30,7 @@ func NewValidator() *Validator { if validateInstance == nil { validateInstance = &Validator{} - registerTranslations() + _ = registerTranslations() } } @@ -43,7 +44,8 @@ func (*Validator) Validate(s interface{}) []*ValidationError { return nil } - if validationErrors, ok := err.(validator.ValidationErrors); ok { + var validationErrors validator.ValidationErrors + if errors.As(err, &validationErrors) { return formatValidationErrors(validationErrors) } @@ -57,20 +59,26 @@ func (*Validator) Validate(s interface{}) []*ValidationError { } // registerTranslations adds translations for validation error messages. -func registerTranslations() { - en_translations.RegisterDefaultTranslations(validate, translator) +func registerTranslations() error { + err := en_translations.RegisterDefaultTranslations(validate, translator) + if err != nil { + return err + } + + return nil } // formatValidationErrors formats the validation errors for API responses. func formatValidationErrors(errs validator.ValidationErrors) []*ValidationError { - var errors []*ValidationError + es := make([]*ValidationError, 0) for _, err := range errs { - errors = append(errors, &ValidationError{ + es = append(es, &ValidationError{ Field: err.Field(), Message: err.Translate(translator), }) } - return errors + + return es } // ValidationError represents a single validation error. diff --git a/repositories/base_repository.go b/repositories/base_repository.go index 09df714..f4fff74 100644 --- a/repositories/base_repository.go +++ b/repositories/base_repository.go @@ -9,7 +9,7 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) -type BaseRepository struct { +type Base struct { Client *mongo.Client DBName string Collection string @@ -17,8 +17,8 @@ type BaseRepository struct { } // NewBaseRepository creates a new BaseRepository instance. -func NewBaseRepository(client *mongo.Client, dbName, collection string, timeout time.Duration) *BaseRepository { - return &BaseRepository{ +func NewBaseRepository(client *mongo.Client, dbName, collection string, timeout time.Duration) *Base { + return &Base{ Client: client, DBName: dbName, Collection: collection, @@ -27,7 +27,7 @@ func NewBaseRepository(client *mongo.Client, dbName, collection string, timeout } // InsertOne inserts a single document into the collection. -func (r *BaseRepository) InsertOne(ctx context.Context, document interface{}) (*mongo.InsertOneResult, error) { +func (r *Base) InsertOne(ctx context.Context, document interface{}) (*mongo.InsertOneResult, error) { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) @@ -37,7 +37,7 @@ func (r *BaseRepository) InsertOne(ctx context.Context, document interface{}) (* } // FindByField finds a single document by a specific field and value. -func (r *BaseRepository) FindByField(ctx context.Context, field string, value interface{}, result interface{}) error { +func (r *Base) FindByField(ctx context.Context, field string, value, result interface{}) error { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) @@ -48,11 +48,12 @@ func (r *BaseRepository) FindByField(ctx context.Context, field string, value in if errors.Is(err, mongo.ErrNoDocuments) { return nil } + return err } // FindOne finds a single document matching the filter. -func (r *BaseRepository) FindOne(ctx context.Context, filter interface{}, result interface{}) error { +func (r *Base) FindOne(ctx context.Context, filter, result interface{}) error { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) @@ -62,11 +63,12 @@ func (r *BaseRepository) FindOne(ctx context.Context, filter interface{}, result if errors.Is(err, mongo.ErrNoDocuments) { return nil } + return err } // FindAll finds all documents matching the filter. -func (r *BaseRepository) FindAll(ctx context.Context, filter interface{}, results interface{}) error { +func (r *Base) FindAll(ctx context.Context, filter, results interface{}) error { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) @@ -82,7 +84,7 @@ func (r *BaseRepository) FindAll(ctx context.Context, filter interface{}, result } // UpdateOne updates a single document matching the filter. -func (r *BaseRepository) UpdateOne(ctx context.Context, filter, update interface{}) (*mongo.UpdateResult, error) { +func (r *Base) UpdateOne(ctx context.Context, filter, update interface{}) (*mongo.UpdateResult, error) { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) @@ -92,7 +94,7 @@ func (r *BaseRepository) UpdateOne(ctx context.Context, filter, update interface } // DeleteOne deletes a single document matching the filter. -func (r *BaseRepository) DeleteOne(ctx context.Context, filter interface{}) (*mongo.DeleteResult, error) { +func (r *Base) DeleteOne(ctx context.Context, filter interface{}) (*mongo.DeleteResult, error) { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) @@ -102,7 +104,7 @@ func (r *BaseRepository) DeleteOne(ctx context.Context, filter interface{}) (*mo } // CountDocuments counts documents matching the filter. -func (r *BaseRepository) CountDocuments(ctx context.Context, filter interface{}) (int64, error) { +func (r *Base) CountDocuments(ctx context.Context, filter interface{}) (int64, error) { collection := r.Client.Database(r.DBName).Collection(r.Collection) ctx, cancel := context.WithTimeout(ctx, r.QueryTimeout) diff --git a/repositories/domain/domain_repository.go b/repositories/domain/domain_repository.go deleted file mode 100644 index 064296b..0000000 --- a/repositories/domain/domain_repository.go +++ /dev/null @@ -1,57 +0,0 @@ -package domain - -import ( - "context" - "time" - - "github.com/dezh-tech/panda/repositories" - schema "github.com/dezh-tech/panda/schemas" - "go.mongodb.org/mongo-driver/bson" - "go.mongodb.org/mongo-driver/mongo" -) - -type DomainRepository struct { - *repositories.BaseRepository -} - -func New(client *mongo.Client, dbName string, timeout time.Duration) *DomainRepository { - return &DomainRepository{ - BaseRepository: repositories.NewBaseRepository(client, dbName, schema.DOMAIN_SCHEMA_NAME, timeout), - } -} - -func (r *DomainRepository) Add(ctx context.Context, d schema.Domain) (*mongo.InsertOneResult, error) { - d.CreatedAt = time.Now() - d.UpdatedAt = time.Now() - - return r.InsertOne(ctx, d) -} - -func (r *DomainRepository) GetByField(ctx context.Context, fieldName string, value interface{}) (*schema.Domain, error) { - var result schema.Domain - err := r.FindOne(ctx, bson.M{fieldName: value}, &result) - if err != nil { - if err == mongo.ErrNoDocuments { - return nil, nil - } - return nil, err - } - return &result, nil -} - -func (r *DomainRepository) GetAll(ctx context.Context, filter interface{}) ([]schema.Domain, error) { - var results []schema.Domain - err := r.FindAll(ctx, filter, &results) - if err != nil { - return nil, err - } - return results, nil -} - -func (r *DomainRepository) Update(ctx context.Context, filter interface{}, update interface{}) (*mongo.UpdateResult, error) { - return r.UpdateOne(ctx, filter, update) -} - -func (r *DomainRepository) Delete(ctx context.Context, filter interface{}) (*mongo.DeleteResult, error) { - return r.DeleteOne(ctx, filter) -} diff --git a/repositories/domain_repository.go b/repositories/domain_repository.go new file mode 100644 index 0000000..33ab148 --- /dev/null +++ b/repositories/domain_repository.go @@ -0,0 +1,62 @@ +package repositories + +import ( + "context" + "errors" + "time" + + schema "github.com/dezh-tech/panda/schemas" + "go.mongodb.org/mongo-driver/bson" + "go.mongodb.org/mongo-driver/mongo" +) + +type Domain struct { + *Base +} + +func NewDomainRepository(client *mongo.Client, dbName string, timeout time.Duration) *Domain { + return &Domain{ + Base: NewBaseRepository(client, dbName, schema.DomainSchemaName, timeout), + } +} + +func (r *Domain) Add(ctx context.Context, d *schema.Domain) (*mongo.InsertOneResult, error) { + d.CreatedAt = time.Now() + d.UpdatedAt = time.Now() + + return r.InsertOne(ctx, d) +} + +func (r *Domain) GetByField(ctx context.Context, + fieldName string, value interface{}, +) (*schema.Domain, error) { + var result *schema.Domain + err := r.FindOne(ctx, bson.M{fieldName: value}, result) + if err != nil { + return nil, err + } + + return result, nil +} + +func (r *Domain) GetAll(ctx context.Context, filter interface{}) (*[]schema.Domain, error) { + results := new([]schema.Domain) + err := r.FindAll(ctx, filter, results) + if err != nil { + if errors.Is(err, mongo.ErrNoDocuments) { + return &[]schema.Domain{}, nil + } + + return nil, err + } + + return results, nil +} + +func (r *Domain) Update(ctx context.Context, filter, update interface{}) (*mongo.UpdateResult, error) { + return r.UpdateOne(ctx, filter, update) +} + +func (r *Domain) Delete(ctx context.Context, filter interface{}) (*mongo.DeleteResult, error) { + return r.DeleteOne(ctx, filter) +} diff --git a/schema.md b/schema.md index 7f7368d..9f273ec 100644 --- a/schema.md +++ b/schema.md @@ -1,108 +1,120 @@ -# **Panda NIP-05 Identifier System Database Schemas** - -This document outlines the MongoDB schemas for a system designed to sell and resolve NIP-05 identifiers. +# Panda Document Schema Diagram + +### **Schema: Users** +**Description**: Stores user credentials and metadata. +```json +{ + "_id": "ObjectId", + "name": "Users", + "npub": "string", // Nostr public key + "passwordHash": "string", // Hashed password for authentication + "createdAt": "ISODate", // User creation timestamp + "updatedAt": "ISODate" // Last updated timestamp +} +``` --- -## **1. Users Collection** - -Stores user information and credentials. - -### **Collection Name**: `Users` -### **Type**: User Management - -| **Field** | **Type** | **Constraints** | **Description** | -|-------------------|-------------|-----------------------------------------------|------------------------------------------------------| -| `_id` | ObjectId | Unique, Indexed | Unique identifier for the user. | -| `npub` | string | Required, Unique, Indexed | Nostr public key (e.g., npub...). | -| `passwordHash` | string | Required | Hashed password. | -| `createdAt` | ISODate | Required | Account creation timestamp. | -| `updatedAt` | ISODate | Required | Last updated timestamp. | +### **Schema: Domains** +**Description**: Defines domain settings, including pricing and TTL values. +```json +{ + "name": "Domains", + "_id": "ObjectId", + "domain": "string", // Domain name (e.g., "example.com") + "basePricePerIdentifier": "number", // Base cost per identifier (in sats) + "pricePerChar": "number", // Cost per character (in sats) + "defaultTTL": "number", // Default Time-to-Live for JSON cache + "createdAt": "ISODate", // Domain creation timestamp + "updatedAt": "ISODate" // Last updated timestamp +} +``` --- -## **2. Domains Collection** - -Defines the available domains for identifiers and their pricing. - -### **Collection Name**: `Domains` -### **Type**: Domain Management - -| **Field** | **Type** | **Constraints** | **Description** | -|-------------------------|-------------|-----------------------------------------------|------------------------------------------------------| -| `_id` | ObjectId | Unique, Indexed | Unique identifier for the domain. | -| `domain` | string | Required, Unique, Indexed | Domain name (e.g., "example.com"). | -| `basePricePerIdentifier` | number | Required | Cost per identifier (in sats). | -| `defaultTTL` | number | Optional | Default Time-to-Live for JSON cache (in seconds). | -| `status` | string | Required, Indexed | Status of the domain (e.g., "active", "inactive"). | -| `createdAt` | ISODate | Required | Creation timestamp. | -| `updatedAt` | ISODate | Required | Last updated timestamp. | +### **Schema: Identifiers** +**Description**: Tracks identifiers assigned to users within a domain. +```json +{ + "name": "Identifiers", + "_id": "ObjectId", + "name": "string", // Identifier name (e.g., "alice") + "domainId": "ObjectId", // Reference to Domains table + "fullIdentifier": "string", // Full identifier (e.g., "alice@example.com") + "userId": "ObjectId", // Reference to Users table + "expiresAt": "ISODate", // Subscription expiration date + "createdAt": "ISODate", // Identifier creation timestamp + "updatedAt": "ISODate" // Last updated timestamp +} +``` --- -## **3. Identifiers Collection** - -Represents individual NIP-05 identifiers. - -### **Collection Name**: `Identifiers` -### **Type**: Identifier Management - -| **Field** | **Type** | **Constraints** | **Description** | -|-------------------|-------------|-----------------------------------------------|------------------------------------------------------| -| `_id` | ObjectId | Unique, Indexed | Unique identifier for the NIP-05 identifier. | -| `name` | string | Required | Identifier name (e.g., "alice"). | -| `domainId` | ObjectId | Required, Indexed | Reference to the `Domains` collection. | -| `fullIdentifier` | string | Required, Unique, Indexed | Full identifier (e.g., "alice@example.com"). | -| `userId` | ObjectId | Required, Indexed | Reference to the `Users` collection. | -| `expiresAt` | ISODate | Required | Expiration date of the identifier. | -| `status` | string | Required, Indexed | Status of the identifier (e.g., "active", "inactive"). | -| `createdAt` | ISODate | Required | Creation timestamp. | -| `updatedAt` | ISODate | Required | Last updated timestamp. | +### **Schema: Transactions** +**Description**: Logs all transactions, including payments for identifiers or domains. +```json +{ + "name": "Transactions", + "_id": "ObjectId", + "userId": "ObjectId", // Reference to Users table + "domainId": "ObjectId", // Reference to Domains table + "identifierId": "ObjectId", // Reference to Identifiers table + "amount": "number", // Amount paid + "currency": "string", // e.g., "BTC", "USD" + "status": "string", // e.g., "completed", "pending" + "createdAt": "ISODate" // Transaction creation timestamp +} +``` --- -## **4. Transactions Collection** - -Tracks all financial transactions, including payments for identifiers or other services. - -### **Collection Name**: `Transactions` -### **Type**: Financial Management - -| **Field** | **Type** | **Constraints** | **Description** | -|----------------------|-------------|-----------------------------------------------|------------------------------------------------------| -| `_id` | ObjectId | Unique, Indexed | Unique identifier for the transaction. | -| `transactionId` | string | Required, Unique, Indexed | Unique ID for the transaction (e.g., UUID). | -| `userId` | ObjectId | Required, Indexed | Reference to the `Users` collection. | -| `domainId` | ObjectId | Optional, Indexed | Reference to the `Domains` collection. | -| `identifierId` | ObjectId | Optional, Indexed | Reference to the `Identifiers` collection. | -| `amount` | number | Required | Amount paid (e.g., in sats or fiat). | -| `currency` | string | Required | Currency type (e.g., "BTC", "USD"). | -| `type` | string | Required, Indexed | Transaction type (e.g., "purchase", "renewal"). | -| `paymentProcessor` | string | Optional | Payment processor used (e.g., "Stripe", "Bitcoin"). | -| `paymentDetails` | object | Optional | Additional details from the payment processor. | -| `paymentDetails.referenceId` | string | Optional, Indexed | Payment processor's reference ID. | -| `paymentDetails.method` | string | Optional | Payment method (e.g., "credit_card"). | -| `paymentDetails.confirmedAt` | ISODate | Optional | When the payment was confirmed (if applicable). | -| `paymentStatus` | string | Required, Indexed | Payment status (e.g., "completed", "pending"). | -| `status` | string | Required, Indexed | Overall transaction status (e.g., "active"). | -| `createdAt` | ISODate | Required | Transaction creation timestamp. | -| `updatedAt` | ISODate | Required | Last updated timestamp. | +### **Schema: Records** +**Description**: Stores resolution records (e.g., TXT, NOSTR, CNAME) for identifiers. +```json +{ + "name": "Records", + "_id": "ObjectId", + "identifierId": "ObjectId", // Reference to Identifiers table + "type": "string", // Record type (e.g., "NOSTR", "CNAME", "URL", "TXT") + "value": "string", // Record value (e.g., public key, alias, URL) + "priority": "number", // Sorting priority for multiple records + "ttl": "number", // Time-to-Live for the record in cache + "createdAt": "ISODate", // Record creation timestamp + "updatedAt": "ISODate" // Last updated timestamp +} +``` --- -## **5. ResolveRecords Collection** - -Stores resolution records for identifiers, similar to DNS records. +### **Schema: ReservedIdentifiers** +**Description**: Tracks reserved identifiers that cannot be registered by users and their associated pricing. +```json +{ + "name": "ReservedIdentifiers", + "_id": "ObjectId", + "fullIdentifier": "string", // Reserved identifier (e.g., "google@abc.com", "a@abc.com") + "reservedBy": "ObjectId", // Reference to Users table (optional) + "reason": "string", // Reason for reservation (e.g., "Trademark", "Premium") + "price": "number", // Price for the reserved identifier (in sats or specified currency) + "createdAt": "ISODate", // Reservation creation timestamp + "updatedAt": "ISODate" // Last updated timestamp +} +``` -### **Collection Name**: `ResolveRecords` -### **Type**: Resolution Management +--- -| **Field** | **Type** | **Constraints** | **Description** | -|-------------------|-------------|-----------------------------------------------|------------------------------------------------------| -| `_id` | ObjectId | Unique, Indexed | Unique identifier for the resolution record. | -| `identifierId` | ObjectId | Required, Indexed | Reference to the `Identifiers` collection. | -| `type` | string | Required, Indexed | Record type (e.g., "NOSTR", "CNAME", "URL", "TXT"). | -| `value` | string | Required | Record value (e.g., public key, alias, URL). | -| `priority` | number | Optional | Priority for sorting multiple records. | -| `createdAt` | ISODate | Required | Creation timestamp. | -| `updatedAt` | ISODate | Required | Last updated timestamp. | +### **Schema: Logs** +**Description**: Tracks various system events, including record changes, user logins, purchases, and more. +```json +{ + "name": "Logs", + "_id": "ObjectId", + "userId": "ObjectId", // Reference to Users table (optional, if applicable to a user) + "eventType": "string", // Type of event (e.g., "record_change", "login", "purchase") + "entityType": "string", // Affected entity type (e.g., "Records", "Users", "Identifiers") + "entityId": "ObjectId", // ID of the affected entity (optional) + "description": "string", // Detailed description of the event + "metadata": "object", // Additional metadata (e.g., changes made, IP address, amount, etc.) + "createdAt": "ISODate" // Log creation timestamp +} +``` \ No newline at end of file diff --git a/schemas/domain.go b/schemas/domain_schema.go similarity index 87% rename from schemas/domain.go rename to schemas/domain_schema.go index d53af09..2dedf57 100644 --- a/schemas/domain.go +++ b/schemas/domain_schema.go @@ -1,6 +1,6 @@ package schema -const DOMAIN_SCHEMA_NAME = "domains" +const DomainSchemaName = "domains" type Domain struct { Domain string `bson:"Domain"` diff --git a/services/domain/domain_create.go b/services/domain/domain_create.go index 2189245..2e673f1 100644 --- a/services/domain/domain_create.go +++ b/services/domain/domain_create.go @@ -1,4 +1,4 @@ -package domainService +package service import ( "context" @@ -7,37 +7,28 @@ import ( schema "github.com/dezh-tech/panda/schemas" ) -type DomainInsertArgs struct { - Domain string - BasePricePerIdentifier uint - DefaultTTL uint32 - Status string -} - -type DomainInsertRes struct { - ID interface{} -} - -func (s DomainService) Create(ctx context.Context, req DomainInsertArgs) (*DomainInsertRes, *validator.Varror) { +func (s Domain) Create(ctx context.Context, domain, status string, + basePricePerIdentifier uint, defaultTTL uint32, +) (interface{}, *validator.Varror) { // Check if the domain already exists - domain, err := s.repo.GetByField(ctx, "Domain", req.Domain) + d, err := s.repo.GetByField(ctx, "Domain", domain) if err != nil { return nil, &validator.Varror{Error: err.Error()} } - if domain != nil { + if d != nil { return nil, &validator.Varror{Error: ErrIsExist.Error()} } - id, err := s.repo.Add(ctx, schema.Domain{ - Domain: req.Domain, - BasePricePerIdentifier: req.BasePricePerIdentifier, - DefaultTTL: req.DefaultTTL, - Status: req.Status, + id, err := s.repo.Add(ctx, &schema.Domain{ + Domain: domain, + BasePricePerIdentifier: basePricePerIdentifier, + DefaultTTL: defaultTTL, + Status: status, }) if err != nil { return nil, &validator.Varror{Error: err.Error()} } - return &DomainInsertRes{ID: id.InsertedID}, nil + return id.InsertedID, nil } diff --git a/services/domain/domain_error.go b/services/domain/domain_error.go index 822746e..de2bb19 100644 --- a/services/domain/domain_error.go +++ b/services/domain/domain_error.go @@ -1,4 +1,4 @@ -package domainService +package service import "errors" diff --git a/services/domain/domain_get.go b/services/domain/domain_get.go new file mode 100644 index 0000000..3bf90d7 --- /dev/null +++ b/services/domain/domain_get.go @@ -0,0 +1,27 @@ +package service + +import ( + "context" + + "github.com/dezh-tech/panda/pkg/validator" + schema "github.com/dezh-tech/panda/schemas" + "go.mongodb.org/mongo-driver/bson" +) + +func (s Domain) GetAll(ctx context.Context, filter interface{}) (*[]schema.Domain, *validator.Varror) { + domains, err := s.repo.GetAll(ctx, filter) + if err != nil { + return nil, &validator.Varror{Error: err.Error()} + } + + return domains, nil +} + +func (s Domain) GetAllWithoutFilter(ctx context.Context) (*[]schema.Domain, *validator.Varror) { + domains, err := s.GetAll(ctx, bson.M{}) + if err != nil { + return nil, err + } + + return domains, nil +} diff --git a/services/domain/domain_service.go b/services/domain/domain_service.go index c615c17..d7c7943 100644 --- a/services/domain/domain_service.go +++ b/services/domain/domain_service.go @@ -1,4 +1,4 @@ -package domainService +package service import ( "context" @@ -8,19 +8,19 @@ import ( "go.mongodb.org/mongo-driver/mongo" ) -type Repository interface { - Add(ctx context.Context, entity schema.Domain) (*mongo.InsertOneResult, error) +type DomainRepository interface { + Add(ctx context.Context, schema *schema.Domain) (*mongo.InsertOneResult, error) GetByField(ctx context.Context, fieldName string, value interface{}) (*schema.Domain, error) - GetAll(ctx context.Context, filter interface{}) ([]schema.Domain, error) - Update(ctx context.Context, filter interface{}, update interface{}) (*mongo.UpdateResult, error) + GetAll(ctx context.Context, filter interface{}) (*[]schema.Domain, error) + Update(ctx context.Context, filter, update interface{}) (*mongo.UpdateResult, error) Delete(ctx context.Context, filter interface{}) (*mongo.DeleteResult, error) } -type DomainService struct { - repo Repository +type Domain struct { + repo DomainRepository validator *validator.Validator } -func New(repo Repository) DomainService { - return DomainService{repo: repo, validator: validator.NewValidator()} +func NewDomainService(repo DomainRepository) Domain { + return Domain{repo: repo, validator: validator.NewValidator()} } diff --git a/services/domain/domain_validator.go b/services/domain/domain_validator.go deleted file mode 100644 index 1dcee43..0000000 --- a/services/domain/domain_validator.go +++ /dev/null @@ -1,7 +0,0 @@ -package domainService - -type insertDomainValidation struct { - Domain string `validate:"required,url"` - BasePricePerIdentifier uint `validate:"required"` - Status string `validate:"oneof=ACTIVE INACTIVE"` -}