feat: custom content types backend
This commit is contained in:
1 parent
ed67b7e70d
commit
58d58f5a04
41 files changed
+4086
-2
No files matched your search
@@ -0,0 +1,153 @@
|
||||
package contenttype
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/response"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
domain "RenewCMS/internal/domain/contenttype"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
CreateUseCase *useCases.CreateContentTypeUseCase
|
||||
ListUseCase *useCases.ListContentTypesUseCase
|
||||
GetUseCase *useCases.GetContentTypeUseCase
|
||||
UpdateUseCase *useCases.UpdateContentTypeUseCase
|
||||
DeleteUseCase *useCases.DeleteContentTypeUseCase
|
||||
}
|
||||
|
||||
func NewHandler(create *useCases.CreateContentTypeUseCase, list *useCases.ListContentTypesUseCase, get *useCases.GetContentTypeUseCase, update *useCases.UpdateContentTypeUseCase, deleteUseCase *useCases.DeleteContentTypeUseCase) *Handler {
|
||||
return &Handler{CreateUseCase: create, ListUseCase: list, GetUseCase: get, UpdateUseCase: update, DeleteUseCase: deleteUseCase}
|
||||
}
|
||||
|
||||
type PostContentType struct {
|
||||
Key string `json:"key" validate:"required"`
|
||||
Name string `json:"name" validate:"required,max=100"`
|
||||
Fields []domain.Field `json:"fields"`
|
||||
}
|
||||
|
||||
type PutContentType struct {
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name" validate:"required,max=100"`
|
||||
Fields []domain.Field `json:"fields"`
|
||||
}
|
||||
|
||||
type contentTypeResponse struct {
|
||||
ID uint32 `json:"id"`
|
||||
Key string `json:"key"`
|
||||
Name string `json:"name"`
|
||||
Fields []domain.Field `json:"fields"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func toResponse(contentType domain.ContentType) contentTypeResponse {
|
||||
fields := make([]domain.Field, 0, len(contentType.Fields))
|
||||
for _, field := range contentType.Fields {
|
||||
if field.Options == nil {
|
||||
field.Options = []string{}
|
||||
}
|
||||
fields = append(fields, field)
|
||||
}
|
||||
return contentTypeResponse{
|
||||
ID: contentType.ID,
|
||||
Key: contentType.Key,
|
||||
Name: contentType.Name,
|
||||
Fields: fields,
|
||||
CreatedAt: contentType.CreatedAt,
|
||||
UpdatedAt: contentType.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
func writeContentTypeError(w http.ResponseWriter, err error) {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrContentTypeNotFound):
|
||||
http.Error(w, apperr.ContentTypeNotFoundErrorMessage, http.StatusNotFound)
|
||||
case errors.Is(err, domain.ErrKeyTaken):
|
||||
http.Error(w, apperr.ContentTypeKeyTakenErrorMessage, http.StatusConflict)
|
||||
case errors.Is(err, domain.ErrHasEntries):
|
||||
http.Error(w, apperr.ContentTypeHasEntriesErrorMessage, http.StatusConflict)
|
||||
case errors.Is(err, domain.ErrSchemaIncompatible):
|
||||
http.Error(w, apperr.SchemaIncompatibleErrorMessage, http.StatusConflict)
|
||||
case errors.Is(err, domain.ErrInvalidName):
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrInvalidKey):
|
||||
http.Error(w, apperr.InvalidContentTypeKeyErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrInvalidField):
|
||||
http.Error(w, apperr.InvalidFieldErrorMessage, http.StatusUnprocessableEntity)
|
||||
default:
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
||||
found, err := h.ListUseCase.List()
|
||||
if err != nil {
|
||||
writeContentTypeError(w, err)
|
||||
return
|
||||
}
|
||||
responses := make([]contentTypeResponse, 0, len(found))
|
||||
for _, contentType := range found {
|
||||
responses = append(responses, toResponse(contentType))
|
||||
}
|
||||
response.WriteJSON(w, http.StatusOK, responses)
|
||||
}
|
||||
|
||||
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
found, err := h.GetUseCase.Get(chi.URLParam(r, "key"))
|
||||
if err != nil {
|
||||
writeContentTypeError(w, err)
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, http.StatusOK, toResponse(found))
|
||||
}
|
||||
|
||||
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
var request PostContentType
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validator.Validate.Struct(request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
created, err := h.CreateUseCase.Create(useCases.CreateContentTypeCommand{Key: request.Key, Name: request.Name, Fields: request.Fields})
|
||||
if err != nil {
|
||||
writeContentTypeError(w, err)
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, http.StatusCreated, toResponse(created))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
var request PutContentType
|
||||
if err := json.NewDecoder(r.Body).Decode(&request); err != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := validator.Validate.Struct(request); err != nil {
|
||||
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
updated, err := h.UpdateUseCase.Update(useCases.UpdateContentTypeCommand{Key: chi.URLParam(r, "key"), Name: request.Name, Fields: request.Fields})
|
||||
if err != nil {
|
||||
writeContentTypeError(w, err)
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, http.StatusOK, toResponse(updated))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
if err := h.DeleteUseCase.Delete(chi.URLParam(r, "key")); err != nil {
|
||||
writeContentTypeError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
@@ -0,0 +1,232 @@
|
||||
package entry
|
||||
|
||||
import (
|
||||
"RenewCMS/api/pkg/apperr"
|
||||
"RenewCMS/api/pkg/claims"
|
||||
"RenewCMS/api/pkg/response"
|
||||
validator "RenewCMS/api/pkg/validator"
|
||||
"RenewCMS/internal/domain/contenttype"
|
||||
domain "RenewCMS/internal/domain/entry"
|
||||
"RenewCMS/internal/infrastructure/useCases"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/go-chi/chi/v5"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
CreateUseCase *useCases.CreateEntryUseCase
|
||||
GetUseCase *useCases.GetEntryUseCase
|
||||
ListUseCase *useCases.ListEntriesUseCase
|
||||
UpdateUseCase *useCases.UpdateEntryUseCase
|
||||
DeleteUseCase *useCases.DeleteEntryUseCase
|
||||
}
|
||||
|
||||
func NewHandler(create *useCases.CreateEntryUseCase, get *useCases.GetEntryUseCase, list *useCases.ListEntriesUseCase, update *useCases.UpdateEntryUseCase, deleteUseCase *useCases.DeleteEntryUseCase) *Handler {
|
||||
return &Handler{CreateUseCase: create, GetUseCase: get, ListUseCase: list, UpdateUseCase: update, DeleteUseCase: deleteUseCase}
|
||||
}
|
||||
|
||||
type EntryBody struct {
|
||||
Data map[string]any `json:"data" validate:"required"`
|
||||
}
|
||||
|
||||
type entryResponse struct {
|
||||
ID uint32 `json:"id"`
|
||||
ContentType string `json:"content_type"`
|
||||
Data map[string]any `json:"data"`
|
||||
CreatedBy uint32 `json:"created_by"`
|
||||
UpdatedBy uint32 `json:"updated_by"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
func toResponse(typed useCases.TypedEntry) entryResponse {
|
||||
return entryResponse{
|
||||
ID: typed.ID,
|
||||
ContentType: typed.ContentTypeKey,
|
||||
Data: typed.Data,
|
||||
CreatedBy: typed.CreatedBy,
|
||||
UpdatedBy: typed.UpdatedBy,
|
||||
CreatedAt: typed.CreatedAt,
|
||||
UpdatedAt: typed.UpdatedAt,
|
||||
}
|
||||
}
|
||||
|
||||
type pageResponse struct {
|
||||
Items []entryResponse `json:"items"`
|
||||
Total int `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Size int `json:"size"`
|
||||
}
|
||||
|
||||
const (
|
||||
defaultListPage = 1
|
||||
defaultListSize = 25
|
||||
)
|
||||
|
||||
type listQuery struct {
|
||||
Page int `validate:"gte=1"`
|
||||
Size int `validate:"oneof=5 25 50 100"`
|
||||
Sort domain.Sort
|
||||
}
|
||||
|
||||
func intQueryParam(values url.Values, name string, fallback int) (int, error) {
|
||||
if !values.Has(name) {
|
||||
return fallback, nil
|
||||
}
|
||||
return strconv.Atoi(values.Get(name))
|
||||
}
|
||||
|
||||
func readListQuery(values url.Values) (listQuery, error) {
|
||||
page, err := intQueryParam(values, "page", defaultListPage)
|
||||
if err != nil {
|
||||
return listQuery{}, err
|
||||
}
|
||||
size, err := intQueryParam(values, "size", defaultListSize)
|
||||
if err != nil {
|
||||
return listQuery{}, err
|
||||
}
|
||||
order := domain.DefaultSort()
|
||||
if values.Has("sort") {
|
||||
order, err = domain.ParseSort(values.Get("sort"))
|
||||
if err != nil {
|
||||
return listQuery{}, err
|
||||
}
|
||||
}
|
||||
return listQuery{Page: page, Size: size, Sort: order}, nil
|
||||
}
|
||||
|
||||
func parseEntryId(r *http.Request) (uint32, error) {
|
||||
id, err := strconv.ParseUint(chi.URLParam(r, "id"), 10, 32)
|
||||
return uint32(id), err
|
||||
}
|
||||
|
||||
func writeEntryError(w http.ResponseWriter, err error) {
|
||||
var fieldError domain.FieldError
|
||||
switch {
|
||||
case errors.Is(err, contenttype.ErrContentTypeNotFound):
|
||||
http.Error(w, apperr.ContentTypeNotFoundErrorMessage, http.StatusNotFound)
|
||||
case errors.Is(err, domain.ErrEntryNotFound):
|
||||
http.Error(w, apperr.EntryNotFoundErrorMessage, http.StatusNotFound)
|
||||
case errors.As(err, &fieldError):
|
||||
http.Error(w, apperr.InvalidEntryErrorMessage+": "+fieldError.Error(), fieldErrorStatus(err))
|
||||
case errors.Is(err, domain.ErrInvalidPage), errors.Is(err, domain.ErrInvalidSort):
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
case errors.Is(err, domain.ErrInvalidEntry):
|
||||
http.Error(w, apperr.InvalidEntryErrorMessage, http.StatusUnprocessableEntity)
|
||||
default:
|
||||
http.Error(w, "Internal server error", http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
func fieldErrorStatus(err error) int {
|
||||
if errors.Is(err, domain.ErrUniqueViolation) {
|
||||
return http.StatusConflict
|
||||
}
|
||||
return http.StatusUnprocessableEntity
|
||||
}
|
||||
|
||||
func readBody(w http.ResponseWriter, r *http.Request) (EntryBody, bool) {
|
||||
var body EntryBody
|
||||
if err := json.NewDecoder(r.Body).Decode(&body); err != nil || validator.Validate.Struct(body) != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return EntryBody{}, false
|
||||
}
|
||||
return body, true
|
||||
}
|
||||
|
||||
func currentUserId(w http.ResponseWriter, r *http.Request) (uint32, bool) {
|
||||
userId, ok := claims.UserId(r)
|
||||
if !ok {
|
||||
http.Error(w, apperr.UnauthorizedErrorMessage, http.StatusUnauthorized)
|
||||
}
|
||||
return userId, ok
|
||||
}
|
||||
|
||||
func (h *Handler) List(w http.ResponseWriter, r *http.Request) {
|
||||
query, err := readListQuery(r.URL.Query())
|
||||
if err != nil || validator.Validate.Struct(query) != nil {
|
||||
http.Error(w, apperr.BodyErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
page, err := h.ListUseCase.ListPage(useCases.ListEntriesCommand{ContentTypeKey: chi.URLParam(r, "key"), Page: query.Page, Size: query.Size, Sort: query.Sort})
|
||||
if err != nil {
|
||||
writeEntryError(w, err)
|
||||
return
|
||||
}
|
||||
items := make([]entryResponse, 0, len(page.Items))
|
||||
for _, typed := range page.Items {
|
||||
items = append(items, toResponse(typed))
|
||||
}
|
||||
response.WriteJSON(w, http.StatusOK, pageResponse{Items: items, Total: page.Total, Page: page.Page, Size: page.Size})
|
||||
}
|
||||
|
||||
func (h *Handler) Get(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseEntryId(r)
|
||||
if err != nil {
|
||||
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
found, err := h.GetUseCase.Get(chi.URLParam(r, "key"), id)
|
||||
if err != nil {
|
||||
writeEntryError(w, err)
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, http.StatusOK, toResponse(found))
|
||||
}
|
||||
|
||||
func (h *Handler) Create(w http.ResponseWriter, r *http.Request) {
|
||||
userId, ok := currentUserId(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
body, ok := readBody(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
created, err := h.CreateUseCase.Create(useCases.CreateEntryCommand{ContentTypeKey: chi.URLParam(r, "key"), Data: body.Data, UserID: userId})
|
||||
if err != nil {
|
||||
writeEntryError(w, err)
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, http.StatusCreated, toResponse(created))
|
||||
}
|
||||
|
||||
func (h *Handler) Update(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseEntryId(r)
|
||||
if err != nil {
|
||||
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
userId, ok := currentUserId(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
body, ok := readBody(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
updated, err := h.UpdateUseCase.Update(useCases.UpdateEntryCommand{ContentTypeKey: chi.URLParam(r, "key"), ID: id, Data: body.Data, UserID: userId})
|
||||
if err != nil {
|
||||
writeEntryError(w, err)
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, http.StatusOK, toResponse(updated))
|
||||
}
|
||||
|
||||
func (h *Handler) Delete(w http.ResponseWriter, r *http.Request) {
|
||||
id, err := parseEntryId(r)
|
||||
if err != nil {
|
||||
http.Error(w, apperr.IdUint32ErrorMessage, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if err := h.DeleteUseCase.Delete(chi.URLParam(r, "key"), id); err != nil {
|
||||
writeEntryError(w, err)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
}
|
||||
Reference in new issue
Block a user