Implement namespace CRUD + various small fixes

Other fixes and improvements:
 - add parseISODateWithErr and parseISODatePtrWithErr handlers for incoming data
 - add service & repository errors
 - cleanup old (unbound) attachment controllers from router
 - fix system repository error prefix (auth => system)
This commit is contained in:
Denis Arh
2019-04-29 18:51:18 +02:00
parent 010a1351ea
commit 5bfbab6a4e
24 changed files with 2222 additions and 59 deletions
File diff suppressed because one or more lines are too long
@@ -0,0 +1,14 @@
CREATE TABLE `compose_namespace` (
`id` BIGINT(20) UNSIGNED NOT NULL,
`name` VARCHAR(64) NOT NULL COMMENT 'Name',
`slug` VARCHAR(64) NOT NULL COMMENT 'URL slug',
`enabled` BOOLEAN NOT NULL COMMENT 'Is namespace enabled?',
`meta` JSON NOT NULL COMMENT 'Meta data',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME DEFAULT NULL,
`deleted_at` DATETIME DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
+25
View File
@@ -0,0 +1,25 @@
package repository
import (
"github.com/pkg/errors"
)
type (
repositoryError string
)
const (
ErrNotImplemented = repositoryError("NotImplemented")
)
func (e repositoryError) Error() string {
return e.String()
}
func (e repositoryError) String() string {
return "crust.compose.repository." + string(e)
}
func (e repositoryError) new() error {
return errors.WithStack(e)
}
+168
View File
@@ -0,0 +1,168 @@
package repository
import (
"context"
"time"
"github.com/titpetric/factory"
"gopkg.in/Masterminds/squirrel.v1"
"github.com/crusttech/crust/compose/types"
)
type (
NamespaceRepository interface {
With(ctx context.Context, db *factory.DB) NamespaceRepository
FindByID(id uint64) (*types.Namespace, error)
Find(filter types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error)
Create(mod *types.Namespace) (*types.Namespace, error)
Update(mod *types.Namespace) (*types.Namespace, error)
DeleteByID(id uint64) error
}
namespace struct {
*repository
}
)
const (
ErrNamespaceNotFound = repositoryError("NamespaceNotFound")
)
func Namespace(ctx context.Context, db *factory.DB) NamespaceRepository {
return (&namespace{}).With(ctx, db)
}
func (r namespace) table() string {
return "compose_namespace"
}
func (r namespace) columns() []string {
return []string{
"id",
"name",
"slug",
"enabled",
"meta",
"created_at",
"updated_at",
"deleted_at",
}
}
func (r *namespace) With(ctx context.Context, db *factory.DB) NamespaceRepository {
return &namespace{
repository: r.repository.With(ctx, db),
}
}
func (r *namespace) FindByID(id uint64) (n *types.Namespace, err error) {
var (
sql string
args []interface{}
)
n = &types.Namespace{}
qb := r.query().
Columns(r.columns()...).
Where("id = ?", id)
if sql, args, err = qb.ToSql(); err != nil {
return
}
if err = r.db().Get(n, sql, args...); err != nil {
return
}
if n == nil || n.ID == 0 {
return nil, ErrNamespaceNotFound
}
return
}
func (r *namespace) Find(filter types.NamespaceFilter) (nn types.NamespaceSet, f types.NamespaceFilter, err error) {
var (
sql string
args []interface{}
)
f = filter
if f.PerPage > 100 {
f.PerPage = 100
} else if f.PerPage == 0 {
f.PerPage = 50
}
qb := r.query()
if f.Query != "" {
q := "%" + f.Query + "%"
qb = qb.Where("name like ? OR slug like ?", q, q)
}
{
cq := qb.Column(squirrel.Alias(squirrel.Expr("COUNT(*)"), "count"))
if sql, args, err = cq.ToSql(); err != nil {
return
}
if err = r.db().Get(&f.Count, sql, args...); err != nil {
return
}
if f.Count == 0 {
// No rows with this filter no need to continue
return
}
}
if f.Page > 0 {
qb = qb.Offset(uint64(f.PerPage * f.Page))
}
qb = qb.
Limit(uint64(f.PerPage)).
Columns(r.columns()...).
OrderBy("id ASC")
if sql, args, err = qb.ToSql(); err != nil {
return
}
if err = r.db().Select(&nn, sql, args...); err != nil {
return
}
return
}
func (r namespace) query() squirrel.SelectBuilder {
return squirrel.
Select().
From(r.table()).
Where("deleted_at IS NULL")
}
func (r *namespace) Create(mod *types.Namespace) (*types.Namespace, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
return mod, r.db().Insert(r.table(), mod)
}
func (r *namespace) Update(mod *types.Namespace) (*types.Namespace, error) {
now := time.Now()
mod.UpdatedAt = &now
return mod, r.db().Replace(r.table(), mod)
}
func (r *namespace) DeleteByID(id uint64) error {
_, err := r.db().Exec("DELETE FROM compose_namespace WHERE id=?", id)
return err
}
+31
View File
@@ -0,0 +1,31 @@
package service
import (
"github.com/pkg/errors"
)
type (
serviceError string
)
const (
ErrInvalidID serviceError = "InvalidID"
ErrStaleData serviceError = "StaleData"
ErrNoCreatePermissions serviceError = "NoCreatePermissions"
ErrNoReadPermissions serviceError = "NoReadPermissions"
ErrNoUpdatePermissions serviceError = "NoUpdatePermissions"
ErrNoDeletePermissions serviceError = "NoDeletePermissions"
ErrNotImplemented serviceError = "NotImplemented"
)
func (e serviceError) Error() string {
return e.String()
}
func (e serviceError) String() string {
return "crust.compose.service." + string(e)
}
func (e serviceError) withStack() error {
return errors.WithStack(e)
}
+132
View File
@@ -0,0 +1,132 @@
package service
import (
"context"
"time"
"github.com/titpetric/factory"
"github.com/crusttech/crust/compose/internal/repository"
"github.com/crusttech/crust/compose/types"
)
type (
namespace struct {
db *factory.DB
ctx context.Context
prmSvc PermissionsService
namespaceRepo repository.NamespaceRepository
}
NamespaceService interface {
With(ctx context.Context) NamespaceService
FindByID(namespaceID uint64) (*types.Namespace, error)
Find(types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error)
Create(namespace *types.Namespace) (*types.Namespace, error)
Update(namespace *types.Namespace) (*types.Namespace, error)
DeleteByID(namespaceID uint64) error
}
)
func Namespace() NamespaceService {
return (&namespace{
prmSvc: DefaultPermissions,
}).With(context.Background())
}
func (svc *namespace) With(ctx context.Context) NamespaceService {
db := repository.DB(ctx)
return &namespace{
db: db,
ctx: ctx,
prmSvc: svc.prmSvc.With(ctx),
namespaceRepo: repository.Namespace(ctx, db),
}
}
func (svc *namespace) FindByID(ID uint64) (n *types.Namespace, err error) {
if ID == 0 {
return nil, ErrInvalidID.withStack()
}
if n, err = svc.namespaceRepo.FindByID(ID); err != nil {
return
} else if !svc.prmSvc.CanReadNamespace(n) {
return nil, ErrNoReadPermissions.withStack()
}
return
}
func (svc *namespace) Find(filter types.NamespaceFilter) (types.NamespaceSet, types.NamespaceFilter, error) {
nn, f, err := svc.namespaceRepo.Find(filter)
if err != nil {
return nil, f, err
}
nn, _ = nn.Filter(func(m *types.Namespace) (bool, error) {
return svc.prmSvc.CanReadNamespace(m), nil
})
return nn, f, nil
}
func (svc *namespace) Create(mod *types.Namespace) (*types.Namespace, error) {
if !svc.prmSvc.CanCreateNamespace() {
return nil, ErrNoCreatePermissions.withStack()
}
return svc.namespaceRepo.Create(mod)
}
func (svc *namespace) Update(updated *types.Namespace) (m *types.Namespace, err error) {
m, err = svc.FindByID(updated.ID)
if err != nil {
return nil, err
}
if isStale(updated.UpdatedAt, m.UpdatedAt, m.CreatedAt) {
return nil, ErrStaleData
}
if !svc.prmSvc.CanUpdateNamespace(m) {
return nil, ErrNoUpdatePermissions.withStack()
}
m.Name = updated.Name
m.Slug = updated.Slug
m.Meta = updated.Meta
m.Enabled = updated.Enabled
return svc.namespaceRepo.Update(m)
}
func (svc *namespace) DeleteByID(ID uint64) error {
if m, err := svc.namespaceRepo.FindByID(ID); err != nil {
return err
} else if !svc.prmSvc.CanDeleteNamespace(m) {
return ErrNoDeletePermissions.withStack()
}
return svc.namespaceRepo.DeleteByID(ID)
}
// Data is stale when new date does not match updatedAt or createdAt (before first update)
func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool {
if new == nil {
return false
}
if updatedAt != nil {
return !new.Equal(*updatedAt)
}
return new.Equal(createdAt)
}
+26 -6
View File
@@ -27,7 +27,10 @@ type (
Effective() (ee []effectivePermission, err error)
CanAccess() bool
CanCreateNamspace() bool
CanCreateNamespace() bool
CanReadNamespace(r permissionResource) bool
CanUpdateNamespace(r permissionResource) bool
CanDeleteNamespace(r permissionResource) bool
CanCreateModule(r permissionResource) bool
CanReadModule(r permissionResource) bool
CanUpdateModule(r permissionResource) bool
@@ -95,7 +98,7 @@ func (p *permissions) Effective() (ee []effectivePermission, err error) {
ee = append(ee, ep("compose", "access", p.CanAccess()))
ee = append(ee, ep("compose", "grant", p.CanGrant()))
ee = append(ee, ep("compose", "namespace.create", p.CanCreateNamspace()))
ee = append(ee, ep("compose", "namespace.create", p.CanCreateNamespace()))
ee = append(ee, ep("compose:namespace:crm", "module.create", p.CanCreateModule(crmNamespace())))
ee = append(ee, ep("compose:namespace:crm", "chart.create", p.CanCreateChart(crmNamespace())))
@@ -113,13 +116,24 @@ func (p *permissions) CanGrant() bool {
return p.checkAccess(types.PermissionResource, "grant")
}
func (p *permissions) CanCreateNamspace() bool {
func (p *permissions) CanCreateNamespace() bool {
return p.checkAccess(types.PermissionResource, "namespace.create")
}
func (p *permissions) CanCreateModule(ns permissionResource) bool {
// @todo move to func args when namespaces are implemented
return p.checkAccess(ns, "module.create")
func (p *permissions) CanReadNamespace(r permissionResource) bool {
return p.checkAccess(r, "read", p.allow())
}
func (p *permissions) CanUpdateNamespace(r permissionResource) bool {
return p.checkAccess(r, "update")
}
func (p *permissions) CanDeleteNamespace(r permissionResource) bool {
return p.checkAccess(r, "delete")
}
func (p *permissions) CanCreateModule(r permissionResource) bool {
return p.checkAccess(r, "module.create")
}
func (p *permissions) CanReadModule(r permissionResource) bool {
@@ -206,3 +220,9 @@ func (p *permissions) checkAccess(resource permissionResource, operation string,
}
return false
}
func (p permissions) allow() func() internalRules.Access {
return func() internalRules.Access {
return internalRules.Allow
}
}
+2
View File
@@ -19,6 +19,7 @@ var (
DefaultNotification NotificationService
DefaultPermissions PermissionsService
DefaultAttachment AttachmentService
DefaultNamespace NamespaceService
)
func Init() error {
@@ -35,6 +36,7 @@ func Init() error {
DefaultChart = Chart()
DefaultNotification = Notification()
DefaultAttachment = Attachment(fs)
DefaultNamespace = Namespace()
return nil
}
-26
View File
@@ -30,12 +30,6 @@ type (
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
}
file struct {
*types.Attachment
content io.ReadSeeker
download bool
}
Attachment struct {
attachment service.AttachmentService
}
@@ -176,23 +170,3 @@ func makeAttachmentPayload(a *types.Attachment, userID uint64) *attachmentPayloa
UpdatedAt: a.UpdatedAt,
}
}
func (f *file) Download() bool {
return f.download
}
func (f *file) Name() string {
return f.Attachment.Name
}
func (f *file) ModTime() time.Time {
return f.Attachment.CreatedAt
}
func (f *file) Content() io.ReadSeeker {
return f.content
}
func (f *file) Valid() bool {
return f.content != nil
}
+161
View File
@@ -0,0 +1,161 @@
package handlers
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `namespace.go`, `namespace.util.go` or `namespace_test.go` to
implement your API calls, helper functions and tests. The file `namespace.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"context"
"net/http"
"github.com/go-chi/chi"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/compose/rest/request"
)
// Internal API interface
type NamespaceAPI interface {
List(context.Context, *request.NamespaceList) (interface{}, error)
Create(context.Context, *request.NamespaceCreate) (interface{}, error)
Read(context.Context, *request.NamespaceRead) (interface{}, error)
Update(context.Context, *request.NamespaceUpdate) (interface{}, error)
Delete(context.Context, *request.NamespaceDelete) (interface{}, error)
}
// HTTP API interface
type Namespace struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
}
func NewNamespace(nh NamespaceAPI) *Namespace {
return &Namespace{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceList()
if err := params.Fill(r); err != nil {
resputil.JSON(w, err)
return
}
if value, err := nh.List(r.Context(), params); err != nil {
resputil.JSON(w, err)
return
} else {
switch fn := value.(type) {
case func(http.ResponseWriter, *http.Request):
fn(w, r)
return
}
resputil.JSON(w, value)
return
}
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceCreate()
if err := params.Fill(r); err != nil {
resputil.JSON(w, err)
return
}
if value, err := nh.Create(r.Context(), params); err != nil {
resputil.JSON(w, err)
return
} else {
switch fn := value.(type) {
case func(http.ResponseWriter, *http.Request):
fn(w, r)
return
}
resputil.JSON(w, value)
return
}
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceRead()
if err := params.Fill(r); err != nil {
resputil.JSON(w, err)
return
}
if value, err := nh.Read(r.Context(), params); err != nil {
resputil.JSON(w, err)
return
} else {
switch fn := value.(type) {
case func(http.ResponseWriter, *http.Request):
fn(w, r)
return
}
resputil.JSON(w, value)
return
}
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceUpdate()
if err := params.Fill(r); err != nil {
resputil.JSON(w, err)
return
}
if value, err := nh.Update(r.Context(), params); err != nil {
resputil.JSON(w, err)
return
} else {
switch fn := value.(type) {
case func(http.ResponseWriter, *http.Request):
fn(w, r)
return
}
resputil.JSON(w, value)
return
}
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewNamespaceDelete()
if err := params.Fill(r); err != nil {
resputil.JSON(w, err)
return
}
if value, err := nh.Delete(r.Context(), params); err != nil {
resputil.JSON(w, err)
return
} else {
switch fn := value.(type) {
case func(http.ResponseWriter, *http.Request):
fn(w, r)
return
}
resputil.JSON(w, value)
return
}
},
}
}
func (nh *Namespace) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/namespace/", nh.List)
r.Post("/namespace/", nh.Create)
r.Get("/namespace/{namespaceID}", nh.Read)
r.Post("/namespace/{namespaceID}", nh.Update)
r.Delete("/namespace/{namespaceID}", nh.Delete)
})
}
+134
View File
@@ -0,0 +1,134 @@
package rest
import (
"context"
"github.com/titpetric/factory/resputil"
"github.com/crusttech/crust/compose/internal/service"
"github.com/crusttech/crust/compose/rest/request"
"github.com/crusttech/crust/compose/types"
)
type (
namespacePayload struct {
*types.Namespace
CanUpdateNamespace bool `json:"canUpdateNamespace"`
CanDeleteNamespace bool `json:"canDeleteNamespace"`
CanCreateModule bool `json:"canCreateModule"`
CanCreateChart bool `json:"canCreateChart"`
CanCreateTrigger bool `json:"canCreateTrigger"`
CanCreatePage bool `json:"canCreatePage"`
}
namespaceSetPayload struct {
Filter types.NamespaceFilter `json:"filter"`
Set []*namespacePayload `json:"set"`
}
)
type Namespace struct {
namespace service.NamespaceService
permissions service.PermissionsService
}
func (Namespace) New() *Namespace {
return &Namespace{
namespace: service.DefaultNamespace,
permissions: service.DefaultPermissions,
}
}
func (ctrl Namespace) List(ctx context.Context, r *request.NamespaceList) (interface{}, error) {
f := types.NamespaceFilter{
Query: r.Query,
PerPage: r.PerPage,
Page: r.Page,
}
nn, filter, err := ctrl.namespace.With(ctx).Find(f)
return ctrl.makeFilterPayload(ctx, nn, filter, err)
}
func (ctrl Namespace) Create(ctx context.Context, r *request.NamespaceCreate) (interface{}, error) {
var err error
ns := &types.Namespace{
Name: r.Name,
Slug: r.Slug,
Meta: r.Meta,
Enabled: r.Enabled,
}
ns, err = ctrl.namespace.With(ctx).Create(ns)
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) Read(ctx context.Context, r *request.NamespaceRead) (interface{}, error) {
ns, err := ctrl.namespace.With(ctx).FindByID(r.NamespaceID)
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) Update(ctx context.Context, r *request.NamespaceUpdate) (interface{}, error) {
var (
ns = &types.Namespace{}
err error
)
ns.ID = r.NamespaceID
ns.Name = r.Name
ns.Slug = r.Slug
ns.Meta = r.Meta
ns.Enabled = r.Enabled
ns.UpdatedAt = r.UpdatedAt
ns, err = ctrl.namespace.With(ctx).Update(ns)
return ctrl.makePayload(ctx, ns, err)
}
func (ctrl Namespace) Delete(ctx context.Context, r *request.NamespaceDelete) (interface{}, error) {
_, err := ctrl.namespace.With(ctx).FindByID(r.NamespaceID)
if err != nil {
return nil, err
}
err = ctrl.namespace.With(ctx).DeleteByID(r.NamespaceID)
if err != nil {
return nil, err
} else {
return resputil.Success("deleted"), nil
}
}
func (ctrl Namespace) makePayload(ctx context.Context, ns *types.Namespace, err error) (*namespacePayload, error) {
if err != nil {
return nil, err
}
perm := ctrl.permissions.With(ctx)
return &namespacePayload{
Namespace: ns,
CanUpdateNamespace: perm.CanUpdateNamespace(ns),
CanDeleteNamespace: perm.CanDeleteNamespace(ns),
CanCreateModule: perm.CanCreateModule(ns),
CanCreateChart: perm.CanCreateChart(ns),
CanCreateTrigger: perm.CanCreateTrigger(ns),
CanCreatePage: perm.CanCreatePage(ns),
}, nil
}
func (ctrl Namespace) makeFilterPayload(ctx context.Context, nn types.NamespaceSet, f types.NamespaceFilter, err error) (*namespaceSetPayload, error) {
if err != nil {
return nil, err
}
nsp := &namespaceSetPayload{Filter: f, Set: make([]*namespacePayload, len(nn))}
for i := range nn {
nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return nsp, nil
}
+311
View File
@@ -0,0 +1,311 @@
package request
/*
Hello! This file is auto-generated from `docs/src/spec.json`.
For development:
In order to update the generated files, edit this file under the location,
add your struct fields, imports, API definitions and whatever you want, and:
1. run [spec](https://github.com/titpetric/spec) in the same folder,
2. run `./_gen.php` in this folder.
You may edit `namespace.go`, `namespace.util.go` or `namespace_test.go` to
implement your API calls, helper functions and tests. The file `namespace.go`
is only generated the first time, and will not be overwritten if it exists.
*/
import (
"io"
"strings"
"encoding/json"
"mime/multipart"
"net/http"
"github.com/go-chi/chi"
"github.com/pkg/errors"
sqlxTypes "github.com/jmoiron/sqlx/types"
"time"
)
var _ = chi.URLParam
var _ = multipart.FileHeader{}
// Namespace list request parameters
type NamespaceList struct {
Query string
Page uint
PerPage uint
}
func NewNamespaceList() *NamespaceList {
return &NamespaceList{}
}
func (nReq *NamespaceList) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(nReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := get["query"]; ok {
nReq.Query = val
}
if val, ok := get["page"]; ok {
nReq.Page = parseUint(val)
}
if val, ok := get["perPage"]; ok {
nReq.PerPage = parseUint(val)
}
return err
}
var _ RequestFiller = NewNamespaceList()
// Namespace create request parameters
type NamespaceCreate struct {
Name string
Slug string
Enabled bool
Meta sqlxTypes.JSONText
}
func NewNamespaceCreate() *NamespaceCreate {
return &NamespaceCreate{}
}
func (nReq *NamespaceCreate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(nReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
if val, ok := post["name"]; ok {
nReq.Name = val
}
if val, ok := post["slug"]; ok {
nReq.Slug = val
}
if val, ok := post["enabled"]; ok {
nReq.Enabled = parseBool(val)
}
if val, ok := post["meta"]; ok {
if nReq.Meta, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
return err
}
var _ RequestFiller = NewNamespaceCreate()
// Namespace read request parameters
type NamespaceRead struct {
NamespaceID uint64 `json:",string"`
}
func NewNamespaceRead() *NamespaceRead {
return &NamespaceRead{}
}
func (nReq *NamespaceRead) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(nReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
nReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID"))
return err
}
var _ RequestFiller = NewNamespaceRead()
// Namespace update request parameters
type NamespaceUpdate struct {
NamespaceID uint64 `json:",string"`
Name string
Slug string
Enabled bool
Meta sqlxTypes.JSONText
UpdatedAt *time.Time
}
func NewNamespaceUpdate() *NamespaceUpdate {
return &NamespaceUpdate{}
}
func (nReq *NamespaceUpdate) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(nReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
nReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID"))
if val, ok := post["name"]; ok {
nReq.Name = val
}
if val, ok := post["slug"]; ok {
nReq.Slug = val
}
if val, ok := post["enabled"]; ok {
nReq.Enabled = parseBool(val)
}
if val, ok := post["meta"]; ok {
if nReq.Meta, err = parseJSONTextWithErr(val); err != nil {
return err
}
}
if val, ok := post["updatedAt"]; ok {
if nReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil {
return err
}
}
return err
}
var _ RequestFiller = NewNamespaceUpdate()
// Namespace delete request parameters
type NamespaceDelete struct {
NamespaceID uint64 `json:",string"`
}
func NewNamespaceDelete() *NamespaceDelete {
return &NamespaceDelete{}
}
func (nReq *NamespaceDelete) Fill(r *http.Request) (err error) {
if strings.ToLower(r.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(r.Body).Decode(nReq)
switch {
case err == io.EOF:
err = nil
case err != nil:
return errors.Wrap(err, "error parsing http request body")
}
}
if err = r.ParseForm(); err != nil {
return err
}
get := map[string]string{}
post := map[string]string{}
urlQuery := r.URL.Query()
for name, param := range urlQuery {
get[name] = string(param[0])
}
postVars := r.Form
for name, param := range postVars {
post[name] = string(param[0])
}
nReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID"))
return err
}
var _ RequestFiller = NewNamespaceDelete()
+14
View File
@@ -4,6 +4,7 @@ import (
"regexp"
"strconv"
"strings"
"time"
"github.com/jmoiron/sqlx/types"
"github.com/pkg/errors"
@@ -17,6 +18,19 @@ func parseJSONTextWithErr(s string) (types.JSONText, error) {
return *result, err
}
func parseISODateWithErr(s string) (time.Time, error) {
return time.Parse(time.RFC3339, s)
}
func parseISODatePtrWithErr(s string) (*time.Time, error) {
t, err := parseISODateWithErr(s)
if err != nil {
return nil, err
}
return &t, nil
}
// parseInt parses a string to int
func parseInt(s string) int {
if s == "" {
+2 -2
View File
@@ -9,6 +9,7 @@ import (
func MountRoutes() func(chi.Router) {
var (
namespace = Namespace{}.New()
module = Module{}.New()
record = Record{}.New()
page = Page{}.New()
@@ -16,8 +17,6 @@ func MountRoutes() func(chi.Router) {
trigger = Trigger{}.New()
notification = Notification{}.New()
attachment = Attachment{}.New()
// pageAttachment = PageAttachment{}.New()
// recordAttachment = RecordAttachment{}.New()
)
// Initialize handlers & controllers.
@@ -32,6 +31,7 @@ func MountRoutes() func(chi.Router) {
r.Use(auth.MiddlewareValidOnly)
r.Use(middlewareAllowedAccess)
handlers.NewNamespace(namespace).MountRoutes(r)
handlers.NewPage(page).MountRoutes(r)
handlers.NewModule(module).MountRoutes(r)
handlers.NewRecord(record).MountRoutes(r)
+67
View File
@@ -0,0 +1,67 @@
package types
// Hello! This file is auto-generated.
type (
// NamespaceSet slice of Namespace
//
// This type is auto-generated.
NamespaceSet []*Namespace
)
// Walk iterates through every slice item and calls w(Namespace) err
//
// This function is auto-generated.
func (set NamespaceSet) Walk(w func(*Namespace) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Namespace) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set NamespaceSet) Filter(f func(*Namespace) (bool, error)) (out NamespaceSet, err error) {
var ok bool
out = NamespaceSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set NamespaceSet) FindByID(ID uint64) *Namespace {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set NamespaceSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
+21 -1
View File
@@ -1,12 +1,32 @@
package types
import (
"time"
"github.com/jmoiron/sqlx/types"
"github.com/crusttech/crust/internal/rules"
)
type (
Namespace struct {
ID uint64 `json:"id,string" db:"id"`
ID uint64 `json:"namespaceID,string" db:"id"`
Name string `json:"name" db:"name"`
Slug string `json:"slug" db:"slug"`
Enabled bool `json:"enabled" db:"enabled"`
Meta types.JSONText `json:"meta" db:"meta"`
CreatedAt time.Time `json:"createdAt,omitempty" db:"created_at"`
UpdatedAt *time.Time `json:"updatedAt,omitempty" db:"updated_at"`
DeletedAt *time.Time `json:"deletedAt,omitempty" db:"deleted_at"`
}
NamespaceFilter struct {
Query string `json:"query"`
Page uint `json:"page"`
PerPage uint `json:"perPage"`
Sort string `json:"sort"`
Count uint `json:"count"`
}
)