diff --git a/api/compose/spec.json b/api/compose/spec.json index 5a3c7f89d..66af9eb76 100644 --- a/api/compose/spec.json +++ b/api/compose/spec.json @@ -159,7 +159,7 @@ { "type": "*time.Time", "name": "updatedAt", - "required": true, + "required": false, "title": "Last update (or creation) date" } ] @@ -755,7 +755,8 @@ "struct": [ { "imports": [ - "sqlxTypes github.com/jmoiron/sqlx/types" + "sqlxTypes github.com/jmoiron/sqlx/types", + "time" ] } ], @@ -773,13 +774,35 @@ { "name": "list", "method": "GET", - "title": "List/read charts from module section", - "path": "/" + "title": "List/read charts", + "path": "/", + "parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against charts", + "type": "string" + }, + { + "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": "List/read charts from module section", + "title": "List/read charts ", "path": "/", "parameters": { "post": [ @@ -801,7 +824,7 @@ { "name": "read", "method": "GET", - "title": "Read charts by ID from module section", + "title": "Read charts by ID", "path": "/{chartID}", "parameters": { "path": [ @@ -817,7 +840,7 @@ { "name": "update", "method": "POST", - "title": "Add/update charts in module section", + "title": "Add/update charts", "path": "/{chartID}", "parameters": { "path": [ @@ -840,6 +863,12 @@ "title": "Chart name", "type": "string", "required": true + }, + { + "type": "*time.Time", + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date" } ] } diff --git a/api/compose/spec/chart.json b/api/compose/spec/chart.json index efa46aded..d2e5dc902 100644 --- a/api/compose/spec/chart.json +++ b/api/compose/spec/chart.json @@ -4,7 +4,8 @@ "Struct": [ { "imports": [ - "sqlxTypes github.com/jmoiron/sqlx/types" + "sqlxTypes github.com/jmoiron/sqlx/types", + "time" ] } ], @@ -25,14 +26,35 @@ { "Name": "list", "Method": "GET", - "Title": "List/read charts from module section", + "Title": "List/read charts", "Path": "/", - "Parameters": null + "Parameters": { + "get": [ + { + "name": "query", + "required": false, + "title": "Search query to match against charts", + "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": "List/read charts from module section", + "Title": "List/read charts ", "Path": "/", "Parameters": { "post": [ @@ -54,7 +76,7 @@ { "Name": "read", "Method": "GET", - "Title": "Read charts by ID from module section", + "Title": "Read charts by ID", "Path": "/{chartID}", "Parameters": { "path": [ @@ -70,7 +92,7 @@ { "Name": "update", "Method": "POST", - "Title": "Add/update charts in module section", + "Title": "Add/update charts", "Path": "/{chartID}", "Parameters": { "path": [ @@ -93,6 +115,12 @@ "required": true, "title": "Chart name", "type": "string" + }, + { + "name": "updatedAt", + "required": false, + "title": "Last update (or creation) date", + "type": "*time.Time" } ] } diff --git a/api/compose/spec/namespace.json b/api/compose/spec/namespace.json index 3b8588e4f..bf994f511 100644 --- a/api/compose/spec/namespace.json +++ b/api/compose/spec/namespace.json @@ -133,7 +133,7 @@ }, { "name": "updatedAt", - "required": true, + "required": false, "title": "Last update (or creation) date", "type": "*time.Time" } diff --git a/compose/internal/repository/chart.go b/compose/internal/repository/chart.go index 9bd1c0db8..70d432dce 100644 --- a/compose/internal/repository/chart.go +++ b/compose/internal/repository/chart.go @@ -5,6 +5,7 @@ import ( "time" "github.com/titpetric/factory" + "gopkg.in/Masterminds/squirrel.v1" "github.com/crusttech/crust/compose/types" ) @@ -13,11 +14,11 @@ type ( ChartRepository interface { With(ctx context.Context, db *factory.DB) ChartRepository - FindByID(id uint64) (*types.Chart, error) - Find() (types.ChartSet, error) + FindByID(namespaceID, attachmentID uint64) (*types.Chart, error) + Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) Create(mod *types.Chart) (*types.Chart, error) Update(mod *types.Chart) (*types.Chart, error) - DeleteByID(id uint64) error + DeleteByID(namespaceID, attachmentID uint64) error } chart struct { @@ -25,48 +26,100 @@ type ( } ) -const sqlChartColumns = "id, name, config, " + - "created_at, updated_at, deleted_at" - -const sqlChartSelect = "SELECT " + sqlChartColumns + " FROM compose_chart" +const ( + ErrChartNotFound = repositoryError("ChartNotFound") +) func Chart(ctx context.Context, db *factory.DB) ChartRepository { return (&chart{}).With(ctx, db) } -func (r *chart) With(ctx context.Context, db *factory.DB) ChartRepository { +func (r chart) With(ctx context.Context, db *factory.DB) ChartRepository { return &chart{ repository: r.repository.With(ctx, db), } } -func (r *chart) FindByID(id uint64) (*types.Chart, error) { - mod := &types.Chart{} - if err := r.db().Get(mod, sqlChartSelect+" WHERE id = ?", id); err != nil { - return nil, err +func (r chart) table() string { + return "compose_chart" +} + +func (r chart) columns() []string { + return []string{ + "id", "rel_namespace", "name", "config", + "created_at", "updated_at", "deleted_at", } - return mod, nil } -func (r *chart) Find() (types.ChartSet, error) { - mod := types.ChartSet{} - return mod, r.db().Select(&mod, sqlChartSelect+" ORDER BY id ASC") +func (r chart) query() squirrel.SelectBuilder { + return squirrel. + Select(). + From(r.table()). + Where("deleted_at IS NULL") + } -func (r *chart) Create(mod *types.Chart) (*types.Chart, error) { +func (r chart) FindByID(namespaceID, chartID uint64) (*types.Chart, error) { + var ( + query = r.query(). + Columns(r.columns()...). + Where("id = ?", chartID) + + c = &types.Chart{} + ) + + if namespaceID > 0 { + query = query.Where("rel_namespace = ?", namespaceID) + } + + return c, isFound(r.fetchOne(c, query), c.ID > 0, ErrChartNotFound) +} + +func (r chart) Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, 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 chart) Create(mod *types.Chart) (*types.Chart, error) { mod.ID = factory.Sonyflake.NextID() mod.CreatedAt = time.Now() - return mod, r.db().Insert("compose_chart", mod) + return mod, r.db().Insert(r.table(), mod) } -func (r *chart) Update(mod *types.Chart) (*types.Chart, error) { +func (r chart) Update(mod *types.Chart) (*types.Chart, error) { now := time.Now() mod.UpdatedAt = &now - return mod, r.db().Replace("compose_chart", mod) + return mod, r.db().Replace(r.table(), mod) } -func (r *chart) DeleteByID(id uint64) error { - _, err := r.db().Exec("DELETE FROM compose_chart WHERE id = ?", id) +func (r chart) 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 } diff --git a/compose/internal/service/chart.go b/compose/internal/service/chart.go index c1684c0f3..60ab1c29a 100644 --- a/compose/internal/service/chart.go +++ b/compose/internal/service/chart.go @@ -3,7 +3,6 @@ package service import ( "context" - "github.com/pkg/errors" "github.com/titpetric/factory" "github.com/crusttech/crust/compose/internal/repository" @@ -23,12 +22,12 @@ type ( ChartService interface { With(ctx context.Context) ChartService - FindByID(chartID uint64) (*types.Chart, error) - Find() (types.ChartSet, error) + FindByID(namespaceID, chartID uint64) (*types.Chart, error) + Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) Create(chart *types.Chart) (*types.Chart, error) Update(chart *types.Chart) (*types.Chart, error) - DeleteByID(chartID uint64) error + DeleteByID(namespaceID, chartID uint64) error } ) @@ -50,73 +49,74 @@ func (svc *chart) With(ctx context.Context) ChartService { } } -func (svc *chart) FindByID(chartID uint64) (c *types.Chart, err error) { - if c, err = svc.chartRepo.FindByID(chartID); err != nil { +func (svc *chart) FindByID(namespaceID, chartID uint64) (c *types.Chart, err error) { + if namespaceID == 0 { + return nil, ErrNamespaceRequired + } + + if c, err = svc.chartRepo.FindByID(namespaceID, chartID); err != nil { return } else if !svc.prmSvc.CanReadChart(c) { - return nil, errors.New("not allowed to access this chart") + return nil, ErrNoReadPermissions.withStack() } return } -func (svc *chart) Find() (cc types.ChartSet, err error) { - if cc, err = svc.chartRepo.Find(); err != nil { - return nil, err - } else { - return cc.Filter(func(m *types.Chart) (bool, error) { - return svc.prmSvc.CanReadChart(m), nil - }) +func (svc *chart) Find(filter types.ChartFilter) (set types.ChartSet, f types.ChartFilter, err error) { + set, f, err = svc.chartRepo.Find(filter) + if err != nil { + return } + + set, _ = set.Filter(func(m *types.Chart) (bool, error) { + return svc.prmSvc.CanReadChart(m), nil + }) + + return } func (svc *chart) Create(mod *types.Chart) (c *types.Chart, err error) { if !svc.prmSvc.CanCreateChart(crmNamespace()) { - return nil, errors.New("not allowed to create this chart") + return nil, ErrNoCreatePermissions.withStack() } - return c, svc.db.Transaction(func() error { - c, err = svc.chartRepo.Create(mod) - return err - }) + return svc.chartRepo.Create(mod) } func (svc *chart) Update(mod *types.Chart) (c *types.Chart, err error) { - validate := func() error { - if mod.ID == 0 { - return errors.New("Error updating chart: invalid ID") - } else if c, err = svc.chartRepo.FindByID(mod.ID); err != nil { - return errors.Wrap(err, "Error while loading chart for update") - } else { - if !svc.prmSvc.CanUpdateChart(c) { - return errors.New("not allowed to update this chart") - } - - mod.CreatedAt = c.CreatedAt - } - - return nil + if mod.ID == 0 { + return nil, ErrInvalidID.withStack() } - if err = validate(); err != nil { - return nil, err + if c, err = svc.chartRepo.FindByID(mod.NamespaceID, mod.ID); err != nil { + return + } + + if isStale(mod.UpdatedAt, c.UpdatedAt, c.CreatedAt) { + return nil, ErrStaleData.withStack() + } + + if !svc.prmSvc.CanUpdateChart(c) { + return nil, ErrNoUpdatePermissions.withStack() } c.Config = mod.Config c.Name = mod.Name - return c, svc.db.Transaction(func() error { - c, err = svc.chartRepo.Update(c) - return err - }) + return svc.chartRepo.Update(c) } -func (svc *chart) DeleteByID(ID uint64) error { - if c, err := svc.chartRepo.FindByID(ID); err != nil { - return errors.Wrap(err, "could not delete chart") - } else if !svc.prmSvc.CanDeleteChart(c) { - return errors.New("not allowed to delete this chart") +func (svc *chart) DeleteByID(namespaceID, chartID uint64) error { + if namespaceID == 0 { + return ErrNamespaceRequired.withStack() } - return svc.chartRepo.DeleteByID(ID) + if c, err := svc.chartRepo.FindByID(namespaceID, chartID); err != nil { + return err + } else if !svc.prmSvc.CanDeleteChart(c) { + return ErrNoDeletePermissions.withStack() + } + + return svc.chartRepo.DeleteByID(namespaceID, chartID) } diff --git a/compose/rest/chart.go b/compose/rest/chart.go index e38f84016..0dde7f717 100644 --- a/compose/rest/chart.go +++ b/compose/rest/chart.go @@ -14,45 +14,109 @@ import ( var _ = errors.Wrap -type Chart struct { - module service.ModuleService - chart service.ChartService -} +type ( + chartPayload struct { + *types.Chart + + CanUpdateChart bool `json:"canUpdateChart"` + CanDeleteChart bool `json:"canDeleteChart"` + } + + chartSetPayload struct { + Filter types.ChartFilter `json:"filter"` + Set []*chartPayload `json:"set"` + } + + Chart struct { + chart service.ChartService + permissions service.PermissionsService + } +) func (Chart) New() *Chart { return &Chart{ - module: service.DefaultModule, - chart: service.DefaultChart, + chart: service.DefaultChart, + permissions: service.DefaultPermissions, } } -func (ctrl *Chart) List(ctx context.Context, r *request.ChartList) (interface{}, error) { - return ctrl.chart.With(ctx).Find() -} - -func (ctrl *Chart) Create(ctx context.Context, r *request.ChartCreate) (interface{}, error) { - chart := &types.Chart{ - Name: r.Name, - Config: r.Config, +func (ctrl Chart) List(ctx context.Context, r *request.ChartList) (interface{}, error) { + f := types.ChartFilter{ + Query: r.Query, + PerPage: r.PerPage, + Page: r.Page, } - return ctrl.chart.With(ctx).Create(chart) + set, filter, err := ctrl.chart.With(ctx).Find(f) + return ctrl.makeFilterPayload(ctx, set, filter, err) } -func (ctrl *Chart) Read(ctx context.Context, r *request.ChartRead) (interface{}, error) { - return ctrl.chart.With(ctx).FindByID(r.ChartID) -} - -func (ctrl *Chart) Update(ctx context.Context, r *request.ChartUpdate) (interface{}, error) { - chart := &types.Chart{ - ID: r.ChartID, - Name: r.Name, - Config: r.Config, +func (ctrl Chart) Create(ctx context.Context, r *request.ChartCreate) (interface{}, error) { + var err error + ns := &types.Chart{ + NamespaceID: r.NamespaceID, + Name: r.Name, + Config: r.Config, } - return ctrl.chart.With(ctx).Update(chart) + ns, err = ctrl.chart.With(ctx).Create(ns) + return ctrl.makePayload(ctx, ns, err) } -func (ctrl *Chart) Delete(ctx context.Context, r *request.ChartDelete) (interface{}, error) { - return resputil.OK(), ctrl.chart.With(ctx).DeleteByID(r.ChartID) +func (ctrl Chart) Read(ctx context.Context, r *request.ChartRead) (interface{}, error) { + return ctrl.chart.With(ctx).FindByID(r.NamespaceID, r.ChartID) +} + +func (ctrl Chart) Update(ctx context.Context, r *request.ChartUpdate) (interface{}, error) { + var ( + ns = &types.Chart{} + err error + ) + + ns.ID = r.ChartID + ns.Name = r.Name + ns.Config = r.Config + ns.NamespaceID = r.NamespaceID + ns.UpdatedAt = r.UpdatedAt + + ns, err = ctrl.chart.With(ctx).Update(ns) + return ctrl.makePayload(ctx, ns, err) +} + +func (ctrl Chart) Delete(ctx context.Context, r *request.ChartDelete) (interface{}, error) { + _, err := ctrl.chart.With(ctx).FindByID(r.NamespaceID, r.ChartID) + if err != nil { + return nil, err + } + + return resputil.OK(), ctrl.chart.With(ctx).DeleteByID(r.NamespaceID, r.ChartID) +} + +func (ctrl Chart) makePayload(ctx context.Context, ns *types.Chart, err error) (*chartPayload, error) { + if err != nil || ns == nil { + return nil, err + } + + perm := ctrl.permissions.With(ctx) + + return &chartPayload{ + Chart: ns, + + CanUpdateChart: perm.CanUpdateChart(ns), + CanDeleteChart: perm.CanDeleteChart(ns), + }, nil +} + +func (ctrl Chart) makeFilterPayload(ctx context.Context, nn types.ChartSet, f types.ChartFilter, err error) (*chartSetPayload, error) { + if err != nil { + return nil, err + } + + nsp := &chartSetPayload{Filter: f, Set: make([]*chartPayload, len(nn))} + + for i := range nn { + nsp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil) + } + + return nsp, nil } diff --git a/compose/rest/request/chart.go b/compose/rest/request/chart.go index b90d61713..6002c4dcf 100644 --- a/compose/rest/request/chart.go +++ b/compose/rest/request/chart.go @@ -27,6 +27,7 @@ import ( "github.com/pkg/errors" sqlxTypes "github.com/jmoiron/sqlx/types" + "time" ) var _ = chi.URLParam @@ -34,6 +35,9 @@ var _ = multipart.FileHeader{} // Chart list request parameters type ChartList struct { + Query string + Page uint + PerPage uint NamespaceID uint64 `json:",string"` } @@ -68,6 +72,18 @@ func (cReq *ChartList) Fill(r *http.Request) (err error) { post[name] = string(param[0]) } + if val, ok := get["query"]; ok { + + cReq.Query = val + } + if val, ok := get["page"]; ok { + + cReq.Page = parseUint(val) + } + if val, ok := get["perPage"]; ok { + + cReq.PerPage = parseUint(val) + } cReq.NamespaceID = parseUInt64(chi.URLParam(r, "namespaceID")) return err @@ -181,6 +197,7 @@ type ChartUpdate struct { NamespaceID uint64 `json:",string"` Config sqlxTypes.JSONText Name string + UpdatedAt *time.Time } func NewChartUpdate() *ChartUpdate { @@ -226,6 +243,12 @@ func (cReq *ChartUpdate) Fill(r *http.Request) (err error) { cReq.Name = val } + if val, ok := post["updatedAt"]; ok { + + if cReq.UpdatedAt, err = parseISODatePtrWithErr(val); err != nil { + return err + } + } return err } diff --git a/compose/types/chart.go b/compose/types/chart.go index 0832a8e43..3930aaaf9 100644 --- a/compose/types/chart.go +++ b/compose/types/chart.go @@ -22,11 +22,12 @@ type ( } ChartFilter struct { - Query string `json:"query"` - Page uint `json:"page"` - PerPage uint `json:"perPage"` - Sort string `json:"sort"` - Count uint `json:"count"` + 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"` } ) diff --git a/docs/compose/README.md b/docs/compose/README.md index 5e81254c1..f833825a7 100644 --- a/docs/compose/README.md +++ b/docs/compose/README.md @@ -115,13 +115,13 @@ | Method | Endpoint | Purpose | | ------ | -------- | ------- | -| `GET` | `/namespace/{namespaceID}/chart/` | List/read charts from module section | -| `POST` | `/namespace/{namespaceID}/chart/` | List/read charts from module section | -| `GET` | `/namespace/{namespaceID}/chart/{chartID}` | Read charts by ID from module section | -| `POST` | `/namespace/{namespaceID}/chart/{chartID}` | Add/update charts in module section | +| `GET` | `/namespace/{namespaceID}/chart/` | List/read charts | +| `POST` | `/namespace/{namespaceID}/chart/` | List/read charts | +| `GET` | `/namespace/{namespaceID}/chart/{chartID}` | Read charts by ID | +| `POST` | `/namespace/{namespaceID}/chart/{chartID}` | Add/update charts | | `DELETE` | `/namespace/{namespaceID}/chart/{chartID}` | Delete chart | -## List/read charts from module section +## List/read charts #### Method @@ -133,9 +133,12 @@ | Parameter | Type | Method | Description | Default | Required? | | --------- | ---- | ------ | ----------- | ------- | --------- | +| query | string | GET | Search query to match against charts | 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 | -## List/read charts from module section +## List/read charts #### Method @@ -151,7 +154,7 @@ | name | string | POST | Chart name | N/A | YES | | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -## Read charts by ID from module section +## Read charts by ID #### Method @@ -166,7 +169,7 @@ | chartID | uint64 | PATH | Chart ID | N/A | YES | | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | -## Add/update charts in module section +## Add/update charts #### Method @@ -182,6 +185,7 @@ | namespaceID | uint64 | PATH | Namespace ID | N/A | YES | | config | sqlxTypes.JSONText | POST | Chart JSON | N/A | YES | | name | string | POST | Chart name | N/A | YES | +| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | ## Delete chart @@ -374,7 +378,7 @@ Compose module definitions | 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 | +| updatedAt | *time.Time | POST | Last update (or creation) date | N/A | NO | ## Delete namespace