3
0

Apply namespace changes to all layers of modules

This commit is contained in:
Denis Arh
2019-04-28 14:16:40 +02:00
parent a41e4bfc19
commit 40999e623e
11 changed files with 316 additions and 142 deletions
+21 -2
View File
@@ -185,7 +185,7 @@
},
{
"title": "Pages",
"description": "Compose module pages",
"description": "Compose pages",
"entrypoint": "page",
"path": "/namespace/{namespaceID}/page",
"authentication": [],
@@ -432,7 +432,8 @@
{
"imports": [
"sqlxTypes github.com/jmoiron/sqlx/types",
"github.com/crusttech/crust/compose/types"
"github.com/crusttech/crust/compose/types",
"time"
]
}
],
@@ -449,6 +450,18 @@
"name": "query",
"required": false,
"title": "Search query"
},
{
"name": "page",
"type": "uint",
"required": false,
"title": "Page number (0 based)"
},
{
"name": "perPage",
"type": "uint",
"required": false,
"title": "Returned items per page (default 50)"
}
]
}
@@ -529,6 +542,12 @@
"name": "meta",
"required": true,
"title": "Module meta data"
},
{
"type": "*time.Time",
"name": "updatedAt",
"required": false,
"title": "Last update (or creation) date"
}
]
}
+20 -1
View File
@@ -6,7 +6,8 @@
{
"imports": [
"sqlxTypes github.com/jmoiron/sqlx/types",
"github.com/crusttech/crust/compose/types"
"github.com/crusttech/crust/compose/types",
"time"
]
}
],
@@ -36,6 +37,18 @@
"required": false,
"title": "Search query",
"type": "string"
},
{
"name": "page",
"required": false,
"title": "Page number (0 based)",
"type": "uint"
},
{
"name": "perPage",
"required": false,
"title": "Returned items per page (default 50)",
"type": "uint"
}
]
}
@@ -116,6 +129,12 @@
"required": true,
"title": "Module meta data",
"type": "sqlxTypes.JSONText"
},
{
"name": "updatedAt",
"required": false,
"title": "Last update (or creation) date",
"type": "*time.Time"
}
]
}
+1 -1
View File
@@ -1,6 +1,6 @@
{
"Title": "Pages",
"Description": "Compose module pages",
"Description": "Compose pages",
"Interface": "Page",
"Struct": [
{
+78 -63
View File
@@ -7,6 +7,7 @@ import (
"github.com/jmoiron/sqlx"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"gopkg.in/Masterminds/squirrel.v1"
"github.com/crusttech/crust/compose/types"
)
@@ -15,11 +16,12 @@ type (
ModuleRepository interface {
With(ctx context.Context, db *factory.DB) ModuleRepository
FindByID(id uint64) (*types.Module, error)
Find() (types.ModuleSet, error)
FindByID(namespaceID, moduleID uint64) (*types.Module, error)
Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error)
FindFields(moduleIDs ...uint64) (ff types.ModuleFieldSet, err error)
Create(mod *types.Module) (*types.Module, error)
Update(mod *types.Module) (*types.Module, error)
DeleteByID(id uint64) error
DeleteByID(namespaceID, moduleID uint64) error
}
module struct {
@@ -28,60 +30,81 @@ type (
)
const (
sqlModuleColumns = `
id, name, json,
created_at, updated_at, deleted_at
`
sqlModuleSelect = `
SELECT ` + sqlModuleColumns + ` FROM compose_module WHERE deleted_at IS NULL
`
ErrModuleNotFound = repositoryError("ModuleNotFound")
)
func Module(ctx context.Context, db *factory.DB) ModuleRepository {
return (&module{}).With(ctx, db)
}
func (r *module) With(ctx context.Context, db *factory.DB) ModuleRepository {
func (r module) With(ctx context.Context, db *factory.DB) ModuleRepository {
return &module{
repository: r.repository.With(ctx, db),
}
}
// @todo: update to accepted DeletedAt column semantics from Messaging
func (r *module) FindByID(id uint64) (mod *types.Module, err error) {
mod = &types.Module{}
if err = r.db().Get(mod, sqlModuleSelect+" AND id = ? ", id); err != nil {
return
}
if mod.Fields, err = r.fields(id); err != nil {
return
}
return
func (r module) table() string {
return "compose_module"
}
func (r *module) Find() (mm types.ModuleSet, err error) {
if err = r.db().Select(&mm, sqlModuleSelect+" ORDER BY id ASC"); err != nil {
return
func (r module) columns() []string {
return []string{
"id", "rel_namespace", "name", "json",
"created_at", "updated_at", "deleted_at",
}
var ff types.ModuleFieldSet
if ff, err = r.fields(mm.IDs()...); err != nil {
return
} else {
_ = ff.Walk(func(f *types.ModuleField) error {
mm.FindByID(f.ModuleID).Fields = append(mm.FindByID(f.ModuleID).Fields, f)
return nil
})
}
return mm, nil
}
func (r *module) Create(mod *types.Module) (*types.Module, error) {
func (r module) query() squirrel.SelectBuilder {
return squirrel.
Select().
From(r.table()).
Where("deleted_at IS NULL")
}
func (r module) FindByID(namespaceID, moduleID uint64) (*types.Module, error) {
var (
query = r.query().
Columns(r.columns()...).
Where("id = ?", moduleID)
c = &types.Module{}
)
if namespaceID > 0 {
query = query.Where("rel_namespace = ?", namespaceID)
}
return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrModuleNotFound)
}
func (r module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) {
f = filter
f.PerPage = normalizePerPage(f.PerPage, 5, 100, 50)
query := r.query()
if filter.NamespaceID > 0 {
query = query.Where("a.rel_namespace = ?", filter.NamespaceID)
}
if f.Query != "" {
q := "%" + f.Query + "%"
query = query.Where("name like ?", q)
}
if f.Count, err = r.count(query); err != nil || f.Count == 0 {
return
}
query = query.
Columns(r.columns()...).
OrderBy("id ASC")
return set, f, r.fetchPaged(&set, query, f.Page, f.PerPage)
}
func (r module) Create(mod *types.Module) (*types.Module, error) {
mod.ID = factory.Sonyflake.NextID()
mod.CreatedAt = time.Now()
@@ -89,10 +112,10 @@ func (r *module) Create(mod *types.Module) (*types.Module, error) {
return nil, err
}
return mod, r.db().Insert("compose_module", mod)
return mod, r.db().Insert(r.table(), mod)
}
func (r *module) Update(mod *types.Module) (*types.Module, error) {
func (r module) Update(mod *types.Module) (*types.Module, error) {
now := time.Now()
mod.UpdatedAt = &now
@@ -100,10 +123,10 @@ func (r *module) Update(mod *types.Module) (*types.Module, error) {
return nil, err
}
return mod, r.db().Replace("compose_module", mod)
return mod, r.db().Replace(r.table(), mod)
}
func (r *module) updateFields(moduleID uint64, ff types.ModuleFieldSet) error {
func (r module) updateFields(moduleID uint64, ff types.ModuleFieldSet) error {
// @todo be more selective when deleting
if _, err := r.db().Exec("DELETE FROM compose_module_form WHERE module_id = ?", moduleID); err != nil {
return errors.Wrap(err, "Error updating module fields")
@@ -120,32 +143,24 @@ func (r *module) updateFields(moduleID uint64, ff types.ModuleFieldSet) error {
return nil
}
func (r *module) DeleteByID(id uint64) error {
_, err := r.db().Exec("DELETE FROM compose_module WHERE id=?", id)
func (r module) DeleteByID(namespaceID, attachmentID uint64) error {
_, err := r.db().Exec(
"UPDATE "+r.table()+" SET deleted_at = NOW() WHERE rel_namespace = ? AND id = ?",
namespaceID,
attachmentID,
)
return err
}
func (r *module) fields(IDs ...uint64) (ff types.ModuleFieldSet, err error) {
if len(IDs) == 0 {
func (r module) FindFields(moduleIDs ...uint64) (ff types.ModuleFieldSet, err error) {
if len(moduleIDs) == 0 {
return
}
if sql, args, err := sqlx.In("SELECT * FROM compose_module_form WHERE module_id IN (?) ORDER BY module_id AND place", IDs); err != nil {
if sql, args, err := sqlx.In("SELECT * FROM compose_module_form WHERE module_id IN (?) ORDER BY module_id AND place", moduleIDs); err != nil {
return nil, err
} else {
return ff, r.db().Select(&ff, sql, args...)
}
}
// // FieldNames returns a slice of field names, used for ordering record row columns
// func (r *module) FieldNames(mod *types.Module) ([]string, error) {
// if fields, err := r.Fields(mod.ID); err != nil {
// return []string{}, err
// } else {
// result := make([]string, len(fields))
// for k, v := range fields {
// result[k] = v.Name
// }
// return result, nil
// }
// }
+2 -2
View File
@@ -114,7 +114,7 @@ func (svc attachment) Find(filter types.AttachmentFilter) (types.AttachmentSet,
}
if filter.ModuleID > 0 {
if _, err := svc.moduleSvc.FindByID(filter.ModuleID); err != nil {
if _, err := svc.moduleSvc.FindByID(filter.NamespaceID, filter.ModuleID); err != nil {
return nil, filter, err
}
}
@@ -174,7 +174,7 @@ func (svc attachment) CreateRecordAttachment(namespaceID uint64, name string, si
var currentUserID uint64 = auth.GetIdentityFromContext(svc.ctx).Identity()
if _, err := svc.moduleSvc.FindByID(moduleID); err != nil {
if _, err := svc.moduleSvc.FindByID(namespaceID, moduleID); err != nil {
return nil, err
} else if r, err := svc.recordSvc.FindByID(recordID); err != nil {
return nil, err
+71 -51
View File
@@ -3,7 +3,6 @@ package service
import (
"context"
"github.com/pkg/errors"
"github.com/titpetric/factory"
"github.com/crusttech/crust/compose/internal/repository"
@@ -24,12 +23,12 @@ type (
ModuleService interface {
With(ctx context.Context) ModuleService
FindByID(moduleID uint64) (*types.Module, error)
Find() (types.ModuleSet, error)
FindByID(namespaceID, moduleID uint64) (*types.Module, error)
Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error)
Create(module *types.Module) (*types.Module, error)
Update(module *types.Module) (*types.Module, error)
DeleteByID(moduleID uint64) error
DeleteByID(namespaceID, moduleID uint64) error
}
)
@@ -52,75 +51,96 @@ func (svc *module) With(ctx context.Context) ModuleService {
}
}
func (svc *module) FindByID(id uint64) (m *types.Module, err error) {
if m, err = svc.moduleRepo.FindByID(id); err != nil {
func (svc *module) FindByID(namespaceID, moduleID uint64) (m *types.Module, err error) {
if namespaceID == 0 {
return nil, ErrNamespaceRequired
}
if m, err = svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil {
return
} else if !svc.prmSvc.CanReadModule(m) {
return nil, errors.New("not allowed to access this module")
return nil, ErrNoReadPermissions.withStack()
}
var ff types.ModuleFieldSet
if ff, err = svc.moduleRepo.FindFields(m.ID); err != nil {
return
} else {
_ = ff.Walk(func(f *types.ModuleField) error {
m.Fields = append(m.Fields, f)
return nil
})
}
return
}
func (svc *module) Find() (mm types.ModuleSet, err error) {
if mm, err = svc.moduleRepo.Find(); err != nil {
return nil, err
func (svc *module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types.ModuleFilter, err error) {
set, f, err = svc.moduleRepo.Find(filter)
if err != nil {
return
}
set, _ = set.Filter(func(m *types.Module) (bool, error) {
return svc.prmSvc.CanReadModule(m), nil
})
// Preload all fields and update all modules
var ff types.ModuleFieldSet
if ff, err = svc.moduleRepo.FindFields(set.IDs()...); err != nil {
return
} else {
return mm.Filter(func(m *types.Module) (bool, error) {
return svc.prmSvc.CanReadModule(m), nil
_ = ff.Walk(func(f *types.ModuleField) error {
set.FindByID(f.ModuleID).Fields = append(set.FindByID(f.ModuleID).Fields, f)
return nil
})
}
return
}
func (svc *module) Create(mod *types.Module) (*types.Module, error) {
if !svc.prmSvc.CanCreateModule(crmNamespace()) {
return nil, errors.New("not allowed to create this module")
}
if len(mod.Fields) == 0 {
return nil, errors.New("Error creating module: no fields")
return nil, ErrNoCreatePermissions.withStack()
}
return svc.moduleRepo.Create(mod)
}
func (svc *module) Update(module *types.Module) (m *types.Module, err error) {
validate := func() error {
if module.ID == 0 {
return errors.New("Error updating module: invalid ID")
} else if m, err = svc.moduleRepo.FindByID(module.ID); err != nil {
return errors.Wrap(err, "Error while loading module for update")
} else {
if !svc.prmSvc.CanUpdateModule(m) {
return errors.New("not allowed to update this module")
}
module.CreatedAt = m.CreatedAt
}
if len(module.Fields) == 0 {
return errors.New("Error updating module: no fields")
}
return nil
func (svc *module) Update(mod *types.Module) (m *types.Module, err error) {
if mod.ID == 0 {
return nil, ErrInvalidID.withStack()
}
if err = validate(); err != nil {
return nil, err
}
return m, svc.db.Transaction(func() (err error) {
m, err = svc.moduleRepo.Update(module)
if m, err = svc.moduleRepo.FindByID(mod.NamespaceID, mod.ID); err != nil {
return
})
}
func (svc *module) DeleteByID(ID uint64) error {
if m, err := svc.moduleRepo.FindByID(ID); err != nil {
return errors.Wrap(err, "could not delete module")
} else if !svc.prmSvc.CanDeleteModule(m) {
return errors.New("not allowed to delete this module")
}
return svc.moduleRepo.DeleteByID(ID)
if isStale(mod.UpdatedAt, m.UpdatedAt, m.CreatedAt) {
return nil, ErrStaleData.withStack()
}
if !svc.prmSvc.CanUpdateModule(m) {
return nil, ErrNoUpdatePermissions.withStack()
}
m.Name = mod.Name
m.Meta = mod.Meta
m.Fields = mod.Fields
return svc.moduleRepo.Update(m)
}
func (svc *module) DeleteByID(namespaceID, moduleID uint64) error {
if namespaceID == 0 {
return ErrNamespaceRequired.withStack()
}
if c, err := svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil {
return err
} else if !svc.prmSvc.CanDeleteModule(c) {
return ErrNoDeletePermissions.withStack()
}
return svc.moduleRepo.DeleteByID(namespaceID, moduleID)
}
+6 -4
View File
@@ -85,9 +85,10 @@ func (svc *record) FindByID(recordID uint64) (r *types.Record, err error) {
func (svc *record) Report(moduleID uint64, metrics, dimensions, filter string) (out interface{}, err error) {
var module *types.Module
var namespaceID uint64 = 0
err = svc.db.Transaction(func() (err error) {
if module, err = svc.moduleRepo.FindByID(moduleID); err != nil {
if module, err = svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil {
return
} else if !svc.prmSvc.CanReadRecord(module) {
return errors.New("not allowed to access this record")
@@ -102,9 +103,10 @@ func (svc *record) Report(moduleID uint64, metrics, dimensions, filter string) (
func (svc *record) Find(moduleID uint64, filter, sort string, page, perPage int) (rsp *repository.FindResponse, err error) {
var module *types.Module
var namespaceID uint64 = 0
err = svc.db.Transaction(func() (err error) {
if module, err = svc.moduleRepo.FindByID(moduleID); err != nil {
if module, err = svc.moduleRepo.FindByID(namespaceID, moduleID); err != nil {
return
} else if !svc.prmSvc.CanReadRecord(module) {
return errors.New("not allowed to access this record")
@@ -129,7 +131,7 @@ func (svc *record) Create(in *types.Record) (record *types.Record, err error) {
var module *types.Module
err = svc.db.Transaction(func() (err error) {
if module, err = svc.moduleRepo.FindByID(in.ModuleID); err != nil {
if module, err = svc.moduleRepo.FindByID(in.NamespaceID, in.ModuleID); err != nil {
return
} else if !svc.prmSvc.CanCreateRecord(module) {
return errors.New("not allowed to create records for this module")
@@ -174,7 +176,7 @@ func (svc *record) Update(updated *types.Record) (record *types.Record, err erro
return errors.New("not allowed to update this record")
}
if module, err = svc.moduleRepo.FindByID(updated.ModuleID); err != nil {
if module, err = svc.moduleRepo.FindByID(updated.NamespaceID, updated.ModuleID); err != nil {
return
}
+86 -17
View File
@@ -11,29 +11,50 @@ import (
)
type (
modulePayload struct {
*types.Module
CanUpdateModule bool `json:"canUpdateModule"`
CanDeleteModule bool `json:"canDeleteModule"`
CanCreateRecord bool `json:"canCreateRecord"`
CanReadRecord bool `json:"canReadRecord"`
CanUpdateRecord bool `json:"canUpdateRecord"`
CanDeleteRecord bool `json:"canDeleteRecord"`
}
moduleSetPayload struct {
Filter types.ModuleFilter `json:"filter"`
Set []*modulePayload `json:"set"`
}
Module struct {
module service.ModuleService
record service.RecordService
module service.ModuleService
record service.RecordService
permissions service.PermissionsService
}
)
func (Module) New() *Module {
return &Module{
module: service.DefaultModule,
record: service.DefaultRecord,
module: service.DefaultModule,
record: service.DefaultRecord,
permissions: service.DefaultPermissions,
}
}
func (ctrl *Module) List(ctx context.Context, r *request.ModuleList) (interface{}, error) {
return ctrl.module.With(ctx).Find()
f := types.ModuleFilter{
Query: r.Query,
PerPage: r.PerPage,
Page: r.Page,
}
set, filter, err := ctrl.module.With(ctx).Find(f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *Module) Read(ctx context.Context, r *request.ModuleRead) (interface{}, error) {
return ctrl.module.With(ctx).FindByID(r.ModuleID)
}
func (ctrl *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) {
return resputil.OK(), ctrl.module.With(ctx).DeleteByID(r.ModuleID)
return ctrl.module.With(ctx).FindByID(r.NamespaceID, r.ModuleID)
}
func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interface{}, error) {
@@ -46,11 +67,59 @@ func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interf
}
func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interface{}, error) {
item := &types.Module{
ID: r.ModuleID,
Name: r.Name,
Fields: r.Fields,
Meta: r.Meta,
}
return ctrl.module.With(ctx).Update(item)
var (
m = &types.Module{}
err error
)
m.ID = r.ModuleID
m.Name = r.Name
m.Meta = r.Meta
m.NamespaceID = r.NamespaceID
m.UpdatedAt = r.UpdatedAt
m, err = ctrl.module.With(ctx).Update(m)
return ctrl.makePayload(ctx, m, err)
}
func (ctrl *Module) Delete(ctx context.Context, r *request.ModuleDelete) (interface{}, error) {
_, err := ctrl.module.With(ctx).FindByID(r.NamespaceID, r.ModuleID)
if err != nil {
return nil, err
}
return resputil.OK(), ctrl.module.With(ctx).DeleteByID(r.NamespaceID, r.ModuleID)
}
func (ctrl Module) makePayload(ctx context.Context, m *types.Module, err error) (*modulePayload, error) {
if err != nil || m == nil {
return nil, err
}
perm := ctrl.permissions.With(ctx)
return &modulePayload{
Module: m,
CanUpdateModule: perm.CanUpdateModule(m),
CanDeleteModule: perm.CanDeleteModule(m),
CanCreateRecord: perm.CanCreateRecord(m),
CanReadRecord: perm.CanReadRecord(m),
CanUpdateRecord: perm.CanUpdateRecord(m),
CanDeleteRecord: perm.CanDeleteRecord(m),
}, nil
}
func (ctrl Module) makeFilterPayload(ctx context.Context, nn types.ModuleSet, f types.ModuleFilter, err error) (*moduleSetPayload, error) {
if err != nil {
return nil, err
}
msp := &moduleSetPayload{Filter: f, Set: make([]*modulePayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}
+18
View File
@@ -28,6 +28,7 @@ import (
"github.com/crusttech/crust/compose/types"
sqlxTypes "github.com/jmoiron/sqlx/types"
"time"
)
var _ = chi.URLParam
@@ -36,6 +37,8 @@ var _ = multipart.FileHeader{}
// Module list request parameters
type ModuleList struct {
Query string
Page uint
PerPage uint
NamespaceID uint64 `json:",string"`
}
@@ -74,6 +77,14 @@ func (mReq *ModuleList) Fill(r *http.Request) (err error) {
mReq.Query = val
}
if val, ok := get["page"]; ok {
mReq.Page = parseUint(val)
}
if val, ok := get["perPage"]; ok {
mReq.PerPage = parseUint(val)
}
mReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID"))
return err
@@ -189,6 +200,7 @@ type ModuleUpdate struct {
Name string
Fields types.ModuleFieldSet
Meta sqlxTypes.JSONText
UpdatedAt *time.Time
}
func NewModuleUpdate() *ModuleUpdate {
@@ -234,6 +246,12 @@ func (mReq *ModuleUpdate) Fill(r *http.Request) (err error) {
return err
}
}
if val, ok := post["updatedAt"]; ok {
if mReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil {
return err
}
}
return err
}
+9
View File
@@ -22,6 +22,15 @@ type (
UpdatedAt *time.Time `db:"updated_at" json:"updatedAt,omitempty"`
DeletedAt *time.Time `db:"deleted_at" json:"deletedAt,omitempty"`
}
ModuleFilter struct {
NamespaceID uint64 `json:"namespaceID,string"`
Query string `json:"query"`
Page uint `json:"page"`
PerPage uint `json:"perPage"`
// Sort string `json:"sort"`
Count uint `json:"count"`
}
)
// Resource returns a system resource ID for this type
+4 -1
View File
@@ -232,6 +232,8 @@ Compose module definitions
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| query | string | GET | Search query | N/A | NO |
| page | uint | GET | Page number (0 based) | N/A | NO |
| perPage | uint | GET | Returned items per page (default 50) | N/A | NO |
| namespaceID | uint64 | PATH | Namespace ID | N/A | YES |
## Create module
@@ -283,6 +285,7 @@ Compose module definitions
| name | string | POST | Module Name | N/A | YES |
| fields | types.ModuleFieldSet | POST | Fields JSON | N/A | YES |
| meta | sqlxTypes.JSONText | POST | Module meta data | N/A | YES |
| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO |
## Delete module
@@ -432,7 +435,7 @@ Compose Notifications
# Pages
Compose module pages
Compose pages
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |