diff --git a/compose/rest.yaml b/compose/rest.yaml index ce0c5df37..c7c9b7974 100644 --- a/compose/rest.yaml +++ b/compose/rest.yaml @@ -11,6 +11,7 @@ endpoints: path: "/namespace" authentication: [] imports: + - github.com/cortezaproject/corteza-server/pkg/label - sqlxTypes github.com/jmoiron/sqlx/types - time apis: @@ -31,6 +32,10 @@ endpoints: - type: uint name: limit title: Limit + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: string name: pageCursor title: Page cursor @@ -47,6 +52,10 @@ endpoints: name: name required: true title: Name + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: string name: slug required: false @@ -96,6 +105,10 @@ endpoints: name: meta required: true title: Meta data + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: "*time.Time" name: updatedAt required: false @@ -132,6 +145,7 @@ endpoints: authentication: [] imports: - sqlxTypes github.com/jmoiron/sqlx/types + - github.com/cortezaproject/corteza-server/pkg/label parameters: path: - type: uint64 @@ -157,6 +171,10 @@ endpoints: name: handle required: false title: Search by handle + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: uint name: limit title: Limit @@ -196,6 +214,10 @@ endpoints: name: weight required: false title: Page tree weight + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: bool name: visible required: false @@ -254,6 +276,10 @@ endpoints: name: weight required: false title: Page tree weight + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: bool name: visible required: false @@ -331,6 +357,7 @@ endpoints: imports: - sqlxTypes github.com/jmoiron/sqlx/types - github.com/cortezaproject/corteza-server/compose/types + - github.com/cortezaproject/corteza-server/pkg/label - time apis: - name: list @@ -357,6 +384,10 @@ endpoints: - type: string name: pageCursor title: Page cursor + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: string name: sort title: Sort items @@ -382,6 +413,10 @@ endpoints: name: meta required: true title: Module meta data + - type: map[string]string + name: labels + title: Module labels + parser: label.ParseStrings - name: read method: GET title: Read module @@ -423,6 +458,10 @@ endpoints: name: updatedAt required: false title: Last update (or creation) date + - type: map[string]string + name: labels + title: Module labels + parser: label.ParseStrings - name: delete method: DELETE title: Delete module @@ -454,6 +493,7 @@ endpoints: path: "/namespace/{namespaceID}/module/{moduleID}/record" authentication: [] imports: + - github.com/cortezaproject/corteza-server/pkg/label - github.com/cortezaproject/corteza-server/compose/types parameters: path: @@ -498,6 +538,10 @@ endpoints: type: string required: false title: Filtering condition (same as query, deprecated) + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: deleted required: false title: Exclude (0, default), include (1) or return only (2) deleted records @@ -606,6 +650,10 @@ endpoints: name: records required: false title: Records + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: read method: GET title: Read records by ID from module section @@ -635,6 +683,10 @@ endpoints: name: records required: false title: Records + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: bulkDelete method: DELETE title: Delete record row from module section @@ -712,6 +764,7 @@ endpoints: authentication: [] imports: - sqlxTypes github.com/jmoiron/sqlx/types + - github.com/cortezaproject/corteza-server/pkg/label - time parameters: path: @@ -734,6 +787,10 @@ endpoints: required: false title: Search charts by handle type: string + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: uint name: limit title: Limit @@ -761,6 +818,10 @@ endpoints: title: Chart handle type: string required: false + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - name: read method: GET title: Read charts by ID @@ -794,6 +855,10 @@ endpoints: title: Chart handle type: string required: false + - type: map[string]string + name: labels + title: Labels + parser: label.ParseStrings - type: "*time.Time" name: updatedAt required: false diff --git a/compose/rest/chart.go b/compose/rest/chart.go index d2e3c3699..312316fef 100644 --- a/compose/rest/chart.go +++ b/compose/rest/chart.go @@ -54,6 +54,7 @@ func (ctrl Chart) List(ctx context.Context, r *request.ChartList) (interface{}, Handle: r.Handle, Query: r.Query, + Labels: r.Labels, } ) @@ -75,6 +76,7 @@ func (ctrl Chart) Create(ctx context.Context, r *request.ChartCreate) (interface NamespaceID: r.NamespaceID, Name: r.Name, Handle: r.Handle, + Labels: r.Labels, } if len(r.Config) > 2 { @@ -101,6 +103,7 @@ func (ctrl Chart) Update(ctx context.Context, r *request.ChartUpdate) (interface Handle: r.Handle, NamespaceID: r.NamespaceID, UpdatedAt: r.UpdatedAt, + Labels: r.Labels, } ) diff --git a/compose/rest/module.go b/compose/rest/module.go index dd2ca33c9..bb77a6500 100644 --- a/compose/rest/module.go +++ b/compose/rest/module.go @@ -79,6 +79,7 @@ func (ctrl *Module) List(ctx context.Context, r *request.ModuleList) (interface{ Query: r.Query, Name: r.Name, Handle: r.Handle, + Labels: r.Labels, } ) @@ -108,6 +109,7 @@ func (ctrl *Module) Create(ctx context.Context, r *request.ModuleCreate) (interf Handle: r.Handle, Fields: r.Fields, Meta: r.Meta, + Labels: r.Labels, } ) @@ -125,6 +127,7 @@ func (ctrl *Module) Update(ctx context.Context, r *request.ModuleUpdate) (interf Handle: r.Handle, Fields: r.Fields, Meta: r.Meta, + Labels: r.Labels, UpdatedAt: r.UpdatedAt, } ) diff --git a/compose/rest/namespace.go b/compose/rest/namespace.go index a19bd6000..92110e174 100644 --- a/compose/rest/namespace.go +++ b/compose/rest/namespace.go @@ -58,8 +58,9 @@ func (ctrl Namespace) List(ctx context.Context, r *request.NamespaceList) (inter var ( err error f = types.NamespaceFilter{ - Query: r.Query, - Slug: r.Slug, + Query: r.Query, + Slug: r.Slug, + Labels: r.Labels, } ) @@ -82,6 +83,7 @@ func (ctrl Namespace) Create(ctx context.Context, r *request.NamespaceCreate) (i Name: r.Name, Slug: r.Slug, Enabled: r.Enabled, + Labels: r.Labels, } ) @@ -106,6 +108,7 @@ func (ctrl Namespace) Update(ctx context.Context, r *request.NamespaceUpdate) (i Name: r.Name, Slug: r.Slug, Enabled: r.Enabled, + Labels: r.Labels, UpdatedAt: r.UpdatedAt, } ) diff --git a/compose/rest/page.go b/compose/rest/page.go index 60fd4a577..5068e8fce 100644 --- a/compose/rest/page.go +++ b/compose/rest/page.go @@ -58,6 +58,7 @@ func (ctrl *Page) List(ctx context.Context, r *request.PageList) (interface{}, e f = types.PageFilter{ NamespaceID: r.NamespaceID, ParentID: r.SelfID, + Labels: r.Labels, Handle: r.Handle, Query: r.Query, @@ -93,6 +94,7 @@ func (ctrl *Page) Create(ctx context.Context, r *request.PageCreate) (interface{ Description: r.Description, Visible: r.Visible, Weight: r.Weight, + Labels: r.Labels, } ) @@ -128,6 +130,7 @@ func (ctrl *Page) Update(ctx context.Context, r *request.PageUpdate) (interface{ Description: r.Description, Visible: r.Visible, Weight: r.Weight, + Labels: r.Labels, } ) diff --git a/compose/rest/record.go b/compose/rest/record.go index 066905c4a..75978bf97 100644 --- a/compose/rest/record.go +++ b/compose/rest/record.go @@ -81,6 +81,7 @@ func (ctrl *Record) List(ctx context.Context, r *request.RecordList) (interface{ f = types.RecordFilter{ NamespaceID: r.NamespaceID, ModuleID: r.ModuleID, + Labels: r.Labels, Deleted: filter.State(r.Deleted), } ) @@ -153,6 +154,7 @@ func (ctrl *Record) Create(ctx context.Context, r *request.RecordCreate) (interf NamespaceID: r.NamespaceID, ModuleID: r.ModuleID, Values: r.Values, + Labels: r.Labels, } oo = append(oo, &types.RecordBulkOperation{ Record: rr, @@ -201,6 +203,7 @@ func (ctrl *Record) Update(ctx context.Context, r *request.RecordUpdate) (interf NamespaceID: r.NamespaceID, ModuleID: r.ModuleID, Values: r.Values, + Labels: r.Labels, } oo = append(oo, &types.RecordBulkOperation{ Record: rr, diff --git a/compose/rest/request/chart.go b/compose/rest/request/chart.go index 3bccf427d..2f2183fc4 100644 --- a/compose/rest/request/chart.go +++ b/compose/rest/request/chart.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" sqlxTypes "github.com/jmoiron/sqlx/types" @@ -47,6 +48,11 @@ type ( // Search charts by handle Handle string + // Labels GET parameter + // + // Labels + Labels map[string]string + // Limit GET parameter // // Limit @@ -83,6 +89,11 @@ type ( // // Chart handle Handle string + + // Labels POST parameter + // + // Labels + Labels map[string]string } ChartRead struct { @@ -123,6 +134,11 @@ type ( // Chart handle Handle string + // Labels POST parameter + // + // Labels + Labels map[string]string + // UpdatedAt POST parameter // // Last update (or creation) date @@ -153,6 +169,7 @@ func (r ChartList) Auditable() map[string]interface{} { "namespaceID": r.NamespaceID, "query": r.Query, "handle": r.Handle, + "labels": r.Labels, "limit": r.Limit, "pageCursor": r.PageCursor, "sort": r.Sort, @@ -174,6 +191,11 @@ func (r ChartList) GetHandle() string { return r.Handle } +// Auditable returns all auditable/loggable parameters +func (r ChartList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r ChartList) GetLimit() uint { return r.Limit @@ -218,6 +240,17 @@ func (r *ChartList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["limit"]; ok && len(val) > 0 { r.Limit, err = payload.ParseUint(val[0]), nil if err != nil { @@ -265,6 +298,7 @@ func (r ChartCreate) Auditable() map[string]interface{} { "config": r.Config, "name": r.Name, "handle": r.Handle, + "labels": r.Labels, } } @@ -288,6 +322,11 @@ func (r ChartCreate) GetHandle() string { return r.Handle } +// Auditable returns all auditable/loggable parameters +func (r ChartCreate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *ChartCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -328,6 +367,18 @@ func (r *ChartCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { @@ -415,6 +466,7 @@ func (r ChartUpdate) Auditable() map[string]interface{} { "config": r.Config, "name": r.Name, "handle": r.Handle, + "labels": r.Labels, "updatedAt": r.UpdatedAt, } } @@ -444,6 +496,11 @@ func (r ChartUpdate) GetHandle() string { return r.Handle } +// Auditable returns all auditable/loggable parameters +func (r ChartUpdate) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r ChartUpdate) GetUpdatedAt() *time.Time { return r.UpdatedAt @@ -490,6 +547,18 @@ func (r *ChartUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } + if val, ok := req.Form["updatedAt"]; ok && len(val) > 0 { r.UpdatedAt, err = payload.ParseISODatePtrWithErr(val[0]) if err != nil { diff --git a/compose/rest/request/module.go b/compose/rest/request/module.go index 47474a984..45c74bbdf 100644 --- a/compose/rest/request/module.go +++ b/compose/rest/request/module.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" sqlxTypes "github.com/jmoiron/sqlx/types" @@ -63,6 +64,11 @@ type ( // Page cursor PageCursor string + // Labels GET parameter + // + // Labels + Labels map[string]string + // Sort GET parameter // // Sort items @@ -94,6 +100,11 @@ type ( // // Module meta data Meta sqlxTypes.JSONText + + // Labels POST parameter + // + // Module labels + Labels map[string]string } ModuleRead struct { @@ -143,6 +154,11 @@ type ( // // Last update (or creation) date UpdatedAt *time.Time + + // Labels POST parameter + // + // Module labels + Labels map[string]string } ModuleDelete struct { @@ -189,6 +205,7 @@ func (r ModuleList) Auditable() map[string]interface{} { "handle": r.Handle, "limit": r.Limit, "pageCursor": r.PageCursor, + "labels": r.Labels, "sort": r.Sort, } } @@ -223,6 +240,11 @@ func (r ModuleList) GetPageCursor() string { return r.PageCursor } +// Auditable returns all auditable/loggable parameters +func (r ModuleList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r ModuleList) GetSort() string { return r.Sort @@ -275,6 +297,17 @@ func (r *ModuleList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["sort"]; ok && len(val) > 0 { r.Sort, err = val[0], nil if err != nil { @@ -311,6 +344,7 @@ func (r ModuleCreate) Auditable() map[string]interface{} { "handle": r.Handle, "fields": r.Fields, "meta": r.Meta, + "labels": r.Labels, } } @@ -339,6 +373,11 @@ func (r ModuleCreate) GetMeta() sqlxTypes.JSONText { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r ModuleCreate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *ModuleCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -386,6 +425,18 @@ func (r *ModuleCreate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { @@ -475,6 +526,7 @@ func (r ModuleUpdate) Auditable() map[string]interface{} { "fields": r.Fields, "meta": r.Meta, "updatedAt": r.UpdatedAt, + "labels": r.Labels, } } @@ -513,6 +565,11 @@ func (r ModuleUpdate) GetUpdatedAt() *time.Time { return r.UpdatedAt } +// Auditable returns all auditable/loggable parameters +func (r ModuleUpdate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *ModuleUpdate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -567,6 +624,18 @@ func (r *ModuleUpdate) Fill(req *http.Request) (err error) { return err } } + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { diff --git a/compose/rest/request/namespace.go b/compose/rest/request/namespace.go index 74f05bc0b..2395b31fc 100644 --- a/compose/rest/request/namespace.go +++ b/compose/rest/request/namespace.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" sqlxTypes "github.com/jmoiron/sqlx/types" @@ -47,6 +48,11 @@ type ( // Limit Limit uint + // Labels GET parameter + // + // Labels + Labels map[string]string + // PageCursor GET parameter // // Page cursor @@ -64,6 +70,11 @@ type ( // Name Name string + // Labels POST parameter + // + // Labels + Labels map[string]string + // Slug POST parameter // // Slug (url path part) @@ -113,6 +124,11 @@ type ( // Meta data Meta sqlxTypes.JSONText + // Labels POST parameter + // + // Labels + Labels map[string]string + // UpdatedAt POST parameter // // Last update (or creation) date @@ -150,6 +166,7 @@ func (r NamespaceList) Auditable() map[string]interface{} { "query": r.Query, "slug": r.Slug, "limit": r.Limit, + "labels": r.Labels, "pageCursor": r.PageCursor, "sort": r.Sort, } @@ -170,6 +187,11 @@ func (r NamespaceList) GetLimit() uint { return r.Limit } +// Auditable returns all auditable/loggable parameters +func (r NamespaceList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r NamespaceList) GetPageCursor() string { return r.PageCursor @@ -215,6 +237,17 @@ func (r *NamespaceList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["pageCursor"]; ok && len(val) > 0 { r.PageCursor, err = val[0], nil if err != nil { @@ -241,6 +274,7 @@ func NewNamespaceCreate() *NamespaceCreate { func (r NamespaceCreate) Auditable() map[string]interface{} { return map[string]interface{}{ "name": r.Name, + "labels": r.Labels, "slug": r.Slug, "enabled": r.Enabled, "meta": r.Meta, @@ -252,6 +286,11 @@ func (r NamespaceCreate) GetName() string { return r.Name } +// Auditable returns all auditable/loggable parameters +func (r NamespaceCreate) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r NamespaceCreate) GetSlug() string { return r.Slug @@ -294,6 +333,18 @@ func (r *NamespaceCreate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } + if val, ok := req.Form["slug"]; ok && len(val) > 0 { r.Slug, err = val[0], nil if err != nil { @@ -377,6 +428,7 @@ func (r NamespaceUpdate) Auditable() map[string]interface{} { "slug": r.Slug, "enabled": r.Enabled, "meta": r.Meta, + "labels": r.Labels, "updatedAt": r.UpdatedAt, } } @@ -406,6 +458,11 @@ func (r NamespaceUpdate) GetMeta() sqlxTypes.JSONText { return r.Meta } +// Auditable returns all auditable/loggable parameters +func (r NamespaceUpdate) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r NamespaceUpdate) GetUpdatedAt() *time.Time { return r.UpdatedAt @@ -459,6 +516,18 @@ func (r *NamespaceUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } + if val, ok := req.Form["updatedAt"]; ok && len(val) > 0 { r.UpdatedAt, err = payload.ParseISODatePtrWithErr(val[0]) if err != nil { diff --git a/compose/rest/request/page.go b/compose/rest/request/page.go index 06df2a9c2..715a7fa6e 100644 --- a/compose/rest/request/page.go +++ b/compose/rest/request/page.go @@ -11,6 +11,7 @@ package request import ( "encoding/json" "fmt" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" sqlxTypes "github.com/jmoiron/sqlx/types" @@ -51,6 +52,11 @@ type ( // Search by handle Handle string + // Labels GET parameter + // + // Labels + Labels map[string]string + // Limit GET parameter // // Limit @@ -103,6 +109,11 @@ type ( // Page tree weight Weight int + // Labels POST parameter + // + // Labels + Labels map[string]string + // Visible POST parameter // // Visible in navigation @@ -174,6 +185,11 @@ type ( // Page tree weight Weight int + // Labels POST parameter + // + // Labels + Labels map[string]string + // Visible POST parameter // // Visible in navigation @@ -261,6 +277,7 @@ func (r PageList) Auditable() map[string]interface{} { "selfID": r.SelfID, "query": r.Query, "handle": r.Handle, + "labels": r.Labels, "limit": r.Limit, "pageCursor": r.PageCursor, "sort": r.Sort, @@ -287,6 +304,11 @@ func (r PageList) GetHandle() string { return r.Handle } +// Auditable returns all auditable/loggable parameters +func (r PageList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r PageList) GetLimit() uint { return r.Limit @@ -337,6 +359,17 @@ func (r *PageList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["limit"]; ok && len(val) > 0 { r.Limit, err = payload.ParseUint(val[0]), nil if err != nil { @@ -387,6 +420,7 @@ func (r PageCreate) Auditable() map[string]interface{} { "handle": r.Handle, "description": r.Description, "weight": r.Weight, + "labels": r.Labels, "visible": r.Visible, "blocks": r.Blocks, } @@ -427,6 +461,11 @@ func (r PageCreate) GetWeight() int { return r.Weight } +// Auditable returns all auditable/loggable parameters +func (r PageCreate) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r PageCreate) GetVisible() bool { return r.Visible @@ -499,6 +538,18 @@ func (r *PageCreate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } + if val, ok := req.Form["visible"]; ok && len(val) > 0 { r.Visible, err = payload.ParseBool(val[0]), nil if err != nil { @@ -647,6 +698,7 @@ func (r PageUpdate) Auditable() map[string]interface{} { "handle": r.Handle, "description": r.Description, "weight": r.Weight, + "labels": r.Labels, "visible": r.Visible, "blocks": r.Blocks, } @@ -692,6 +744,11 @@ func (r PageUpdate) GetWeight() int { return r.Weight } +// Auditable returns all auditable/loggable parameters +func (r PageUpdate) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r PageUpdate) GetVisible() bool { return r.Visible @@ -764,6 +821,18 @@ func (r *PageUpdate) Fill(req *http.Request) (err error) { } } + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } + if val, ok := req.Form["visible"]; ok && len(val) > 0 { r.Visible, err = payload.ParseBool(val[0]), nil if err != nil { diff --git a/compose/rest/request/record.go b/compose/rest/request/record.go index c3109e872..d7e178cfc 100644 --- a/compose/rest/request/record.go +++ b/compose/rest/request/record.go @@ -12,6 +12,7 @@ import ( "encoding/json" "fmt" "github.com/cortezaproject/corteza-server/compose/types" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/payload" "github.com/go-chi/chi" "io" @@ -78,6 +79,11 @@ type ( // Filtering condition (same as query, deprecated) Filter string + // Labels GET parameter + // + // Labels + Labels map[string]string + // Deleted GET parameter // // Exclude (0, default), include (1) or return only (2) deleted records @@ -239,6 +245,11 @@ type ( // // Records Records types.RecordBulkSet + + // Labels POST parameter + // + // Labels + Labels map[string]string } RecordRead struct { @@ -283,6 +294,11 @@ type ( // // Records Records types.RecordBulkSet + + // Labels POST parameter + // + // Labels + Labels map[string]string } RecordBulkDelete struct { @@ -507,6 +523,7 @@ func (r RecordList) Auditable() map[string]interface{} { "moduleID": r.ModuleID, "query": r.Query, "filter": r.Filter, + "labels": r.Labels, "deleted": r.Deleted, "limit": r.Limit, "pageCursor": r.PageCursor, @@ -534,6 +551,11 @@ func (r RecordList) GetFilter() string { return r.Filter } +// Auditable returns all auditable/loggable parameters +func (r RecordList) GetLabels() map[string]string { + return r.Labels +} + // Auditable returns all auditable/loggable parameters func (r RecordList) GetDeleted() uint { return r.Deleted @@ -583,6 +605,17 @@ func (r *RecordList) Fill(req *http.Request) (err error) { return err } } + if val, ok := tmp["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := tmp["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } if val, ok := tmp["deleted"]; ok && len(val) > 0 { r.Deleted, err = payload.ParseUint(val[0]), nil if err != nil { @@ -1108,6 +1141,7 @@ func (r RecordCreate) Auditable() map[string]interface{} { "moduleID": r.ModuleID, "values": r.Values, "records": r.Records, + "labels": r.Labels, } } @@ -1131,6 +1165,11 @@ func (r RecordCreate) GetRecords() types.RecordBulkSet { return r.Records } +// Auditable returns all auditable/loggable parameters +func (r RecordCreate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *RecordCreate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -1164,6 +1203,18 @@ func (r *RecordCreate) Fill(req *http.Request) (err error) { // return err // } //} + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { @@ -1269,6 +1320,7 @@ func (r RecordUpdate) Auditable() map[string]interface{} { "recordID": r.RecordID, "values": r.Values, "records": r.Records, + "labels": r.Labels, } } @@ -1297,6 +1349,11 @@ func (r RecordUpdate) GetRecords() types.RecordBulkSet { return r.Records } +// Auditable returns all auditable/loggable parameters +func (r RecordUpdate) GetLabels() map[string]string { + return r.Labels +} + // Fill processes request and fills internal variables func (r *RecordUpdate) Fill(req *http.Request) (err error) { if strings.ToLower(req.Header.Get("content-type")) == "application/json" { @@ -1330,6 +1387,18 @@ func (r *RecordUpdate) Fill(req *http.Request) (err error) { // return err // } //} + + if val, ok := req.Form["labels[]"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } else if val, ok := req.Form["labels"]; ok { + r.Labels, err = label.ParseStrings(val) + if err != nil { + return err + } + } } { diff --git a/compose/service/chart.go b/compose/service/chart.go index 8317898da..0b7dd0972 100644 --- a/compose/service/chart.go +++ b/compose/service/chart.go @@ -6,7 +6,9 @@ import ( "github.com/cortezaproject/corteza-server/pkg/actionlog" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/store" + "reflect" ) type ( @@ -37,7 +39,15 @@ type ( DeleteByID(namespaceID, chartID uint64) error } - chartUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Chart) (bool, error) + chartUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Chart) (chartChanges, error) + + chartChanges uint8 +) + +const ( + chartUnchanged chartChanges = 0 + chartChanged chartChanges = 1 + chartLabelsChanged chartChanges = 2 ) func Chart() ChartService { @@ -77,10 +87,33 @@ func (svc chart) Find(filter types.ChartFilter) (set types.ChartSet, f types.Cha aProps.setNamespace(ns) } + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.Chart{}.LabelResourceKind(), + filter.Labels, + filter.ChartID..., + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + if set, f, err = store.SearchComposeCharts(svc.ctx, svc.store, filter); err != nil { return err } + if err = label.Load(svc.ctx, svc.store, toLabeledCharts(set)...); err != nil { + return err + } + return nil }() @@ -116,32 +149,40 @@ func (svc chart) Create(new *types.Chart) (*types.Chart, error) { aProps = &chartActionProps{changed: new} ) - err = func() error { + err = store.Tx(svc.ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) { if !handle.IsValid(new.Handle) { return ChartErrInvalidHandle() } - if ns, err = loadNamespace(svc.ctx, svc.store, new.NamespaceID); err != nil { + if ns, err = loadNamespace(ctx, s, new.NamespaceID); err != nil { return err } aProps.setNamespace(ns) - if !svc.ac.CanCreateChart(svc.ctx, ns) { + if !svc.ac.CanCreateChart(ctx, ns) { return ChartErrNotAllowedToCreate() } + if err = svc.uniqueCheck(new); err != nil { + return err + } + new.ID = nextID() new.CreatedAt = *now() new.UpdatedAt = nil new.DeletedAt = nil - if err = svc.uniqueCheck(new); err != nil { + if err = store.CreateComposeChart(ctx, s, new); err != nil { return err } - return store.CreateComposeChart(svc.ctx, svc.store, new) - }() + if err = label.Create(ctx, s, new); err != nil { + return + } + + return nil + }) return new, svc.recordAction(svc.ctx, aProps, ChartActionCreate, err) } @@ -181,6 +222,10 @@ func (svc chart) lookup(namespaceID uint64, lookup func(*chartActionProps) (*typ return ChartErrNotAllowedToRead() } + if err = label.Load(svc.ctx, svc.store, c); err != nil { + return err + } + return nil }() @@ -189,7 +234,7 @@ func (svc chart) lookup(namespaceID uint64, lookup func(*chartActionProps) (*typ func (svc chart) updater(namespaceID, chartID uint64, action func(...*chartActionProps) *chartAction, fn chartUpdateHandler) (*types.Chart, error) { var ( - changed bool + changes chartChanges ns *types.Namespace c *types.Chart aProps = &chartActionProps{chart: &types.Chart{ID: chartID, NamespaceID: namespaceID}} @@ -202,16 +247,30 @@ func (svc chart) updater(namespaceID, chartID uint64, action func(...*chartActio return } + if err = label.Load(svc.ctx, svc.store, c); err != nil { + return err + } + aProps.setNamespace(ns) aProps.setChanged(c) - if changed, err = fn(ctx, ns, c); err != nil { + if changes, err = fn(ctx, ns, c); err != nil { return err - } else if !changed { - return } - return store.UpdateComposeChart(ctx, s, c) + if changes&chartChanged > 0 { + if err = store.UpdateComposeChart(ctx, s, c); err != nil { + return err + } + } + + if changes&chartLabelsChanged > 0 { + if err = label.Update(ctx, s, c); err != nil { + return + } + } + + return nil }) return c, svc.recordAction(svc.ctx, aProps, action, err) @@ -228,57 +287,79 @@ func (svc chart) uniqueCheck(c *types.Chart) (err error) { } func (svc chart) handleUpdate(upd *types.Chart) chartUpdateHandler { - return func(ctx context.Context, ns *types.Namespace, c *types.Chart) (bool, error) { - if isStale(upd.UpdatedAt, c.UpdatedAt, c.CreatedAt) { - return false, ChartErrStaleData() + return func(ctx context.Context, ns *types.Namespace, res *types.Chart) (changes chartChanges, err error) { + if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) { + return chartUnchanged, ChartErrStaleData() } - if upd.Handle != c.Handle && !handle.IsValid(upd.Handle) { - return false, ChartErrInvalidHandle() + if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) { + return chartUnchanged, ChartErrInvalidHandle() } if err := svc.uniqueCheck(upd); err != nil { - return false, err + return chartUnchanged, err } - if !svc.ac.CanUpdateChart(svc.ctx, c) { - return false, ChartErrNotAllowedToUpdate() + if !svc.ac.CanUpdateChart(svc.ctx, res) { + return chartUnchanged, ChartErrNotAllowedToUpdate() } - c.Name = upd.Name - c.Handle = upd.Handle - c.Config = upd.Config - c.UpdatedAt = now() - return true, nil + if res.Name != upd.Name { + changes |= chartChanged + res.Name = upd.Name + } + + if res.Handle != upd.Handle { + changes |= chartChanged + res.Handle = upd.Handle + } + + if !reflect.DeepEqual(upd.Config, res.Config) { + changes |= chartChanged + res.Config = upd.Config + } + + if changes&chartChanged > 0 { + res.UpdatedAt = now() + } + + if upd.Labels != nil { + if label.Changed(res.Labels, upd.Labels) { + changes |= chartLabelsChanged + res.Labels = upd.Labels + } + } + + return } } -func (svc chart) handleDelete(ctx context.Context, ns *types.Namespace, c *types.Chart) (bool, error) { +func (svc chart) handleDelete(ctx context.Context, ns *types.Namespace, c *types.Chart) (chartChanges, error) { if !svc.ac.CanDeleteChart(ctx, c) { - return false, ChartErrNotAllowedToDelete() + return chartUnchanged, ChartErrNotAllowedToDelete() } if c.DeletedAt != nil { // chart already deleted - return false, nil + return chartUnchanged, nil } c.DeletedAt = now() - return true, nil + return chartChanged, nil } -func (svc chart) handleUndelete(ctx context.Context, ns *types.Namespace, c *types.Chart) (bool, error) { +func (svc chart) handleUndelete(ctx context.Context, ns *types.Namespace, c *types.Chart) (chartChanges, error) { if !svc.ac.CanDeleteChart(ctx, c) { - return false, ChartErrNotAllowedToUndelete() + return chartUnchanged, ChartErrNotAllowedToUndelete() } if c.DeletedAt == nil { // chart not deleted - return false, nil + return chartUnchanged, nil } c.DeletedAt = nil - return true, nil + return chartChanged, nil } func loadChart(ctx context.Context, s store.Storer, namespaceID, chartID uint64) (ns *types.Namespace, c *types.Chart, err error) { @@ -303,3 +384,19 @@ func loadChart(ctx context.Context, s store.Storer, namespaceID, chartID uint64) return } + +// toLabeledCharts converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledCharts(set []*types.Chart) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/compose/service/module.go b/compose/service/module.go index 21ad21619..760d24ddb 100644 --- a/compose/service/module.go +++ b/compose/service/module.go @@ -10,7 +10,9 @@ import ( "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/filter" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/store" + "reflect" "sort" "strconv" ) @@ -46,7 +48,16 @@ type ( DeleteByID(namespaceID, moduleID uint64) error } - moduleUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Module) (bool, bool, error) + moduleUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Module) (moduleChanges, error) + + moduleChanges uint8 +) + +const ( + moduleUnchanged moduleChanges = 0 + moduleChanged moduleChanges = 1 + moduleLabelsChanged moduleChanges = 2 + moduleFieldsChanged moduleChanges = 4 ) func Module() ModuleService { @@ -82,16 +93,40 @@ func (svc module) Find(filter types.ModuleFilter) (set types.ModuleSet, f types. } err = func() error { + if ns, err := loadNamespace(svc.ctx, svc.store, filter.NamespaceID); err != nil { return err } else { aProps.setNamespace(ns) } + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.Module{}.LabelResourceKind(), + filter.Labels, + filter.ModuleID..., + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + if set, f, err = store.SearchComposeModules(svc.ctx, svc.store, filter); err != nil { return err } + if err = loadModuleLabels(svc.ctx, svc.store, set...); err != nil { + return err + } + return loadModuleFields(svc.ctx, svc.store, set...) }() @@ -171,12 +206,12 @@ func (svc module) Create(new *types.Module) (*types.Module, error) { return err } + aProps.setNamespace(ns) + if !svc.ac.CanCreateModule(ctx, ns) { return ModuleErrNotAllowedToCreate() } - aProps.setNamespace(ns) - // Calling before-create scripts if err = svc.eventbus.WaitFor(ctx, event.ModuleBeforeCreate(new, nil, ns)); err != nil { return err @@ -212,6 +247,10 @@ func (svc module) Create(new *types.Module) (*types.Module, error) { return err } + if err = label.Create(ctx, s, new); err != nil { + return + } + _ = svc.eventbus.WaitFor(ctx, event.ModuleAfterCreate(new, nil, ns)) return nil }) @@ -233,7 +272,7 @@ func (svc module) UndeleteByID(namespaceID, moduleID uint64) error { func (svc module) updater(namespaceID, moduleID uint64, action func(...*moduleActionProps) *moduleAction, fn moduleUpdateHandler) (*types.Module, error) { var ( - moduleChanged, fieldsChanged bool + changes moduleChanges ns *types.Namespace m, old *types.Module @@ -247,6 +286,10 @@ func (svc module) updater(namespaceID, moduleID uint64, action func(...*moduleAc return } + if err = loadModuleLabels(svc.ctx, svc.store, m); err != nil { + return err + } + old = m.Clone() aProps.setNamespace(ns) @@ -262,17 +305,17 @@ func (svc module) updater(namespaceID, moduleID uint64, action func(...*moduleAc return } - if moduleChanged, fieldsChanged, err = fn(svc.ctx, ns, m); err != nil { + if changes, err = fn(svc.ctx, ns, m); err != nil { return err } - if moduleChanged { + if changes&moduleChanged > 0 { if err = store.UpdateComposeModule(svc.ctx, svc.store, m); err != nil { return err } } - if fieldsChanged { + if changes&moduleFieldsChanged > 0 { var ( hasRecords bool set types.RecordSet @@ -289,6 +332,12 @@ func (svc module) updater(namespaceID, moduleID uint64, action func(...*moduleAc } } + if changes&moduleLabelsChanged > 0 { + if err = label.Update(ctx, s, m); err != nil { + return + } + } + if m.DeletedAt == nil { err = svc.eventbus.WaitFor(svc.ctx, event.ModuleAfterUpdate(m, old, ns)) } else { @@ -324,6 +373,10 @@ func (svc module) lookup(namespaceID uint64, lookup func(*moduleActionProps) (*t return ModuleErrNotAllowedToRead() } + if err = loadModuleLabels(svc.ctx, svc.store, m); err != nil { + return err + } + return loadModuleFields(svc.ctx, svc.store, m) }() @@ -348,79 +401,99 @@ func (svc module) uniqueCheck(m *types.Module) (err error) { } func (svc module) handleUpdate(upd *types.Module) moduleUpdateHandler { - return func(ctx context.Context, ns *types.Namespace, m *types.Module) (mch bool, fch bool, err error) { - if isStale(upd.UpdatedAt, m.UpdatedAt, m.CreatedAt) { - return false, false, ModuleErrStaleData() + return func(ctx context.Context, ns *types.Namespace, res *types.Module) (changes moduleChanges, err error) { + if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) { + return moduleUnchanged, ModuleErrStaleData() } - if upd.Handle != m.Handle && !handle.IsValid(upd.Handle) { - return false, false, ModuleErrInvalidHandle() + if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) { + return moduleUnchanged, ModuleErrInvalidHandle() } if err = svc.uniqueCheck(upd); err != nil { - return false, false, err + return moduleUnchanged, err } - if !svc.ac.CanUpdateModule(svc.ctx, m) { - return false, false, ModuleErrNotAllowedToUpdate() + if !svc.ac.CanUpdateModule(svc.ctx, res) { + return moduleUnchanged, ModuleErrNotAllowedToUpdate() } - if m.Name != upd.Name { - mch = true - m.Name = upd.Name + if res.Name != upd.Name { + changes |= moduleChanged + res.Name = upd.Name } - if m.Handle != upd.Handle { - mch = true - m.Handle = upd.Handle + if res.Handle != upd.Handle { + changes |= moduleChanged + res.Handle = upd.Handle } - if m.Meta.String() != upd.Meta.String() { - mch = true - m.Meta = upd.Meta + { + oldMeta := res.Meta.String() + if oldMeta == "{}" { + oldMeta = "" + } + + newMeta := upd.Meta.String() + if newMeta == "{}" { + newMeta = "" + } + + if oldMeta != newMeta { + changes |= moduleChanged + res.Meta = upd.Meta + } + } // @todo make field-change detection more optimal - if len(upd.Fields) > 0 { - fch = true - m.Fields = upd.Fields + if !reflect.DeepEqual(res.Fields, upd.Fields) { + changes |= moduleFieldsChanged + res.Fields = upd.Fields } - if mch { - m.UpdatedAt = now() + if upd.Labels != nil { + if label.Changed(res.Labels, upd.Labels) { + changes |= moduleLabelsChanged + res.Labels = upd.Labels + } + } + + if changes&moduleChanged > 0 { + res.UpdatedAt = now() } // for now, we assume that - return mch, fch, nil + return } } -func (svc module) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Module) (bool, bool, error) { +func (svc module) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Module) (moduleChanges, error) { if !svc.ac.CanDeleteModule(ctx, m) { - return false, false, ModuleErrNotAllowedToDelete() + return moduleUnchanged, ModuleErrNotAllowedToDelete() } if m.DeletedAt != nil { // module already deleted - return false, false, nil + return moduleUnchanged, nil } m.DeletedAt = now() - return true, false, nil + return moduleChanged, nil } -func (svc module) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Module) (bool, bool, error) { +func (svc module) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Module) (moduleChanges, error) { if !svc.ac.CanDeleteModule(ctx, m) { - return false, false, ModuleErrNotAllowedToUndelete() + return moduleUnchanged, ModuleErrNotAllowedToUndelete() } if m.DeletedAt == nil { // module not deleted - return false, false, nil + return moduleUnchanged, nil } m.DeletedAt = nil - return true, false, nil + return moduleChanged, nil } // updates module fields @@ -445,7 +518,7 @@ func updateModuleFields(ctx context.Context, s store.Storer, m *types.Module, ne for _, ef := range m.Fields { f := newFields.FindByID(ef.ID) - if f != nil || f.DeletedAt == nil { + if f == nil || f.DeletedAt == nil { continue } @@ -474,17 +547,39 @@ func updateModuleFields(ctx context.Context, s store.Storer, m *types.Module, ne f.UpdatedAt = now() err = store.UpdateComposeModuleField(ctx, s, f) + + if err != nil { + return err + } + + if label.Changed(f.Labels, e.Labels) { + if err = label.Update(ctx, s, f); err != nil { + return + } + } + + // override existing with new field + *e = *f + } else { f.ID = nextID() f.CreatedAt = *now() err = store.CreateComposeModuleField(ctx, s, f) - } - if err != nil { - return err + if err != nil { + return err + } + + if err = label.Update(ctx, s, f); err != nil { + return + } + + m.Fields = append(m.Fields, f) } } + sort.Sort(m.Fields) + return nil } @@ -551,3 +646,31 @@ func loadModule(ctx context.Context, s store.Storer, moduleID uint64) (m *types. return } + +// loadLabeledModules loads labels on one or more modules and their fields +// +func loadModuleLabels(ctx context.Context, s store.Labels, set ...*types.Module) error { + if len(set) == 0 { + return nil + } + + mll := make([]label.LabeledResource, 0, len(set)) + fll := make([]label.LabeledResource, 0, len(set)*10) + for i := range set { + mll = append(mll, set[i]) + + for j := range set[i].Fields { + fll = append(fll, set[i].Fields[j]) + } + } + + if err := label.Load(ctx, s, mll...); err != nil { + return err + } + + if err := label.Load(ctx, s, fll...); err != nil { + return err + } + + return nil +} diff --git a/compose/service/module_test.go b/compose/service/module_test.go index 878d631ec..bc93a18ba 100644 --- a/compose/service/module_test.go +++ b/compose/service/module_test.go @@ -3,7 +3,7 @@ package service import ( "context" "github.com/cortezaproject/corteza-server/compose/types" - "github.com/cortezaproject/corteza-server/pkg/errors" + "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/store/sqlite3" @@ -17,6 +17,9 @@ func TestModules(t *testing.T) { ctx = context.Background() s, err = sqlite3.ConnectInMemory(ctx) + // ctx = logger.ContextWithValue(context.Background(), logger.MakeDebugLogger()) + // s, err = sqlite3.ConnectInMemoryWithDebug(ctx) + namespaceID = nextID() ns *types.Namespace ) @@ -33,8 +36,8 @@ func TestModules(t *testing.T) { t.Fatalf("failed to truncate compose namespaces: %v", err) } - if err = s.TruncateComposeCharts(ctx); err != nil { - t.Fatalf("failed to truncate compose charts: %v", err) + if err = s.TruncateComposeModules(ctx); err != nil { + t.Fatalf("failed to truncate compose modules: %v", err) } ns = &types.Namespace{Name: "testing", ID: namespaceID, CreatedAt: *now()} @@ -44,56 +47,167 @@ func TestModules(t *testing.T) { t.Run("crud", func(t *testing.T) { req := require.New(t) - svc := chart{ - store: s, - ctx: context.Background(), - ac: AccessControl(&rbac.ServiceAllowAll{}), + svc := module{ + store: s, + ctx: context.Background(), + ac: AccessControl(&rbac.ServiceAllowAll{}), + eventbus: eventbus.New(), } - res, err := svc.Create(&types.Chart{Name: "My first chart", NamespaceID: namespaceID}) - req.NoError(unwrapModuleInternal(err)) + res, err := svc.Create(&types.Module{Name: "My first module", NamespaceID: namespaceID}) + req.NoError(err) req.NotNil(res) res, err = svc.FindByID(namespaceID, res.ID) - req.NoError(unwrapModuleInternal(err)) + req.NoError(err) req.NotNil(res) res, err = svc.FindByHandle(namespaceID, res.Handle) - req.NoError(unwrapModuleInternal(err)) + req.NoError(err) req.NotNil(res) res.Name = "Changed" res, err = svc.Update(res) - req.NoError(unwrapModuleInternal(err)) + req.NoError(err) req.NotNil(res) req.NotNil(res.UpdatedAt) req.Equal(res.Name, "Changed") res, err = svc.FindByID(namespaceID, res.ID) - req.NoError(unwrapModuleInternal(err)) + req.NoError(err) req.NotNil(res) req.Equal(res.Name, "Changed") err = svc.DeleteByID(namespaceID, res.ID) - req.NoError(unwrapModuleInternal(err)) + req.NoError(err) req.NotNil(res) // this works because we're allowed to do everything res, err = svc.FindByID(namespaceID, res.ID) - req.NoError(unwrapModuleInternal(err)) + req.NoError(err) req.NotNil(res) req.NotNil(res.DeletedAt) + }) + t.Run("labels", func(t *testing.T) { + t.Run("search", func(t *testing.T) { + req := require.New(t) + svc := module{ + store: s, + ctx: ctx, + ac: AccessControl(&rbac.ServiceAllowAll{}), + eventbus: eventbus.New(), + } + + makeModule := func(n ...string) *types.Module { + mod := &types.Module{ + NamespaceID: namespaceID, + Name: n[0], + Labels: map[string]string{}, + } + + for i := 1; i < len(n); i += 2 { + mod.Labels[n[i]] = n[i+1] + } + + out, err := svc.Create(mod) + + req.NoError(err) + return out + } + + findModules := func(labels map[string]string, IDs []uint64) types.ModuleSet { + f := types.ModuleFilter{NamespaceID: namespaceID, Labels: labels, ModuleID: IDs} + set, _, err := svc.Find(f) + req.NoError(err) + + return set + } + + makeModule("labeled module 1", "label1", "value1", "label2", "value2") + m2 := makeModule("labeled module 2", "label1", "value1") + m3 := makeModule("labeled module 3") + + // return all -- no label/ID filter, return all + req.Len(findModules(nil, nil), 3) + + // return 2 - both that have label1=valu1 + req.Len(findModules(map[string]string{"label1": "value1"}, nil), 2) + + // return 0 - none have foo=foo + req.Len(findModules(map[string]string{"missing": "missing"}, nil), 0) + + // one has label2=value2 + req.Len(findModules(map[string]string{"label2": "value2"}, nil), 1) + + // explicit by ID and label + req.Len(findModules(map[string]string{"label1": "value1"}, []uint64{m2.ID}), 1) + + // none with this combo + req.Len(findModules(map[string]string{"foo": "foo"}, []uint64{m3.ID}), 0) + + // one with explicit ID (regression) and nil for label filter + req.Len(findModules(nil, []uint64{m3.ID}), 1) + + // one with explicit ID (regression) and empty map for label filter + req.Len(findModules(map[string]string{}, []uint64{m3.ID}), 1) + }) + + t.Run("CRUD", func(t *testing.T) { + req := require.New(t) + svc := module{ + store: s, + ctx: ctx, + ac: AccessControl(&rbac.ServiceAllowAll{}), + eventbus: eventbus.New(), + } + + findAndReturnLabel := func(id uint64) map[string]string { + res, err := svc.FindByID(namespaceID, id) + req.NoError(err) + req.NotNil(res) + return res.Labels + } + + // create unlabeled module + res, err := svc.Create(&types.Module{Name: "unLabeledIDs", NamespaceID: namespaceID}) + req.NoError(err) + req.NotNil(res) + req.Nil(res.Labels) + + // no labels should be present + req.Nil(findAndReturnLabel(res.ID)) + + // update the module with labels + res.Labels = map[string]string{"label1": "1st"} + res, err = svc.Update(res) + req.NoError(err) + req.NotNil(res) + req.Contains(res.Labels, "label1") + + // must contain the added label + req.Contains(findAndReturnLabel(res.ID), "label1") + + res, err = svc.Create(&types.Module{Name: "LabeledIDs", NamespaceID: namespaceID, Labels: map[string]string{"label2": "2nd"}}) + req.NoError(err) + req.NotNil(res) + req.Contains(res.Labels, "label2") + + // must contain the added label + req.Contains(findAndReturnLabel(res.ID), "label2") + + // update with Labels:nil (should keep labels intact) + res.Labels = nil + res, err = svc.Update(res) + req.NoError(err) + + req.Contains(findAndReturnLabel(res.ID), "label2") + + // update with Labels:empty-map (should remove all labels) + res.Labels = map[string]string{} + res, err = svc.Update(res) + req.NoError(err) + + req.Empty(findAndReturnLabel(res.ID)) + }) }) } - -func unwrapModuleInternal(err error) error { - g := ModuleErrGeneric() - for { - if errors.Is(err, g) { - err = errors.Unwrap(err) - continue - } - - return err - } -} diff --git a/compose/service/namespace.go b/compose/service/namespace.go index 2bfb09c3b..d3b14c8f5 100644 --- a/compose/service/namespace.go +++ b/compose/service/namespace.go @@ -8,8 +8,10 @@ import ( "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/pkg/rbac" "github.com/cortezaproject/corteza-server/store" + "reflect" "strconv" ) @@ -44,7 +46,14 @@ type ( DeleteByID(namespaceID uint64) error } - namespaceUpdateHandler func(ctx context.Context, ns *types.Namespace) (bool, error) + namespaceUpdateHandler func(ctx context.Context, ns *types.Namespace) (namespaceChanges, error) + namespaceChanges uint8 +) + +const ( + namespaceUnchanged namespaceChanges = 0 + namespaceChanged namespaceChanges = 1 + namespaceLabelsChanged namespaceChanges = 2 ) func Namespace() NamespaceService { @@ -80,10 +89,32 @@ func (svc namespace) Find(filter types.NamespaceFilter) (set types.NamespaceSet, } err = func() error { + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.Namespace{}.LabelResourceKind(), + filter.Labels, + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + if set, f, err = store.SearchComposeNamespaces(svc.ctx, svc.store, filter); err != nil { return err } + if err = label.Load(svc.ctx, svc.store, toLabeledNamespaces(set)...); err != nil { + return err + } + return nil }() @@ -173,6 +204,10 @@ func (svc namespace) Create(new *types.Namespace) (*types.Namespace, error) { return err } + if err = label.Create(ctx, s, new); err != nil { + return + } + _ = svc.eventbus.WaitFor(svc.ctx, event.NamespaceAfterCreate(new, nil)) return nil }) @@ -194,7 +229,7 @@ func (svc namespace) UndeleteByID(namespaceID uint64) error { func (svc namespace) updater(namespaceID uint64, action func(...*namespaceActionProps) *namespaceAction, fn namespaceUpdateHandler) (*types.Namespace, error) { var ( - changed bool + changes namespaceChanges ns, old *types.Namespace aProps = &namespaceActionProps{namespace: &types.Namespace{ID: namespaceID}} err error @@ -206,6 +241,10 @@ func (svc namespace) updater(namespaceID uint64, action func(...*namespaceAction return } + if err = label.Load(svc.ctx, svc.store, ns); err != nil { + return err + } + old = ns.Clone() aProps.setNamespace(ns) @@ -221,16 +260,22 @@ func (svc namespace) updater(namespaceID uint64, action func(...*namespaceAction return } - if changed, err = fn(svc.ctx, ns); err != nil { + if changes, err = fn(svc.ctx, ns); err != nil { return err } - if changed { + if changes&namespaceChanged > 0 { if err = store.UpdateComposeNamespace(svc.ctx, svc.store, ns); err != nil { return err } } + if changes&namespaceLabelsChanged > 0 { + if err = label.Update(ctx, s, ns); err != nil { + return + } + } + if ns.DeletedAt == nil { err = svc.eventbus.WaitFor(svc.ctx, event.NamespaceAfterUpdate(ns, old)) } else { @@ -260,6 +305,10 @@ func (svc namespace) lookup(lookup func(*namespaceActionProps) (*types.Namespace return NamespaceErrNotAllowedToRead() } + if err = label.Load(svc.ctx, svc.store, ns); err != nil { + return err + } + return nil }() @@ -277,59 +326,84 @@ func (svc namespace) uniqueCheck(ns *types.Namespace) (err error) { } func (svc namespace) handleUpdate(upd *types.Namespace) namespaceUpdateHandler { - return func(ctx context.Context, ns *types.Namespace) (bool, error) { - if isStale(upd.UpdatedAt, ns.UpdatedAt, ns.CreatedAt) { - return false, NamespaceErrStaleData() + return func(ctx context.Context, res *types.Namespace) (changes namespaceChanges, err error) { + if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) { + return namespaceUnchanged, NamespaceErrStaleData() } - if upd.Slug != ns.Slug && !handle.IsValid(upd.Slug) { - return false, NamespaceErrInvalidHandle() + if upd.Slug != res.Slug && !handle.IsValid(upd.Slug) { + return namespaceUnchanged, NamespaceErrInvalidHandle() } if err := svc.uniqueCheck(upd); err != nil { - return false, err + return namespaceUnchanged, err } - if !svc.ac.CanUpdateNamespace(svc.ctx, ns) { - return false, NamespaceErrNotAllowedToUpdate() + if !svc.ac.CanUpdateNamespace(svc.ctx, res) { + return namespaceUnchanged, NamespaceErrNotAllowedToUpdate() } - ns.Name = upd.Name - ns.Slug = upd.Slug - ns.Meta = upd.Meta - ns.Enabled = upd.Enabled - ns.UpdatedAt = now() + if res.Name != upd.Name { + changes |= namespaceChanged + res.Name = upd.Name + } - return true, nil + if res.Slug != upd.Slug { + changes |= namespaceChanged + res.Slug = upd.Slug + } + + if res.Enabled != upd.Enabled { + changes |= namespaceChanged + res.Enabled = upd.Enabled + } + + if !reflect.DeepEqual(upd.Meta, res.Meta) { + changes |= namespaceChanged + res.Meta = upd.Meta + } + + if upd.Labels != nil { + if label.Changed(res.Labels, upd.Labels) { + changes |= namespaceLabelsChanged + res.Labels = upd.Labels + } + } + + if changes&namespaceChanged > 0 { + res.UpdatedAt = now() + } + + return } } -func (svc namespace) handleDelete(ctx context.Context, ns *types.Namespace) (bool, error) { +func (svc namespace) handleDelete(ctx context.Context, ns *types.Namespace) (namespaceChanges, error) { if !svc.ac.CanDeleteNamespace(ctx, ns) { - return false, NamespaceErrNotAllowedToDelete() + return namespaceUnchanged, NamespaceErrNotAllowedToDelete() } if ns.DeletedAt != nil { // namespace already deleted - return false, nil + return namespaceUnchanged, nil } ns.DeletedAt = now() - return true, nil + return namespaceChanged, nil } -func (svc namespace) handleUndelete(ctx context.Context, ns *types.Namespace) (bool, error) { +func (svc namespace) handleUndelete(ctx context.Context, ns *types.Namespace) (namespaceChanges, error) { if !svc.ac.CanDeleteNamespace(ctx, ns) { - return false, NamespaceErrNotAllowedToUndelete() + return namespaceUnchanged, NamespaceErrNotAllowedToUndelete() } if ns.DeletedAt == nil { // namespace not deleted - return false, nil + return namespaceUnchanged, nil } ns.DeletedAt = nil - return true, nil + return namespaceChanged, nil } func loadNamespace(ctx context.Context, s store.Storer, namespaceID uint64) (ns *types.Namespace, err error) { @@ -343,3 +417,19 @@ func loadNamespace(ctx context.Context, s store.Storer, namespaceID uint64) (ns return } + +// toLabeledNamespaces converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledNamespaces(set []*types.Namespace) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/compose/service/page.go b/compose/service/page.go index a288f9666..d533ef8f1 100644 --- a/compose/service/page.go +++ b/compose/service/page.go @@ -8,7 +8,9 @@ import ( "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/eventbus" "github.com/cortezaproject/corteza-server/pkg/handle" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/store" + "reflect" ) type ( @@ -45,7 +47,14 @@ type ( Reorder(namespaceID, selfID uint64, pageIDs []uint64) error } - pageUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Page) (bool, error) + pageUpdateHandler func(ctx context.Context, ns *types.Namespace, c *types.Page) (pageChanges, error) + pageChanges uint8 +) + +const ( + pageUnchanged pageChanges = 0 + pageChanged pageChanges = 1 + pageLabelsChanged pageChanges = 2 ) func Page() PageService { @@ -125,10 +134,32 @@ func (svc page) search(filter types.PageFilter) (set types.PageSet, f types.Page aProps.setNamespace(ns) } + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.Page{}.LabelResourceKind(), + filter.Labels, + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + if set, f, err = store.SearchComposePages(svc.ctx, svc.store, filter); err != nil { return err } + if err = label.Load(svc.ctx, svc.store, toLabeledPages(set)...); err != nil { + return err + } + return nil }() @@ -271,6 +302,10 @@ func (svc page) Create(new *types.Page) (*types.Page, error) { return err } + if err = label.Create(ctx, s, new); err != nil { + return + } + _ = svc.eventbus.WaitFor(svc.ctx, event.PageAfterCreate(new, nil, ns)) return err }) @@ -292,7 +327,7 @@ func (svc page) UndeleteByID(namespaceID, pageID uint64) error { func (svc page) updater(namespaceID, pageID uint64, action func(...*pageActionProps) *pageAction, fn pageUpdateHandler) (*types.Page, error) { var ( - changed bool + changes pageChanges ns *types.Namespace p, old *types.Page @@ -306,6 +341,10 @@ func (svc page) updater(namespaceID, pageID uint64, action func(...*pageActionPr return } + if err = label.Load(svc.ctx, svc.store, p); err != nil { + return err + } + old = p.Clone() aProps.setNamespace(ns) @@ -321,16 +360,22 @@ func (svc page) updater(namespaceID, pageID uint64, action func(...*pageActionPr return } - if changed, err = fn(svc.ctx, ns, p); err != nil { + if changes, err = fn(svc.ctx, ns, p); err != nil { return err } - if changed { + if changes&pageChanged > 0 { if err = store.UpdateComposePage(svc.ctx, svc.store, p); err != nil { return err } } + if changes&pageLabelsChanged > 0 { + if err = label.Update(ctx, s, p); err != nil { + return + } + } + if p.DeletedAt == nil { err = svc.eventbus.WaitFor(svc.ctx, event.PageAfterUpdate(p, old, ns)) } else { @@ -366,6 +411,10 @@ func (svc page) lookup(namespaceID uint64, lookup func(*pageActionProps) (*types return PageErrNotAllowedToRead() } + if err = label.Load(svc.ctx, svc.store, p); err != nil { + return err + } + return nil }() @@ -389,63 +438,104 @@ func (svc page) uniqueCheck(p *types.Page) (err error) { } func (svc page) handleUpdate(upd *types.Page) pageUpdateHandler { - return func(ctx context.Context, ns *types.Namespace, p *types.Page) (bool, error) { - if isStale(upd.UpdatedAt, p.UpdatedAt, p.CreatedAt) { - return false, PageErrStaleData() + return func(ctx context.Context, ns *types.Namespace, res *types.Page) (changes pageChanges, err error) { + if isStale(upd.UpdatedAt, res.UpdatedAt, res.CreatedAt) { + return pageUnchanged, PageErrStaleData() } - if upd.Handle != p.Handle && !handle.IsValid(upd.Handle) { - return false, PageErrInvalidHandle() + if upd.Handle != res.Handle && !handle.IsValid(upd.Handle) { + return pageUnchanged, PageErrInvalidHandle() } if err := svc.uniqueCheck(upd); err != nil { - return false, err + return pageUnchanged, err } - if !svc.ac.CanUpdatePage(svc.ctx, p) { - return false, PageErrNotAllowedToUpdate() + if !svc.ac.CanUpdatePage(svc.ctx, res) { + return pageUnchanged, PageErrNotAllowedToUpdate() } - p.ID = upd.ID - p.SelfID = upd.SelfID - p.Blocks = upd.Blocks - p.Title = upd.Title - p.Handle = upd.Handle - p.Description = upd.Description - p.Visible = upd.Visible - p.Weight = upd.Weight - p.UpdatedAt = now() + if res.ID != upd.ID { + res.ID = upd.ID + changes |= pageChanged + } - return true, nil + if res.SelfID != upd.SelfID { + res.SelfID = upd.SelfID + changes |= pageChanged + } + + if !reflect.DeepEqual(res.Blocks, upd.Blocks) { + res.Blocks = upd.Blocks + changes |= pageChanged + } + + if res.Title != upd.Title { + res.Title = upd.Title + changes |= pageChanged + } + + if res.Handle != upd.Handle { + res.Handle = upd.Handle + changes |= pageChanged + } + + if res.Description != upd.Description { + res.Description = upd.Description + changes |= pageChanged + } + + if res.Visible != upd.Visible { + res.Visible = upd.Visible + changes |= pageChanged + } + + if res.Weight != upd.Weight { + res.Weight = upd.Weight + changes |= pageChanged + } + + if upd.Labels != nil { + if label.Changed(res.Labels, upd.Labels) { + changes |= pageLabelsChanged + res.Labels = upd.Labels + } + } + + if changes&pageChanged > 0 { + res.UpdatedAt = now() + } + + return } } -func (svc page) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Page) (bool, error) { +func (svc page) handleDelete(ctx context.Context, ns *types.Namespace, m *types.Page) (pageChanges, error) { if !svc.ac.CanDeletePage(ctx, m) { - return false, PageErrNotAllowedToDelete() + return pageUnchanged, PageErrNotAllowedToDelete() } if m.DeletedAt != nil { // page already deleted - return false, nil + return pageUnchanged, nil } m.DeletedAt = now() - return true, nil + return pageChanged, nil } -func (svc page) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Page) (bool, error) { +func (svc page) handleUndelete(ctx context.Context, ns *types.Namespace, m *types.Page) (pageChanges, error) { if !svc.ac.CanDeletePage(ctx, m) { - return false, PageErrNotAllowedToUndelete() + return pageUnchanged, PageErrNotAllowedToUndelete() } if m.DeletedAt == nil { // page not deleted - return false, nil + return pageUnchanged, nil } m.DeletedAt = nil - return true, nil + return pageChanged, nil } func loadPage(ctx context.Context, s store.Storer, namespaceID, pageID uint64) (ns *types.Namespace, m *types.Page, err error) { @@ -470,3 +560,19 @@ func loadPage(ctx context.Context, s store.Storer, namespaceID, pageID uint64) ( return } + +// toLabeledPages converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledPages(set []*types.Page) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/compose/service/record.go b/compose/service/record.go index 23ac059cb..e4dc3fcb4 100644 --- a/compose/service/record.go +++ b/compose/service/record.go @@ -12,6 +12,7 @@ import ( "github.com/cortezaproject/corteza-server/pkg/corredor" "github.com/cortezaproject/corteza-server/pkg/errors" "github.com/cortezaproject/corteza-server/pkg/eventbus" + "github.com/cortezaproject/corteza-server/pkg/label" "github.com/cortezaproject/corteza-server/store" "regexp" "strconv" @@ -226,6 +227,10 @@ func (svc record) lookup(namespaceID, moduleID uint64, lookup func(*types.Module trimUnreadableRecordFields(svc.ctx, svc.ac, m, r) + if err = label.Load(svc.ctx, svc.store, r); err != nil { + return err + } + return nil }() @@ -273,11 +278,33 @@ func (svc record) Find(filter types.RecordFilter) (set types.RecordSet, f types. return err } + if len(filter.Labels) > 0 { + filter.LabeledIDs, err = label.Search( + svc.ctx, + svc.store, + types.Record{}.LabelResourceKind(), + filter.Labels, + ) + + if err != nil { + return err + } + + // labels specified but no labeled resources found + if len(filter.LabeledIDs) == 0 { + return nil + } + } + set, f, err = store.SearchComposeRecords(svc.ctx, svc.store, m, filter) if err != nil { return err } + if err = label.Load(svc.ctx, svc.store, toLabeledRecords(set)...); err != nil { + return err + } + trimUnreadableRecordFields(svc.ctx, svc.ac, m, set...) return nil @@ -540,6 +567,10 @@ func (svc record) create(new *types.Record) (rec *types.Record, err error) { return nil, err } + if err = label.Create(svc.ctx, svc.store, new); err != nil { + return + } + // At this point we can return the value rec = new @@ -628,8 +659,13 @@ func (svc record) update(upd *types.Record) (rec *types.Record, err error) { } err = store.Tx(svc.ctx, svc.store, func(ctx context.Context, s store.Storer) error { - return store.UpdateComposeRecord(ctx, s, m, upd) + if label.Changed(old.Labels, upd.Labels) { + if err = label.Update(ctx, s, upd); err != nil { + return err + } + } + return store.UpdateComposeRecord(ctx, s, m, upd) }) if err != nil { @@ -1362,3 +1398,19 @@ func loadRecordCombo(ctx context.Context, s store.Storer, namespaceID, moduleID, return } + +// toLabeledRecords converts to []label.LabeledResource +// +// This function is auto-generated. +func toLabeledRecords(set []*types.Record) []label.LabeledResource { + if len(set) == 0 { + return nil + } + + ll := make([]label.LabeledResource, len(set)) + for i := range set { + ll[i] = set[i] + } + + return ll +} diff --git a/compose/types/chart.go b/compose/types/chart.go index 2cc7e2abb..a482a22db 100644 --- a/compose/types/chart.go +++ b/compose/types/chart.go @@ -16,6 +16,8 @@ type ( Name string `json:"name"` Config ChartConfig `json:"config"` + Labels map[string]string `json:"labels,omitempty"` + NamespaceID uint64 `json:"namespaceID,string"` CreatedAt time.Time `json:"createdAt,omitempty"` @@ -40,9 +42,13 @@ type ( } ChartFilter struct { - NamespaceID uint64 `json:"namespaceID,string"` - Handle string `json:"handle"` - Query string `json:"query"` + NamespaceID uint64 `json:"namespaceID,string"` + ChartID []uint64 `json:"chartID"` + Handle string `json:"handle"` + Query string `json:"query"` + + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` Deleted filter.State `json:"deleted"` diff --git a/compose/types/module.go b/compose/types/module.go index 37f3b7699..5fbce15ec 100644 --- a/compose/types/module.go +++ b/compose/types/module.go @@ -2,11 +2,9 @@ package types import ( "github.com/cortezaproject/corteza-server/pkg/filter" - "time" - - "github.com/jmoiron/sqlx/types" - "github.com/cortezaproject/corteza-server/pkg/rbac" + "github.com/jmoiron/sqlx/types" + "time" ) type ( @@ -17,6 +15,8 @@ type ( Meta types.JSONText `json:"meta"` Fields ModuleFieldSet `json:"fields"` + Labels map[string]string `json:"labels,omitempty"` + NamespaceID uint64 `json:"namespaceID,string"` CreatedAt time.Time `json:"createdAt,omitempty"` @@ -25,10 +25,14 @@ type ( } ModuleFilter struct { - NamespaceID uint64 `json:"namespaceID,string"` - Query string `json:"query"` - Handle string `json:"handle"` - Name string `json:"name"` + ModuleID []uint64 `json:"moduleID"` + NamespaceID uint64 `json:"namespaceID,string"` + Query string `json:"query"` + Handle string `json:"handle"` + Name string `json:"name"` + + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` Deleted filter.State `json:"deleted"` diff --git a/compose/types/module_field.go b/compose/types/module_field.go index 449daaa4c..f3752db7d 100644 --- a/compose/types/module_field.go +++ b/compose/types/module_field.go @@ -28,6 +28,8 @@ type ( Multi bool `json:"isMulti"` DefaultValue RecordValueSet `json:"defaultValue"` + Labels map[string]string `json:"labels,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` DeletedAt *time.Time `json:"deletedAt,omitempty"` diff --git a/compose/types/namespace.go b/compose/types/namespace.go index beb77c679..d7572b760 100644 --- a/compose/types/namespace.go +++ b/compose/types/namespace.go @@ -19,6 +19,8 @@ type ( Enabled bool `json:"enabled"` Meta NamespaceMeta `json:"meta"` + Labels map[string]string `json:"labels,omitempty"` + CreatedAt time.Time `json:"createdAt,omitempty"` UpdatedAt *time.Time `json:"updatedAt,omitempty"` DeletedAt *time.Time `json:"deletedAt,omitempty"` @@ -29,6 +31,9 @@ type ( Slug string `json:"slug"` Name string `json:"name"` + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` + Deleted filter.State `json:"deleted"` // Check fn is called by store backend for each resource found function can diff --git a/compose/types/page.go b/compose/types/page.go index 2d34f04b3..50d9ec90b 100644 --- a/compose/types/page.go +++ b/compose/types/page.go @@ -29,6 +29,8 @@ type ( Children PageSet `json:"children,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Visible bool `json:"visible"` Weight int `json:"weight"` @@ -59,6 +61,9 @@ type ( Handle string `json:"handle"` Query string `json:"query"` + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` + Deleted filter.State `json:"deleted"` // Check fn is called by store backend for each resource found function can diff --git a/compose/types/record.go b/compose/types/record.go index edc5cb7e4..a4a28b376 100644 --- a/compose/types/record.go +++ b/compose/types/record.go @@ -37,6 +37,8 @@ type ( Values RecordValueSet `json:"values,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + NamespaceID uint64 `json:"namespaceID,string"` OwnedBy uint64 `json:"ownedBy,string"` @@ -53,8 +55,8 @@ type ( NamespaceID uint64 `json:"namespaceID,string"` Query string `json:"query"` - // Preloaded set of additional modules that are used for record filtering - // Modules ModuleSet + LabeledIDs []uint64 `json:"-"` + Labels map[string]string `json:"labels,omitempty"` Deleted filter.State `json:"deleted"` diff --git a/compose/types/type_labels.gen.go b/compose/types/type_labels.gen.go new file mode 100644 index 000000000..22e9ab5c7 --- /dev/null +++ b/compose/types/type_labels.gen.go @@ -0,0 +1,153 @@ +package types + +// This file is auto-generated. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +// Definitions file that controls how this file is generated: +// compose/types/types.yaml + +// SetLabel adds new label to label map +func (m *Chart) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Chart) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Chart) LabelResourceKind() string { + return "compose:chart" +} + +// GetLabels adds new label to label map +func (m Chart) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *Module) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Module) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Module) LabelResourceKind() string { + return "compose:module" +} + +// GetLabels adds new label to label map +func (m Module) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *ModuleField) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m ModuleField) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (ModuleField) LabelResourceKind() string { + return "compose:module:field" +} + +// GetLabels adds new label to label map +func (m ModuleField) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *Namespace) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Namespace) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Namespace) LabelResourceKind() string { + return "compose:namespace" +} + +// GetLabels adds new label to label map +func (m Namespace) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *Page) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Page) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Page) LabelResourceKind() string { + return "compose:page" +} + +// GetLabels adds new label to label map +func (m Page) LabelResourceID() uint64 { + return m.ID +} + +// SetLabel adds new label to label map +func (m *Record) SetLabel(key string, value string) { + if m.Labels == nil { + m.Labels = make(map[string]string) + } + + m.Labels[key] = value +} + +// GetLabels adds new label to label map +func (m Record) GetLabels() map[string]string { + return m.Labels +} + +// GetLabels adds new label to label map +func (Record) LabelResourceKind() string { + return "compose:record" +} + +// GetLabels adds new label to label map +func (m Record) LabelResourceID() uint64 { + return m.ID +} diff --git a/compose/types/types.yaml b/compose/types/types.yaml index e9c5affcb..a7648750e 100644 --- a/compose/types/types.yaml +++ b/compose/types/types.yaml @@ -1,10 +1,17 @@ types: Namespace: + labelResourceType: compose:namespace Attachment: Module: - Page: - Chart: - Record: + labelResourceType: compose:module ModuleField: + labelResourceType: compose:module:field + Page: + labelResourceType: compose:page + Chart: + labelResourceType: compose:chart + Record: + labelResourceType: compose:record RecordValue: noIdField: true + diff --git a/store/rdbms/compose_charts.go b/store/rdbms/compose_charts.go index 3e3d3d1d4..81be9a7fb 100644 --- a/store/rdbms/compose_charts.go +++ b/store/rdbms/compose_charts.go @@ -12,6 +12,14 @@ func (s Store) convertComposeChartFilter(f types.ChartFilter) (query squirrel.Se query = filter.StateCondition(query, "cch.deleted_at", f.Deleted) + if len(f.ChartID) > 0 { + query = query.Where(squirrel.Eq{"cch.id": f.ChartID}) + } + + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"cch.id": f.LabeledIDs}) + } + if f.NamespaceID > 0 { query = query.Where("cch.rel_namespace = ?", f.NamespaceID) } diff --git a/store/rdbms/compose_modules.go b/store/rdbms/compose_modules.go index 42e66f888..20e6e9eaa 100644 --- a/store/rdbms/compose_modules.go +++ b/store/rdbms/compose_modules.go @@ -12,6 +12,14 @@ func (s Store) convertComposeModuleFilter(f types.ModuleFilter) (query squirrel. query = filter.StateCondition(query, "cmd.deleted_at", f.Deleted) + if len(f.ModuleID) > 0 { + query = query.Where(squirrel.Eq{"cmd.id": f.ModuleID}) + } + + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"cmd.id": f.LabeledIDs}) + } + if f.NamespaceID > 0 { query = query.Where("cmd.rel_namespace = ?", f.NamespaceID) } diff --git a/store/rdbms/compose_namespaces.go b/store/rdbms/compose_namespaces.go index 2a8226c44..1df3f00bc 100644 --- a/store/rdbms/compose_namespaces.go +++ b/store/rdbms/compose_namespaces.go @@ -12,6 +12,10 @@ func (s Store) convertComposeNamespaceFilter(f types.NamespaceFilter) (query squ query = filter.StateCondition(query, "cns.deleted_at", f.Deleted) + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"cns.id": f.LabeledIDs}) + } + if f.Query != "" { q := "%" + strings.ToLower(f.Query) + "%" query = query.Where(squirrel.Or{ diff --git a/store/rdbms/compose_pages.go b/store/rdbms/compose_pages.go index f793b815b..9110d0801 100644 --- a/store/rdbms/compose_pages.go +++ b/store/rdbms/compose_pages.go @@ -18,6 +18,10 @@ func (s Store) convertComposePageFilter(f types.PageFilter) (query squirrel.Sele query = query.Where("cpg.rel_namespace = ?", f.NamespaceID) } + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"cpg.id": f.LabeledIDs}) + } + if f.ParentID > 0 { query = query.Where("self_id = ?", f.ParentID) } else if f.Root { diff --git a/store/rdbms/compose_records.go b/store/rdbms/compose_records.go index 6805ba4b2..a6f2b4bc7 100644 --- a/store/rdbms/compose_records.go +++ b/store/rdbms/compose_records.go @@ -247,6 +247,10 @@ func (s Store) convertComposeRecordFilter(m *types.Module, f types.RecordFilter) // Inc/exclude deleted records according to filter settings query = filter.StateCondition(query, "crd.deleted_at", f.Deleted) + if len(f.LabeledIDs) > 0 { + query = query.Where(squirrel.Eq{"crd.id": f.LabeledIDs}) + } + // Parse filters. if f.Query != "" { var ( diff --git a/tests/compose/chart_test.go b/tests/compose/chart_test.go index 8e865af51..26fb15ede 100644 --- a/tests/compose/chart_test.go +++ b/tests/compose/chart_test.go @@ -9,7 +9,9 @@ import ( "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/tests/helpers" jsonpath "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -226,3 +228,89 @@ func TestChartDelete(t *testing.T) { res = h.lookupChartByID(res.ID) h.a.NotNil(res.DeletedAt) } + +func TestChartLabels(t *testing.T) { + h := newHelper(t) + h.clearCharts() + + h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + h.allow(types.NamespaceRBACResource.AppendWildcard(), "chart.create") + h.allow(types.ChartRBACResource.AppendWildcard(), "read") + h.allow(types.ChartRBACResource.AppendWildcard(), "update") + h.allow(types.ChartRBACResource.AppendWildcard(), "delete") + + var ( + ns = h.makeNamespace("some-namespace") + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.Chart{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/chart/", ns.ID), + types.Chart{Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.Chart{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/chart/%d", ns.ID, ID), + types.Chart{Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.ChartSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/chart/", ns.ID), + &set, url.Values{"labels": []string{"baz=123"}}, + ) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) +} diff --git a/tests/compose/main_test.go b/tests/compose/main_test.go index 5ef86df18..e48796f9b 100644 --- a/tests/compose/main_test.go +++ b/tests/compose/main_test.go @@ -50,7 +50,7 @@ func init() { func InitTestApp() { if testApp == nil { - ctx := cli.Context() + ctx := logger.ContextWithValue(cli.Context(), logger.MakeDebugLogger()) testApp = helpers.NewIntegrationTestApp(ctx, func(app *app.CortezaApp) (err error) { service.DefaultStore = app.Store diff --git a/tests/compose/module_test.go b/tests/compose/module_test.go index 84a2c2f4a..b41cb30a0 100644 --- a/tests/compose/module_test.go +++ b/tests/compose/module_test.go @@ -9,7 +9,9 @@ import ( "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -133,7 +135,7 @@ func TestModuleListQuery(t *testing.T) { End() } -func TestModuleList_filterForbiden(t *testing.T) { +func TestModuleList_filterForbidden(t *testing.T) { h := newHelper(t) h.clearModules() @@ -334,3 +336,156 @@ func TestModuleDelete(t *testing.T) { res = h.lookupModuleByID(res.ID) h.a.NotNil(res.DeletedAt) } + +func TestModuleLabels(t *testing.T) { + h := newHelper(t) + h.clearModules() + + h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + h.allow(types.NamespaceRBACResource.AppendWildcard(), "module.create") + h.allow(types.ModuleRBACResource.AppendWildcard(), "read") + h.allow(types.ModuleRBACResource.AppendWildcard(), "update") + h.allow(types.ModuleRBACResource.AppendWildcard(), "delete") + + var ( + ns = h.makeNamespace("some-namespace") + fieldID, ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.Module{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/module/", ns.ID), + types.Module{Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.Module{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/module/%d", ns.ID, ID), + types.Module{Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.ModuleSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/module/", ns.ID), + &set, + url.Values{"labels": []string{"baz=123"}}, + ) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID).Labels) + }) + + t.Run("field create", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + ctx = context.Background() + req = require.New(t) + mod, err = store.LookupComposeModuleByID(ctx, service.DefaultStore, ID) + + payload = struct{ Response *types.Module }{} + ) + + req.NoError(err) + req.NotNil(mod) + mod.Fields = append(mod.Fields, &types.ModuleField{ + Kind: "String", + Name: "labeled", + Label: "", + Labels: map[string]string{"fldfoo": "fldbar"}, + }) + + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, ID)). + JSON(helpers.JSON(mod)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Equal(`$.response.fields[0].labels.fldfoo`, "fldbar")). + End(). + JSON(&payload) + + fieldID = payload.Response.Fields[0].ID + + }) + + t.Run("field update", func(t *testing.T) { + if fieldID == 0 { + t.Skip("label/field create test not ran") + } + + var ( + ctx = context.Background() + req = require.New(t) + mod, err = store.LookupComposeModuleByID(ctx, service.DefaultStore, ID) + ) + + req.NoError(err) + req.NotNil(mod) + mod.Fields = append(mod.Fields, &types.ModuleField{ + ID: fieldID, + Kind: "String", + Name: "labeled", + Label: "", + Labels: map[string]string{"fldfoo": "fldbaz"}, + }) + + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d", ns.ID, ID)). + JSON(helpers.JSON(mod)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + Assert(jsonpath.Equal(`$.response.fields[0].labels.fldfoo`, "fldbaz")). + End() + }) +} diff --git a/tests/compose/namespace_test.go b/tests/compose/namespace_test.go index 89830dcac..d3400bd1d 100644 --- a/tests/compose/namespace_test.go +++ b/tests/compose/namespace_test.go @@ -10,7 +10,9 @@ import ( "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/tests/helpers" jsonpath "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -197,3 +199,84 @@ func TestNamespaceDelete(t *testing.T) { ns = h.lookupNamespaceByID(ns.ID) h.a.NotNil(ns.DeletedAt) } + +func TestNamespaceLabels(t *testing.T) { + h := newHelper(t) + h.clearNamespaces() + + h.allow(types.ComposeRBACResource, "namespace.create") + h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + h.allow(types.NamespaceRBACResource.AppendWildcard(), "update") + h.allow(types.NamespaceRBACResource.AppendWildcard(), "delete") + + var ( + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.Namespace{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + "/namespace/", + types.Namespace{Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.Namespace{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d", ID), + &types.Namespace{Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.NamespaceSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, "/namespace/", &set, url.Values{"labels": []string{"baz=123"}}) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) +} diff --git a/tests/compose/page_test.go b/tests/compose/page_test.go index 37c1796e2..dbe68d0fe 100644 --- a/tests/compose/page_test.go +++ b/tests/compose/page_test.go @@ -9,7 +9,9 @@ import ( "github.com/cortezaproject/corteza-server/store" "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "net/http" + "net/url" "testing" "time" ) @@ -262,3 +264,91 @@ func TestPageTreeRead(t *testing.T) { Assert(jsonpath.Equal(`$.response[3].title`, "p4")). End() } + +func TestPageLabels(t *testing.T) { + h := newHelper(t) + h.clearPages() + + h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + h.allow(types.NamespaceRBACResource.AppendWildcard(), "page.create") + h.allow(types.PageRBACResource.AppendWildcard(), "read") + h.allow(types.PageRBACResource.AppendWildcard(), "update") + h.allow(types.PageRBACResource.AppendWildcard(), "delete") + + var ( + ns = h.makeNamespace("some-namespace") + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + payload = &types.Page{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/page/", ns.ID), + types.Page{Labels: map[string]string{"foo": "bar", "bar": "42"}}, + payload, + ) + req.NotZero(payload.ID) + + h.a.Equal(payload.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + + ID = payload.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + payload = &types.Page{} + ) + + helpers.SetLabelsViaAPI(h.apiInit(), + t, + fmt.Sprintf("/namespace/%d/page/%d", ns.ID, ID), + types.Page{Labels: map[string]string{"foo": "baz", "baz": "123"}}, + payload, + ) + req.NotZero(payload.ID) + req.Nil(payload.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.LabelResourceKind(), payload.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.PageSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/page/", ns.ID), + &set, + url.Values{"labels": []string{"baz=123"}}, + ) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) +} diff --git a/tests/compose/record_test.go b/tests/compose/record_test.go index 32532f8a9..59fc51352 100644 --- a/tests/compose/record_test.go +++ b/tests/compose/record_test.go @@ -11,9 +11,11 @@ import ( "github.com/cortezaproject/corteza-server/tests/helpers" "github.com/steinfletcher/apitest" "github.com/steinfletcher/apitest-jsonpath" + "github.com/stretchr/testify/require" "io/ioutil" "mime/multipart" "net/http" + "net/url" "testing" "time" ) @@ -558,5 +560,124 @@ func TestRecordFieldModulePermissionCheck(t *testing.T) { }) }) } - +} + +func TestRecordLabels(t *testing.T) { + h := newHelper(t) + h.clearRecords() + + h.allow(types.NamespaceRBACResource.AppendWildcard(), "read") + h.allow(types.ModuleRBACResource.AppendWildcard(), "read") + h.allow(types.ModuleRBACResource.AppendWildcard(), "record.create") + h.allow(types.ModuleRBACResource.AppendWildcard(), "record.update") + h.allow(types.ModuleRBACResource.AppendWildcard(), "record.read") + + var ( + ns = h.makeNamespace("some-namespace") + mod = h.makeModule(ns, "some-module", &types.ModuleField{Kind: "String", Name: "dummy"}) + ID uint64 + ) + + t.Run("create", func(t *testing.T) { + var ( + req = require.New(t) + + payload = struct { + Response *types.Record + }{} + + rec = &types.Record{ + Values: types.RecordValueSet{&types.RecordValue{Name: "dummy", Value: "dummy"}}, + Labels: map[string]string{ + "foo": "bar", + "bar": "42", + }, + } + ) + + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d/record/", ns.ID, mod.ID)). + JSON(helpers.JSON(rec)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End(). + JSON(&payload) + + req.NotNil(payload.Response) + req.NotZero(payload.Response.ID) + + h.a.Equal(payload.Response.Labels["foo"], "bar", + "labels must contain foo with value bar") + h.a.Equal(payload.Response.Labels["bar"], "42", + "labels must contain bar with value 42") + req.Equal(payload.Response.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.Response.LabelResourceKind(), payload.Response.ID), + "response must match stored labels") + + ID = payload.Response.ID + }) + + t.Run("update", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + + payload = struct { + Response *types.Record + }{} + + rec = &types.Record{ + ID: ID, + Values: types.RecordValueSet{&types.RecordValue{Name: "dummy", Value: "dummy"}}, + Labels: map[string]string{ + "foo": "baz", + "baz": "123", + }, + } + ) + + h.apiInit(). + Post(fmt.Sprintf("/namespace/%d/module/%d/record/%d", ns.ID, mod.ID, ID)). + JSON(helpers.JSON(rec)). + Expect(t). + Status(http.StatusOK). + Assert(helpers.AssertNoErrors). + End(). + JSON(&payload) + req.NotZero(payload.Response.ID) + + // disabled for now + //req.Nil(payload.Response.UpdatedAt, "updatedAt must not change after changing labels") + + req.Equal(payload.Response.Labels["foo"], "baz", + "labels must contain foo with value baz") + req.NotContains(payload.Response.Labels, "bar", + "labels must not contain bar") + req.Equal(payload.Response.Labels["baz"], "123", + "labels must contain baz with value 123") + req.Equal(payload.Response.Labels, helpers.LoadLabelsFromStore(t, service.DefaultStore, payload.Response.LabelResourceKind(), payload.Response.ID), + "response must match stored labels") + }) + + t.Run("search", func(t *testing.T) { + if ID == 0 { + t.Skip("label/create test not ran") + } + + var ( + req = require.New(t) + set = types.RecordSet{} + ) + + helpers.SearchWithLabelsViaAPI(h.apiInit(), t, + fmt.Sprintf("/namespace/%d/module/%d/record/", ns.ID, mod.ID), + &set, url.Values{"labels": []string{"baz=123"}}, + ) + req.NotEmpty(set) + req.NotNil(set.FindByID(ID)) + req.NotNil(set.FindByID(ID).Labels) + }) } diff --git a/tests/helpers/app.go b/tests/helpers/app.go index c5cccf217..e09a48f51 100644 --- a/tests/helpers/app.go +++ b/tests/helpers/app.go @@ -13,6 +13,8 @@ import ( ) func NewIntegrationTestApp(ctx context.Context, initTestServices func(*app.CortezaApp) error) *app.CortezaApp { + logger.Init() + var ( a = app.New() ) @@ -24,7 +26,7 @@ func NewIntegrationTestApp(ctx context.Context, initTestServices func(*app.Corte a.Opt.Auth.Secret = string(rand.Bytes(32)) a.Opt.Auth.Expiry = time.Minute - a.Log = logger.MakeDebugLogger() + a.Log = logger.Default() cli.HandleError(a.InitStore(ctx)) cli.HandleError(initTestServices(a)) diff --git a/tests/helpers/json.go b/tests/helpers/json.go new file mode 100644 index 000000000..6868dc3d6 --- /dev/null +++ b/tests/helpers/json.go @@ -0,0 +1,11 @@ +package helpers + +import "encoding/json" + +func JSON(i interface{}) string { + if b, err := json.Marshal(i); err != nil { + panic(err) + } else { + return string(b) + } +} diff --git a/tests/helpers/labels.go b/tests/helpers/labels.go new file mode 100644 index 000000000..26baa647c --- /dev/null +++ b/tests/helpers/labels.go @@ -0,0 +1,63 @@ +package helpers + +import ( + "context" + "github.com/cortezaproject/corteza-server/pkg/label/types" + "github.com/cortezaproject/corteza-server/store" + "github.com/steinfletcher/apitest" + "github.com/stretchr/testify/require" + "net/http" + "net/url" + "strings" + "testing" +) + +func SetLabelsViaAPI(api *apitest.APITest, t *testing.T, endpoint string, in, out interface{}) { + var ( + payload = struct { + Response interface{} + }{} + req *apitest.Request + ) + + payload.Response = out + + if strings.HasPrefix(endpoint, "PUT ") { + // a little workaround for our inconsistencies... + req = api.Put(endpoint[4:]) + } else { + req = api.Post(endpoint) + } + + req.JSON(JSON(in)). + Expect(t). + Status(http.StatusOK). + Assert(AssertNoErrors). + End(). + JSON(&payload) +} + +func SearchWithLabelsViaAPI(api *apitest.APITest, t *testing.T, endpoint string, res interface{}, labels url.Values) { + var payload = struct{ Response struct{ Set interface{} } }{} + + payload.Response.Set = res + + api.Get(endpoint). + QueryCollection(labels). + Expect(t). + Status(http.StatusOK). + Assert(AssertNoErrors). + End(). + JSON(&payload) +} + +func LoadLabelsFromStore(t *testing.T, s store.Storer, resKind string, id uint64) (sl map[string]string) { + ll, _, err := s.SearchLabels(context.Background(), types.LabelFilter{Kind: resKind}) + require.NoError(t, err) + if len(ll) == 0 { + // small adjustment + return nil + } + + return ll.FilterByResource(resKind, id) +}