3
0

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-28 08:56:44 +02:00
parent 010a1351ea
commit 5bfbab6a4e
24 changed files with 2222 additions and 59 deletions

View File

@@ -24,6 +24,165 @@
} }
] ]
}, },
{
"title": "Namespaces",
"parameters": {},
"entrypoint": "namespace",
"path": "/namespace",
"authentication": [],
"struct": [
{
"imports": [
"sqlxTypes github.com/jmoiron/sqlx/types",
"time"
]
}
],
"apis": [
{
"name": "list",
"method": "GET",
"title": "List namespaces",
"path": "/",
"parameters": {
"get": [
{
"type": "string",
"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)"
}
]
}
},
{
"name": "create",
"method": "POST",
"title": "Create namespace",
"path": "/",
"parameters": {
"post": [
{
"type": "string",
"name": "name",
"required": true,
"title": "Name"
},
{
"type": "string",
"name": "slug",
"required": true,
"title": "Slug (url path part)"
},
{
"type": "bool",
"name": "enabled",
"required": true,
"title": "Enabled"
},
{
"type": "sqlxTypes.JSONText",
"name": "meta",
"required": true,
"title": "Meta data"
}
]
}
},
{
"name": "read",
"method": "GET",
"title": "Read namespace",
"path": "/{namespaceID}",
"parameters": {
"path": [
{
"type": "uint64",
"name": "namespaceID",
"required": true,
"title": "ID"
}
]
}
},
{
"name": "update",
"method": "POST",
"title": "Update namespace",
"path": "/{namespaceID}",
"parameters": {
"path": [
{
"type": "uint64",
"name": "namespaceID",
"required": true,
"title": "ID"
}
],
"post": [
{
"type": "string",
"name": "name",
"required": true,
"title": "Name"
},
{
"type": "string",
"name": "slug",
"required": true,
"title": "Slug (url path part)"
},
{
"type": "bool",
"name": "enabled",
"required": true,
"title": "Enabled"
},
{
"type": "sqlxTypes.JSONText",
"name": "meta",
"required": true,
"title": "Meta data"
},
{
"type": "*time.Time",
"name": "updatedAt",
"required": true,
"title": "Last update (or creation) date"
}
]
}
},
{
"name": "delete",
"method": "DELETE",
"title": "Delete namespace",
"path": "/{namespaceID}",
"parameters": {
"path": [
{
"type": "uint64",
"name": "namespaceID",
"required": true,
"title": "ID"
}
]
}
}
]
},
{ {
"title": "Pages", "title": "Pages",
"description": "Compose module pages", "description": "Compose module pages",
@@ -374,7 +533,7 @@
}, },
{ {
"title": "Records", "title": "Records",
"description": "Compose records ", "description": "Compose records",
"entrypoint": "record", "entrypoint": "record",
"path": "/module/{moduleID}/record", "path": "/module/{moduleID}/record",
"authentication": [], "authentication": [],

View File

@@ -0,0 +1,160 @@
{
"Title": "Namespaces",
"Interface": "Namespace",
"Struct": [
{
"imports": [
"sqlxTypes github.com/jmoiron/sqlx/types",
"time"
]
}
],
"Parameters": {},
"Protocol": "",
"Authentication": [],
"Path": "/namespace",
"APIs": [
{
"Name": "list",
"Method": "GET",
"Title": "List namespaces",
"Path": "/",
"Parameters": {
"get": [
{
"name": "query",
"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"
}
]
}
},
{
"Name": "create",
"Method": "POST",
"Title": "Create namespace",
"Path": "/",
"Parameters": {
"post": [
{
"name": "name",
"required": true,
"title": "Name",
"type": "string"
},
{
"name": "slug",
"required": true,
"title": "Slug (url path part)",
"type": "string"
},
{
"name": "enabled",
"required": true,
"title": "Enabled",
"type": "bool"
},
{
"name": "meta",
"required": true,
"title": "Meta data",
"type": "sqlxTypes.JSONText"
}
]
}
},
{
"Name": "read",
"Method": "GET",
"Title": "Read namespace",
"Path": "/{namespaceID}",
"Parameters": {
"path": [
{
"name": "namespaceID",
"required": true,
"title": "ID",
"type": "uint64"
}
]
}
},
{
"Name": "update",
"Method": "POST",
"Title": "Update namespace",
"Path": "/{namespaceID}",
"Parameters": {
"path": [
{
"name": "namespaceID",
"required": true,
"title": "ID",
"type": "uint64"
}
],
"post": [
{
"name": "name",
"required": true,
"title": "Name",
"type": "string"
},
{
"name": "slug",
"required": true,
"title": "Slug (url path part)",
"type": "string"
},
{
"name": "enabled",
"required": true,
"title": "Enabled",
"type": "bool"
},
{
"name": "meta",
"required": true,
"title": "Meta data",
"type": "sqlxTypes.JSONText"
},
{
"name": "updatedAt",
"required": true,
"title": "Last update (or creation) date",
"type": "*time.Time"
}
]
}
},
{
"Name": "delete",
"Method": "DELETE",
"Title": "Delete namespace",
"Path": "/{namespaceID}",
"Parameters": {
"path": [
{
"name": "namespaceID",
"required": true,
"title": "ID",
"type": "uint64"
}
]
}
}
]
}

View File

@@ -22,6 +22,7 @@ function types {
CGO_ENABLED=0 go build -o ./build/gen-type-set codegen/v2/type-set.go CGO_ENABLED=0 go build -o ./build/gen-type-set codegen/v2/type-set.go
fi fi
./build/gen-type-set --types Namespace --output compose/types/namespace.gen.go
./build/gen-type-set --types Attachment --output compose/types/attachment.gen.go ./build/gen-type-set --types Attachment --output compose/types/attachment.gen.go
./build/gen-type-set --types Module --output compose/types/module.gen.go ./build/gen-type-set --types Module --output compose/types/module.gen.go
./build/gen-type-set --types Page --output compose/types/page.gen.go ./build/gen-type-set --types Page --output compose/types/page.gen.go

View File

@@ -101,6 +101,8 @@ $parsers = array(
"int" => "parseInt", "int" => "parseInt",
"uint" => "parseUint", "uint" => "parseUint",
"bool" => "parseBool", "bool" => "parseBool",
"time.Time" => "parseISODateWithErr",
"*time.Time" => "parseISODatePtrWithErr",
"sqlxTypes.JSONText" => "parseJSONTextWithErr", "sqlxTypes.JSONText" => "parseJSONTextWithErr",
); );

File diff suppressed because one or more lines are too long

View File

@@ -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;

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)
}

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
}

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)
}

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)
}

View File

@@ -27,7 +27,10 @@ type (
Effective() (ee []effectivePermission, err error) Effective() (ee []effectivePermission, err error)
CanAccess() bool CanAccess() bool
CanCreateNamspace() bool CanCreateNamespace() bool
CanReadNamespace(r permissionResource) bool
CanUpdateNamespace(r permissionResource) bool
CanDeleteNamespace(r permissionResource) bool
CanCreateModule(r permissionResource) bool CanCreateModule(r permissionResource) bool
CanReadModule(r permissionResource) bool CanReadModule(r permissionResource) bool
CanUpdateModule(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", "access", p.CanAccess()))
ee = append(ee, ep("compose", "grant", p.CanGrant())) 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", "module.create", p.CanCreateModule(crmNamespace())))
ee = append(ee, ep("compose:namespace:crm", "chart.create", p.CanCreateChart(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") return p.checkAccess(types.PermissionResource, "grant")
} }
func (p *permissions) CanCreateNamspace() bool { func (p *permissions) CanCreateNamespace() bool {
return p.checkAccess(types.PermissionResource, "namespace.create") return p.checkAccess(types.PermissionResource, "namespace.create")
} }
func (p *permissions) CanCreateModule(ns permissionResource) bool { func (p *permissions) CanReadNamespace(r permissionResource) bool {
// @todo move to func args when namespaces are implemented return p.checkAccess(r, "read", p.allow())
return p.checkAccess(ns, "module.create") }
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 { func (p *permissions) CanReadModule(r permissionResource) bool {
@@ -206,3 +220,9 @@ func (p *permissions) checkAccess(resource permissionResource, operation string,
} }
return false return false
} }
func (p permissions) allow() func() internalRules.Access {
return func() internalRules.Access {
return internalRules.Allow
}
}

View File

@@ -19,6 +19,7 @@ var (
DefaultNotification NotificationService DefaultNotification NotificationService
DefaultPermissions PermissionsService DefaultPermissions PermissionsService
DefaultAttachment AttachmentService DefaultAttachment AttachmentService
DefaultNamespace NamespaceService
) )
func Init() error { func Init() error {
@@ -35,6 +36,7 @@ func Init() error {
DefaultChart = Chart() DefaultChart = Chart()
DefaultNotification = Notification() DefaultNotification = Notification()
DefaultAttachment = Attachment(fs) DefaultAttachment = Attachment(fs)
DefaultNamespace = Namespace()
return nil return nil
} }

View File

@@ -30,12 +30,6 @@ type (
UpdatedAt *time.Time `json:"updatedAt,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"`
} }
file struct {
*types.Attachment
content io.ReadSeeker
download bool
}
Attachment struct { Attachment struct {
attachment service.AttachmentService attachment service.AttachmentService
} }
@@ -176,23 +170,3 @@ func makeAttachmentPayload(a *types.Attachment, userID uint64) *attachmentPayloa
UpdatedAt: a.UpdatedAt, 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
}

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
compose/rest/namespace.go Normal file
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
}

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()

View File

@@ -4,6 +4,7 @@ import (
"regexp" "regexp"
"strconv" "strconv"
"strings" "strings"
"time"
"github.com/jmoiron/sqlx/types" "github.com/jmoiron/sqlx/types"
"github.com/pkg/errors" "github.com/pkg/errors"
@@ -17,6 +18,19 @@ func parseJSONTextWithErr(s string) (types.JSONText, error) {
return *result, err 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 // parseInt parses a string to int
func parseInt(s string) int { func parseInt(s string) int {
if s == "" { if s == "" {

View File

@@ -9,6 +9,7 @@ import (
func MountRoutes() func(chi.Router) { func MountRoutes() func(chi.Router) {
var ( var (
namespace = Namespace{}.New()
module = Module{}.New() module = Module{}.New()
record = Record{}.New() record = Record{}.New()
page = Page{}.New() page = Page{}.New()
@@ -16,8 +17,6 @@ func MountRoutes() func(chi.Router) {
trigger = Trigger{}.New() trigger = Trigger{}.New()
notification = Notification{}.New() notification = Notification{}.New()
attachment = Attachment{}.New() attachment = Attachment{}.New()
// pageAttachment = PageAttachment{}.New()
// recordAttachment = RecordAttachment{}.New()
) )
// Initialize handlers & controllers. // Initialize handlers & controllers.
@@ -32,6 +31,7 @@ func MountRoutes() func(chi.Router) {
r.Use(auth.MiddlewareValidOnly) r.Use(auth.MiddlewareValidOnly)
r.Use(middlewareAllowedAccess) r.Use(middlewareAllowedAccess)
handlers.NewNamespace(namespace).MountRoutes(r)
handlers.NewPage(page).MountRoutes(r) handlers.NewPage(page).MountRoutes(r)
handlers.NewModule(module).MountRoutes(r) handlers.NewModule(module).MountRoutes(r)
handlers.NewRecord(record).MountRoutes(r) handlers.NewRecord(record).MountRoutes(r)

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
}

View File

@@ -1,12 +1,32 @@
package types package types
import ( import (
"time"
"github.com/jmoiron/sqlx/types"
"github.com/crusttech/crust/internal/rules" "github.com/crusttech/crust/internal/rules"
) )
type ( type (
Namespace struct { 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"`
} }
) )

788
docs/compose/README.md Normal file
View File

@@ -0,0 +1,788 @@
# Attachments
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/attachment/{kind}/` | List, filter all page attachments |
| `GET` | `/attachment/{kind}/{attachmentID}` | Attachment details |
| `GET` | `/attachment/{kind}/{attachmentID}/original/{name}` | Serves attached file |
| `GET` | `/attachment/{kind}/{attachmentID}/preview.{ext}` | Serves preview of an attached file |
## List, filter all page attachments
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | GET | Filter attachments by page ID | N/A | NO |
| moduleID | uint64 | GET | Filter attachments by mnodule ID | N/A | NO |
| recordID | uint64 | GET | Filter attachments by record ID | N/A | NO |
| fieldName | string | GET | Filter attachments by field name | 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 |
| sign | string | GET | Signature | N/A | YES |
| userID | uint64 | GET | User ID | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
## Attachment details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
| sign | string | GET | Signature | N/A | YES |
| userID | uint64 | GET | User ID | N/A | YES |
## Serves attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}/original/{name}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| download | bool | GET | Force file download | N/A | NO |
| sign | string | GET | Signature | N/A | YES |
| userID | uint64 | GET | User ID | N/A | YES |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| name | string | PATH | File name | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
## Serves preview of an attached file
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/attachment/{kind}/{attachmentID}/preview.{ext}` | HTTP/S | GET | Client ID, Session ID |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| attachmentID | uint64 | PATH | Attachment ID | N/A | YES |
| ext | string | PATH | Preview extension/format | N/A | YES |
| kind | string | PATH | Attachment kind | N/A | YES |
| sign | string | GET | Signature | N/A | YES |
| userID | uint64 | GET | User ID | N/A | YES |
---
# Charts
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/chart/` | List/read charts from module section |
| `POST` | `/chart/` | List/read charts from module section |
| `GET` | `/chart/{chartID}` | Read charts by ID from module section |
| `POST` | `/chart/{chartID}` | Add/update charts in module section |
| `DELETE` | `/chart/{chartID}` | Delete chart |
## List/read charts from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/chart/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
## List/read charts from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/chart/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| config | sqlxTypes.JSONText | POST | Chart JSON | N/A | YES |
| name | string | POST | Chart name | N/A | YES |
## Read charts by ID from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/chart/{chartID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| chartID | uint64 | PATH | Chart ID | N/A | YES |
## Add/update charts in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/chart/{chartID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| chartID | uint64 | PATH | Chart ID | N/A | YES |
| config | sqlxTypes.JSONText | POST | Chart JSON | N/A | YES |
| name | string | POST | Chart name | N/A | YES |
## Delete chart
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/chart/{chartID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| chartID | uint64 | PATH | Chart ID | N/A | YES |
---
# Modules
Compose module definitions
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/module/` | List modules |
| `POST` | `/module/` | Create module |
| `GET` | `/module/{moduleID}` | Read module |
| `POST` | `/module/{moduleID}` | Update module |
| `DELETE` | `/module/{moduleID}` | Delete module |
## List modules
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| query | string | GET | Search query | N/A | NO |
## Create module
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| 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 |
## Read module
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Update module
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| 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 |
## Delete module
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
---
# Namespaces
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/namespace/` | List namespaces |
| `POST` | `/namespace/` | Create namespace |
| `GET` | `/namespace/{namespaceID}` | Read namespace |
| `POST` | `/namespace/{namespaceID}` | Update namespace |
| `DELETE` | `/namespace/{namespaceID}` | Delete namespace |
## List namespaces
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/` | HTTP/S | GET | |
#### Request parameters
| 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 |
## Create namespace
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| name | string | POST | Name | N/A | YES |
| slug | string | POST | Slug (url path part) | N/A | YES |
| enabled | bool | POST | Enabled | N/A | YES |
| meta | sqlxTypes.JSONText | POST | Meta data | N/A | YES |
## Read namespace
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/{namespaceID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| namespaceID | uint64 | PATH | ID | N/A | YES |
## Update namespace
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/{namespaceID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| namespaceID | uint64 | PATH | ID | N/A | YES |
| name | string | POST | Name | N/A | YES |
| slug | string | POST | Slug (url path part) | N/A | YES |
| enabled | bool | POST | Enabled | N/A | YES |
| meta | sqlxTypes.JSONText | POST | Meta data | N/A | YES |
| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | YES |
## Delete namespace
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/namespace/{namespaceID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| namespaceID | uint64 | PATH | ID | N/A | YES |
---
# Notifications
Compose Notifications
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `POST` | `/notification/email` | Send email from the Compose |
## Send email from the Compose
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/notification/email` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| to | []string | POST | Email addresses or Crust user IDs | N/A | YES |
| cc | []string | POST | Email addresses or Crust user IDs | N/A | NO |
| replyTo | string | POST | Crust user ID or email address in reply-to field | N/A | NO |
| subject | string | POST | Email subject | N/A | NO |
| content | sqlxTypes.JSONText | POST | Message content | N/A | YES |
---
# Pages
Compose module pages
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/page/` | List available pages |
| `POST` | `/page/` | Create page |
| `GET` | `/page/{pageID}` | Get page details |
| `GET` | `/page/tree` | Get page all (non-record) pages, hierarchically |
| `POST` | `/page/{pageID}` | Update page |
| `POST` | `/page/{selfID}/reorder` | Reorder pages |
| `Delete` | `/page/{pageID}` | Delete page |
| `POST` | `/page/{pageID}/attachment` | Uploads attachment to page |
## List available pages
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| selfID | uint64 | GET | Parent page ID | N/A | NO |
## Create page
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| selfID | uint64 | POST | Parent Page ID | N/A | NO |
| moduleID | uint64 | POST | Module ID | N/A | NO |
| title | string | POST | Title | N/A | YES |
| description | string | POST | Description | N/A | NO |
| visible | bool | POST | Visible in navigation | N/A | NO |
| blocks | sqlxTypes.JSONText | POST | Blocks JSON | N/A | YES |
## Get page details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/{pageID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | PATH | Page ID | N/A | YES |
## Get page all (non-record) pages, hierarchically
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/tree` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
## Update page
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/{pageID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | PATH | Page ID | N/A | YES |
| selfID | uint64 | POST | Parent Page ID | N/A | NO |
| moduleID | uint64 | POST | Module ID (optional) | N/A | NO |
| title | string | POST | Title | N/A | YES |
| description | string | POST | Description | N/A | NO |
| visible | bool | POST | Visible in navigation | N/A | NO |
| blocks | sqlxTypes.JSONText | POST | Blocks JSON | N/A | YES |
## Reorder pages
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/{selfID}/reorder` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| selfID | uint64 | PATH | Parent page ID | N/A | YES |
| pageIDs | []string | POST | Page ID order | N/A | YES |
## Delete page
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/{pageID}` | HTTP/S | Delete | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | PATH | Page ID | N/A | YES |
## Uploads attachment to page
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/page/{pageID}/attachment` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| pageID | uint64 | PATH | Page ID | N/A | YES |
| upload | *multipart.FileHeader | POST | File to upload | N/A | YES |
---
# Permissions
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/permissions/effective` | Effective rules for current user |
## Effective rules for current user
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/permissions/effective` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| resource | string | GET | Show only rules for a specific resource | N/A | NO |
---
# Records
Compose records
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/module/{moduleID}/record/report` | Generates report from module records |
| `GET` | `/module/{moduleID}/record/` | List/read records from module section |
| `POST` | `/module/{moduleID}/record/` | Create record in module section |
| `GET` | `/module/{moduleID}/record/{recordID}` | Read records by ID from module section |
| `POST` | `/module/{moduleID}/record/{recordID}` | Update records in module section |
| `DELETE` | `/module/{moduleID}/record/{recordID}` | Delete record row from module section |
| `POST` | `/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | Uploads attachment and validates it against record field requirements |
## Generates report from module records
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/report` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| metrics | string | GET | Metrics (eg: 'SUM(money), MAX(calls)') | N/A | NO |
| dimensions | string | GET | Dimensions (eg: 'DATE(foo), status') | N/A | YES |
| filter | string | GET | Filter (eg: 'DATE(foo) > 2010') | N/A | NO |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## List/read records from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| filter | string | GET | Filtering condition | N/A | NO |
| page | int | GET | Page number (0 based) | N/A | NO |
| perPage | int | GET | Returned items per page (default 50) | N/A | NO |
| sort | string | GET | Sort field (default id desc) | N/A | NO |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Create record in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| values | types.RecordValueSet | POST | Record values | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Read records by ID from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Update records in module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| values | types.RecordValueSet | POST | Record values | N/A | YES |
## Delete record row from module section
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}` | HTTP/S | DELETE | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
## Uploads attachment and validates it against record field requirements
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/module/{moduleID}/record/{recordID}/{fieldName}/attachment` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| recordID | uint64 | PATH | Record ID | N/A | YES |
| fieldName | string | PATH | Field name | N/A | YES |
| moduleID | uint64 | PATH | Module ID | N/A | YES |
| upload | *multipart.FileHeader | POST | File to upload | N/A | YES |
---
# Triggers
Compose Triggers
| Method | Endpoint | Purpose |
| ------ | -------- | ------- |
| `GET` | `/trigger/` | List available triggers |
| `POST` | `/trigger/` | Create trigger |
| `GET` | `/trigger/{triggerID}` | Get trigger details |
| `POST` | `/trigger/{triggerID}` | Update trigger |
| `Delete` | `/trigger/{triggerID}` | Delete trigger |
## List available triggers
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/trigger/` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | GET | Filter triggers by module | N/A | NO |
## Create trigger
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/trigger/` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| moduleID | uint64 | POST | Module ID | N/A | NO |
| name | string | POST | Name | N/A | YES |
| actions | []string | POST | Actions that trigger this trigger | N/A | NO |
| enabled | bool | POST | Enabled | N/A | NO |
| source | string | POST | Trigger source code | N/A | NO |
## Get trigger details
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/trigger/{triggerID}` | HTTP/S | GET | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| triggerID | uint64 | PATH | Trigger ID | N/A | YES |
## Update trigger
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/trigger/{triggerID}` | HTTP/S | POST | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| triggerID | uint64 | PATH | Trigger ID | N/A | YES |
| moduleID | uint64 | POST | Module ID | N/A | NO |
| name | string | POST | Name | N/A | YES |
| actions | []string | POST | Actions that trigger this trigger | N/A | NO |
| enabled | bool | POST | Enabled | N/A | NO |
| source | string | POST | Trigger source code | N/A | NO |
## Delete trigger
#### Method
| URI | Protocol | Method | Authentication |
| --- | -------- | ------ | -------------- |
| `/trigger/{triggerID}` | HTTP/S | Delete | |
#### Request parameters
| Parameter | Type | Method | Description | Default | Required? |
| --------- | ---- | ------ | ----------- | ------- | --------- |
| triggerID | uint64 | PATH | Trigger ID | N/A | YES |
---

View File

@@ -304,26 +304,6 @@ A channel is a representation of a sequence of messages. It has meta data like c
# Messages # Messages
Messages represent individual messages in the chat system. Messages are typed, indicating the event which triggered the message.
Currently expected message types are:
| Name | Description |
| ---- | ----------- |
| CREATE | The first message when the channel is created |
| TOPIC | A member changed the topic of the channel |
| RENAME | A member renamed the channel |
| MESSAGE | A member posted a message to the channel |
| FILE | A member uploaded a file to the channel |
The following event types may be sent with a message event:
| Name | Description |
| ---- | ----------- |
| CREATED | A message has been created on a channel |
| EDITED | A message has been edited by the sender |
| REMOVED | A message has been removed by the sender |
| Method | Endpoint | Purpose | | Method | Endpoint | Purpose |
| ------ | -------- | ------- | | ------ | -------- | ------- |
| `POST` | `/channels/{channelID}/messages/` | Post new message to the channel | | `POST` | `/channels/{channelID}/messages/` | Post new message to the channel |

View File

@@ -70,10 +70,10 @@ func rolesResetCmd(ctx context.Context, db *factory.DB) func(cmd *cobra.Command,
{2, "compose", "namespace.create", 2}, {2, "compose", "namespace.create", 2},
{2, "compose", "access", 2}, {2, "compose", "access", 2},
{2, "compose", "grant", 2}, {2, "compose", "grant", 2},
{2, "compose:namespace:*", "page.create", 2},
{2, "compose:namespace:*", "read", 2}, {2, "compose:namespace:*", "read", 2},
{2, "compose:namespace:*", "update", 2}, {2, "compose:namespace:*", "update", 2},
{2, "compose:namespace:*", "delete", 2}, {2, "compose:namespace:*", "delete", 2},
{2, "compose:namespace:*", "page.create", 2},
{2, "compose:namespace:*", "module.create", 2}, {2, "compose:namespace:*", "module.create", 2},
{2, "compose:namespace:*", "chart.create", 2}, {2, "compose:namespace:*", "chart.create", 2},
{2, "compose:namespace:*", "trigger.create", 2}, {2, "compose:namespace:*", "trigger.create", 2},

View File

@@ -14,5 +14,5 @@ func (e repositoryError) Error() string {
} }
func (e repositoryError) String() string { func (e repositoryError) String() string {
return "crust.auth.repository." + string(e) return "crust.system.repository." + string(e)
} }