3
0

Define base templating system

This commit is contained in:
Tomaž Jerman
2021-02-04 17:29:04 +01:00
parent deb68938fa
commit 127b43a69e
39 changed files with 5122 additions and 16 deletions

View File

@@ -218,6 +218,7 @@ func (app *CortezaApp) InitServices(ctx context.Context) (err error) {
err = sysService.Initialize(ctx, app.Log, app.Store, sysService.Config{
ActionLog: app.Opt.ActionLog,
Storage: app.Opt.ObjStore,
Template: app.Opt.Template,
})
if err != nil {

View File

@@ -12,6 +12,7 @@ type (
Auth options.AuthOpt
HTTPClient options.HTTPClientOpt
DB options.DBOpt
Template options.TemplateOpt
Upgrade options.UpgradeOpt
Provision options.ProvisionOpt
Sentry options.SentryOpt
@@ -35,6 +36,7 @@ func NewOptions() *Options {
SMTP: *options.SMTP(),
HTTPClient: *options.HTTPClient(),
DB: *options.DB(),
Template: *options.Template(),
Upgrade: *options.Upgrade(),
Provision: *options.Provision(),
Sentry: *options.Sentry(),

View File

@@ -0,0 +1,37 @@
package options
// 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:
// pkg/options/template.yaml
type (
TemplateOpt struct {
RendererGotenbergAddress string `env:"TEMPLATE_RENDERER_GOTENBERG_ADDRESS"`
RendererGotenbergEnabled bool `env:"TEMPLATE_RENDERER_GOTENBERG_ENABLED"`
}
)
// Template initializes and returns a TemplateOpt with default values
func Template() (o *TemplateOpt) {
o = &TemplateOpt{
RendererGotenbergAddress: "",
RendererGotenbergEnabled: false,
}
fill(o)
// Function that allows access to custom logic inside the parent function.
// The custom logic in the other file should be like:
// func (o *Template) Defaults() {...}
func(o interface{}) {
if def, ok := o.(interface{ Defaults() }); ok {
def.Defaults()
}
}(o)
return
}

11
pkg/options/template.yaml Normal file
View File

@@ -0,0 +1,11 @@
docs:
title: Rendering engine
props:
- name: rendererGotenbergAddress
default: ""
description: Gotenberg rendering container address.
- name: rendererGotenbergEnabled
type: bool
default: false
description: Is Gotenberg rendering container enabled.

View File

@@ -35,6 +35,7 @@ package store
// - store/role_members.yaml
// - store/roles.yaml
// - store/settings.yaml
// - store/templates.yaml
// - store/users.yaml
//
// Changes to this file may cause incorrect behavior and will be lost if
@@ -75,6 +76,7 @@ type (
RoleMembers
Roles
Settings
Templates
Users
}
)

View File

@@ -58,6 +58,7 @@ func (s Schema) Tables() []*Table {
s.RbacRules(),
s.Settings(),
s.Labels(),
s.Templates(),
s.ComposeAttachment(),
s.ComposeChart(),
s.ComposeModule(),
@@ -238,6 +239,23 @@ func (Schema) Labels() *Table {
)
}
func (Schema) Templates() *Table {
return TableDef("templates",
ID,
ColumnDef("rel_owner", ColumnTypeIdentifier),
ColumnDef("handle", ColumnTypeVarchar, ColumnTypeLength(handleLength)),
ColumnDef("language", ColumnTypeText),
ColumnDef("type", ColumnTypeText),
ColumnDef("partial", ColumnTypeBoolean),
ColumnDef("meta", ColumnTypeJson),
ColumnDef("template", ColumnTypeText),
CUDTimestamps,
ColumnDef("last_used_at", ColumnTypeTimestamp, Null),
AddIndex("unique_language_handle", IColumn("language"), IExpr("LOWER(handle)"), IWhere("LENGTH(handle) > 0 AND deleted_at IS NULL")),
)
}
func (Schema) ComposeAttachment() *Table {
// @todo merge with general attachment table

View File

@@ -0,0 +1,634 @@
package rdbms
// This file is an auto-generated file
//
// Template: pkg/codegen/assets/store_rdbms.gen.go.tpl
// Definitions: store/templates.yaml
//
// Changes to this file may cause incorrect behavior
// and will be lost if the code is regenerated.
import (
"context"
"database/sql"
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/store/rdbms/builders"
"github.com/cortezaproject/corteza-server/system/types"
)
var _ = errors.Is
// SearchTemplates returns all matching rows
//
// This function calls convertTemplateFilter with the given
// types.TemplateFilter and expects to receive a working squirrel.SelectBuilder
func (s Store) SearchTemplates(ctx context.Context, f types.TemplateFilter) (types.TemplateSet, types.TemplateFilter, error) {
var (
err error
set []*types.Template
q squirrel.SelectBuilder
)
return set, f, func() error {
q, err = s.convertTemplateFilter(f)
if err != nil {
return err
}
// Paging enabled
// {search: {enablePaging:true}}
// Cleanup unwanted cursor values (only relevant is f.PageCursor, next&prev are reset and returned)
f.PrevPage, f.NextPage = nil, nil
if f.PageCursor != nil {
// Page cursor exists so we need to validate it against used sort
// To cover the case when paging cursor is set but sorting is empty, we collect the sorting instructions
// from the cursor.
// This (extracted sorting info) is then returned as part of response
if f.Sort, err = f.PageCursor.Sort(f.Sort); err != nil {
return err
}
}
// Make sure results are always sorted at least by primary keys
if f.Sort.Get("id") == nil {
f.Sort = append(f.Sort, &filter.SortExpr{
Column: "id",
Descending: f.Sort.LastDescending(),
})
}
// Cloned sorting instructions for the actual sorting
// Original are passed to the fetchFullPageOfUsers fn used for cursor creation so it MUST keep the initial
// direction information
sort := f.Sort.Clone()
// When cursor for a previous page is used it's marked as reversed
// This tells us to flip the descending flag on all used sort keys
if f.PageCursor != nil && f.PageCursor.ROrder {
sort.Reverse()
}
// Apply sorting expr from filter to query
if q, err = setOrderBy(q, sort, s.sortableTemplateColumns()); err != nil {
return err
}
set, f.PrevPage, f.NextPage, err = s.fetchFullPageOfTemplates(
ctx,
q, f.Sort, f.PageCursor,
f.Limit,
f.Check,
func(cur *filter.PagingCursor) squirrel.Sqlizer {
return builders.CursorCondition(cur, nil)
},
)
if err != nil {
return err
}
f.PageCursor = nil
return nil
}()
}
// fetchFullPageOfTemplates collects all requested results.
//
// Function applies:
// - cursor conditions (where ...)
// - limit
//
// Main responsibility of this function is to perform additional sequential queries in case when not enough results
// are collected due to failed check on a specific row (by check fn).
//
// Function then moves cursor to the last item fetched
func (s Store) fetchFullPageOfTemplates(
ctx context.Context,
q squirrel.SelectBuilder,
sort filter.SortExprSet,
cursor *filter.PagingCursor,
reqItems uint,
check func(*types.Template) (bool, error),
cursorCond func(*filter.PagingCursor) squirrel.Sqlizer,
) (set []*types.Template, prev, next *filter.PagingCursor, err error) {
var (
aux []*types.Template
// When cursor for a previous page is used it's marked as reversed
// This tells us to flip the descending flag on all used sort keys
reversedOrder = cursor != nil && cursor.ROrder
// copy of the select builder
tryQuery squirrel.SelectBuilder
// Copy no. of required items to limit
// Limit will change when doing subsequent queries to fill
// the set with all required items
limit = reqItems
// cursor to prev. page is only calculated when cursor is used
hasPrev = cursor != nil
// next cursor is calculated when there are more pages to come
hasNext bool
)
set = make([]*types.Template, 0, DefaultSliceCapacity)
for try := 0; try < MaxRefetches; try++ {
if cursor != nil {
tryQuery = q.Where(cursorCond(cursor))
} else {
tryQuery = q
}
if limit > 0 {
// fetching + 1 so we know if there are more items
// we can fetch (next-page cursor)
tryQuery = tryQuery.Limit(uint64(limit + 1))
}
if aux, err = s.QueryTemplates(ctx, tryQuery, check); err != nil {
return nil, nil, nil, err
}
if len(aux) == 0 {
// nothing fetched
break
}
// append fetched items
set = append(set, aux...)
if reqItems == 0 {
// no max requested items specified, break out
break
}
collected := uint(len(set))
if reqItems > collected {
// not enough items fetched, try again with adjusted limit
limit = reqItems - collected
if limit < MinEnsureFetchLimit {
// In case limit is set very low and we've missed records in the first fetch,
// make sure next fetch limit is a bit higher
limit = MinEnsureFetchLimit
}
// Update cursor so that it points to the last item fetched
cursor = s.collectTemplateCursorValues(set[collected-1], sort...)
// Copy reverse flag from sorting
cursor.LThen = sort.Reversed()
continue
}
if reqItems < collected {
set = set[:reqItems]
hasNext = true
}
break
}
collected := len(set)
if collected == 0 {
return nil, nil, nil, nil
}
if reversedOrder {
// Fetched set needs to be reversed because we've forced a descending order to get the previous page
for i, j := 0, collected-1; i < j; i, j = i+1, j-1 {
set[i], set[j] = set[j], set[i]
}
// when in reverse-order rules on what cursor to return change
hasPrev, hasNext = hasNext, hasPrev
}
if hasPrev {
prev = s.collectTemplateCursorValues(set[0], sort...)
prev.ROrder = true
prev.LThen = !sort.Reversed()
}
if hasNext {
next = s.collectTemplateCursorValues(set[collected-1], sort...)
next.LThen = sort.Reversed()
}
return set, prev, next, nil
}
// QueryTemplates queries the database, converts and checks each row and
// returns collected set
//
// Fn also returns total number of fetched items and last fetched item so that the caller can construct cursor
// for next page of results
func (s Store) QueryTemplates(
ctx context.Context,
q squirrel.Sqlizer,
check func(*types.Template) (bool, error),
) ([]*types.Template, error) {
var (
set = make([]*types.Template, 0, DefaultSliceCapacity)
res *types.Template
// Query rows with
rows, err = s.Query(ctx, q)
)
if err != nil {
return nil, err
}
defer rows.Close()
for rows.Next() {
if err = rows.Err(); err == nil {
res, err = s.internalTemplateRowScanner(rows)
}
if err != nil {
return nil, err
}
// check fn set, call it and see if it passed the test
// if not, skip the item
if check != nil {
if chk, err := check(res); err != nil {
return nil, err
} else if !chk {
continue
}
}
set = append(set, res)
}
return set, rows.Err()
}
// LookupTemplateByID searches for template by ID
//
// It also returns deleted templates.
func (s Store) LookupTemplateByID(ctx context.Context, id uint64) (*types.Template, error) {
return s.execLookupTemplate(ctx, squirrel.Eq{
s.preprocessColumn("tpl.id", ""): store.PreprocessValue(id, ""),
})
}
// LookupTemplateByHandle searches for template by the handle
//
// It returns only valid templates (not deleted)
func (s Store) LookupTemplateByHandle(ctx context.Context, handle string) (*types.Template, error) {
return s.execLookupTemplate(ctx, squirrel.Eq{
s.preprocessColumn("tpl.handle", "lower"): store.PreprocessValue(handle, "lower"),
"tpl.deleted_at": nil,
})
}
// CreateTemplate creates one or more rows in templates table
func (s Store) CreateTemplate(ctx context.Context, rr ...*types.Template) (err error) {
for _, res := range rr {
err = s.checkTemplateConstraints(ctx, res)
if err != nil {
return err
}
err = s.execCreateTemplates(ctx, s.internalTemplateEncoder(res))
if err != nil {
return err
}
}
return
}
// UpdateTemplate updates one or more existing rows in templates
func (s Store) UpdateTemplate(ctx context.Context, rr ...*types.Template) error {
return s.partialTemplateUpdate(ctx, nil, rr...)
}
// partialTemplateUpdate updates one or more existing rows in templates
func (s Store) partialTemplateUpdate(ctx context.Context, onlyColumns []string, rr ...*types.Template) (err error) {
for _, res := range rr {
err = s.checkTemplateConstraints(ctx, res)
if err != nil {
return err
}
err = s.execUpdateTemplates(
ctx,
squirrel.Eq{
s.preprocessColumn("tpl.id", ""): store.PreprocessValue(res.ID, ""),
},
s.internalTemplateEncoder(res).Skip("id").Only(onlyColumns...))
if err != nil {
return err
}
}
return
}
// UpsertTemplate updates one or more existing rows in templates
func (s Store) UpsertTemplate(ctx context.Context, rr ...*types.Template) (err error) {
for _, res := range rr {
err = s.checkTemplateConstraints(ctx, res)
if err != nil {
return err
}
err = s.execUpsertTemplates(ctx, s.internalTemplateEncoder(res))
if err != nil {
return err
}
}
return nil
}
// DeleteTemplate Deletes one or more rows from templates table
func (s Store) DeleteTemplate(ctx context.Context, rr ...*types.Template) (err error) {
for _, res := range rr {
err = s.execDeleteTemplates(ctx, squirrel.Eq{
s.preprocessColumn("tpl.id", ""): store.PreprocessValue(res.ID, ""),
})
if err != nil {
return err
}
}
return nil
}
// DeleteTemplateByID Deletes row from the templates table
func (s Store) DeleteTemplateByID(ctx context.Context, ID uint64) error {
return s.execDeleteTemplates(ctx, squirrel.Eq{
s.preprocessColumn("tpl.id", ""): store.PreprocessValue(ID, ""),
})
}
// TruncateTemplates Deletes all rows from the templates table
func (s Store) TruncateTemplates(ctx context.Context) error {
return s.Truncate(ctx, s.templateTable())
}
// execLookupTemplate prepares Template query and executes it,
// returning types.Template (or error)
func (s Store) execLookupTemplate(ctx context.Context, cnd squirrel.Sqlizer) (res *types.Template, err error) {
var (
row rowScanner
)
row, err = s.QueryRow(ctx, s.templatesSelectBuilder().Where(cnd))
if err != nil {
return
}
res, err = s.internalTemplateRowScanner(row)
if err != nil {
return
}
return res, nil
}
// execCreateTemplates updates all matched (by cnd) rows in templates with given data
func (s Store) execCreateTemplates(ctx context.Context, payload store.Payload) error {
return s.Exec(ctx, s.InsertBuilder(s.templateTable()).SetMap(payload))
}
// execUpdateTemplates updates all matched (by cnd) rows in templates with given data
func (s Store) execUpdateTemplates(ctx context.Context, cnd squirrel.Sqlizer, set store.Payload) error {
return s.Exec(ctx, s.UpdateBuilder(s.templateTable("tpl")).Where(cnd).SetMap(set))
}
// execUpsertTemplates inserts new or updates matching (by-primary-key) rows in templates with given data
func (s Store) execUpsertTemplates(ctx context.Context, set store.Payload) error {
upsert, err := s.config.UpsertBuilder(
s.config,
s.templateTable(),
set,
s.preprocessColumn("id", ""),
)
if err != nil {
return err
}
return s.Exec(ctx, upsert)
}
// execDeleteTemplates Deletes all matched (by cnd) rows in templates with given data
func (s Store) execDeleteTemplates(ctx context.Context, cnd squirrel.Sqlizer) error {
return s.Exec(ctx, s.DeleteBuilder(s.templateTable("tpl")).Where(cnd))
}
func (s Store) internalTemplateRowScanner(row rowScanner) (res *types.Template, err error) {
res = &types.Template{}
if _, has := s.config.RowScanners["template"]; has {
scanner := s.config.RowScanners["template"].(func(_ rowScanner, _ *types.Template) error)
err = scanner(row, res)
} else {
err = row.Scan(
&res.ID,
&res.Handle,
&res.Language,
&res.Type,
&res.Partial,
&res.Meta,
&res.Template,
&res.OwnerID,
&res.CreatedAt,
&res.UpdatedAt,
&res.DeletedAt,
&res.LastUsedAt,
)
}
if err == sql.ErrNoRows {
return nil, store.ErrNotFound.Stack(1)
}
if err != nil {
return nil, errors.Store("could not scan template db row").Wrap(err)
} else {
return res, nil
}
}
// QueryTemplates returns squirrel.SelectBuilder with set table and all columns
func (s Store) templatesSelectBuilder() squirrel.SelectBuilder {
return s.SelectBuilder(s.templateTable("tpl"), s.templateColumns("tpl")...)
}
// templateTable name of the db table
func (Store) templateTable(aa ...string) string {
var alias string
if len(aa) > 0 {
alias = " AS " + aa[0]
}
return "templates" + alias
}
// TemplateColumns returns all defined table columns
//
// With optional string arg, all columns are returned aliased
func (Store) templateColumns(aa ...string) []string {
var alias string
if len(aa) > 0 {
alias = aa[0] + "."
}
return []string{
alias + "id",
alias + "handle",
alias + "language",
alias + "type",
alias + "partial",
alias + "meta",
alias + "template",
alias + "rel_owner",
alias + "created_at",
alias + "updated_at",
alias + "deleted_at",
alias + "last_used_at",
}
}
// {true true false true true true}
// sortableTemplateColumns returns all Template columns flagged as sortable
//
// With optional string arg, all columns are returned aliased
func (Store) sortableTemplateColumns() map[string]string {
return map[string]string{
"id": "id", "handle": "handle", "created_at": "created_at",
"createdat": "created_at",
"updated_at": "updated_at",
"updatedat": "updated_at",
"deleted_at": "deleted_at",
"deletedat": "deleted_at",
"last_used_at": "last_used_at",
"lastusedat": "last_used_at",
}
}
// internalTemplateEncoder encodes fields from types.Template to store.Payload (map)
//
// Encoding is done by using generic approach or by calling encodeTemplate
// func when rdbms.customEncoder=true
func (s Store) internalTemplateEncoder(res *types.Template) store.Payload {
return store.Payload{
"id": res.ID,
"handle": res.Handle,
"language": res.Language,
"type": res.Type,
"partial": res.Partial,
"meta": res.Meta,
"template": res.Template,
"rel_owner": res.OwnerID,
"created_at": res.CreatedAt,
"updated_at": res.UpdatedAt,
"deleted_at": res.DeletedAt,
"last_used_at": res.LastUsedAt,
}
}
// collectTemplateCursorValues collects values from the given resource that and sets them to the cursor
// to be used for pagination
//
// Values that are collected must come from sortable, unique or primary columns/fields
// At least one of the collected columns must be flagged as unique, otherwise fn appends primary keys at the end
//
// Known issue:
// when collecting cursor values for query that sorts by unique column with partial index (ie: unique handle on
// undeleted items)
func (s Store) collectTemplateCursorValues(res *types.Template, cc ...*filter.SortExpr) *filter.PagingCursor {
var (
cursor = &filter.PagingCursor{LThen: filter.SortExprSet(cc).Reversed()}
hasUnique bool
// All known primary key columns
pkId bool
collect = func(cc ...*filter.SortExpr) {
for _, c := range cc {
switch c.Column {
case "id":
cursor.Set(c.Column, res.ID, c.Descending)
pkId = true
case "handle":
cursor.Set(c.Column, res.Handle, c.Descending)
hasUnique = true
case "created_at":
cursor.Set(c.Column, res.CreatedAt, c.Descending)
case "updated_at":
cursor.Set(c.Column, res.UpdatedAt, c.Descending)
case "deleted_at":
cursor.Set(c.Column, res.DeletedAt, c.Descending)
case "last_used_at":
cursor.Set(c.Column, res.LastUsedAt, c.Descending)
}
}
}
)
collect(cc...)
if !hasUnique || !(pkId && true) {
collect(&filter.SortExpr{Column: "id", Descending: false})
}
return cursor
}
// checkTemplateConstraints performs lookups (on valid) resource to check if any of the values on unique fields
// already exists in the store
//
// Using built-in constraint checking would be more performant but unfortunately we can not rely
// on the full support (MySQL does not support conditional indexes)
func (s *Store) checkTemplateConstraints(ctx context.Context, res *types.Template) error {
// Consider resource valid when all fields in unique constraint check lookups
// have valid (non-empty) value
//
// Only string and uint64 are supported for now
// feel free to add additional types if needed
var valid = true
valid = valid && len(res.Handle) > 0
if !valid {
return nil
}
{
ex, err := s.LookupTemplateByHandle(ctx, res.Handle)
if err == nil && ex != nil && ex.ID != res.ID {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
}
return nil
}

36
store/rdbms/templates.go Normal file
View File

@@ -0,0 +1,36 @@
package rdbms
import (
"github.com/Masterminds/squirrel"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/system/types"
)
func (s Store) convertTemplateFilter(f types.TemplateFilter) (query squirrel.SelectBuilder, err error) {
query = s.templatesSelectBuilder()
query = filter.StateCondition(query, "tpl.deleted_at", f.Deleted)
if len(f.LabeledIDs) > 0 {
query = query.Where(squirrel.Eq{"tpl.id": f.LabeledIDs})
}
if f.Partial {
query = query.Where(squirrel.Eq{"tpl.partial": true})
}
if len(f.TemplateID) > 0 {
query = query.Where(squirrel.Eq{"tpl.id": f.TemplateID})
}
if f.Handle != "" {
query = query.Where(squirrel.Eq{"tpl.handle": f.Handle})
}
if f.Type != "" {
query = query.Where(squirrel.Eq{"tpl.type": f.Type})
}
if f.OwnerID > 0 {
query = query.Where(squirrel.Eq{"tpl.rel_owner": f.OwnerID})
}
return
}

85
store/templates.gen.go Normal file
View File

@@ -0,0 +1,85 @@
package store
// This file is auto-generated.
//
// Template: pkg/codegen/assets/store_base.gen.go.tpl
// Definitions: store/templates.yaml
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
import (
"context"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
Templates interface {
SearchTemplates(ctx context.Context, f types.TemplateFilter) (types.TemplateSet, types.TemplateFilter, error)
LookupTemplateByID(ctx context.Context, id uint64) (*types.Template, error)
LookupTemplateByHandle(ctx context.Context, handle string) (*types.Template, error)
CreateTemplate(ctx context.Context, rr ...*types.Template) error
UpdateTemplate(ctx context.Context, rr ...*types.Template) error
UpsertTemplate(ctx context.Context, rr ...*types.Template) error
DeleteTemplate(ctx context.Context, rr ...*types.Template) error
DeleteTemplateByID(ctx context.Context, ID uint64) error
TruncateTemplates(ctx context.Context) error
}
)
var _ *types.Template
var _ context.Context
// SearchTemplates returns all matching Templates from store
func SearchTemplates(ctx context.Context, s Templates, f types.TemplateFilter) (types.TemplateSet, types.TemplateFilter, error) {
return s.SearchTemplates(ctx, f)
}
// LookupTemplateByID searches for template by ID
//
// It also returns deleted templates.
func LookupTemplateByID(ctx context.Context, s Templates, id uint64) (*types.Template, error) {
return s.LookupTemplateByID(ctx, id)
}
// LookupTemplateByHandle searches for template by the handle
//
// It returns only valid templates (not deleted)
func LookupTemplateByHandle(ctx context.Context, s Templates, handle string) (*types.Template, error) {
return s.LookupTemplateByHandle(ctx, handle)
}
// CreateTemplate creates one or more Templates in store
func CreateTemplate(ctx context.Context, s Templates, rr ...*types.Template) error {
return s.CreateTemplate(ctx, rr...)
}
// UpdateTemplate updates one or more (existing) Templates in store
func UpdateTemplate(ctx context.Context, s Templates, rr ...*types.Template) error {
return s.UpdateTemplate(ctx, rr...)
}
// UpsertTemplate creates new or updates existing one or more Templates in store
func UpsertTemplate(ctx context.Context, s Templates, rr ...*types.Template) error {
return s.UpsertTemplate(ctx, rr...)
}
// DeleteTemplate Deletes one or more Templates from store
func DeleteTemplate(ctx context.Context, s Templates, rr ...*types.Template) error {
return s.DeleteTemplate(ctx, rr...)
}
// DeleteTemplateByID Deletes Template from store
func DeleteTemplateByID(ctx context.Context, s Templates, ID uint64) error {
return s.DeleteTemplateByID(ctx, ID)
}
// TruncateTemplates Deletes all Templates from store
func TruncateTemplates(ctx context.Context, s Templates) error {
return s.TruncateTemplates(ctx)
}

34
store/templates.yaml Normal file
View File

@@ -0,0 +1,34 @@
import:
- github.com/cortezaproject/corteza-server/system/types
fields:
- { field: ID }
- { field: Handle, sortable: true, unique: true, lookupFilterPreprocessor: lower }
- { field: Language }
- { field: Type }
- { field: Partial }
- { field: Meta, type: "*types.TemplateMeta" }
- { field: Template }
- { field: OwnerID }
- { field: CreatedAt, sortable: true }
- { field: UpdatedAt, sortable: true }
- { field: DeletedAt, sortable: true }
- { field: LastUsedAt, sortable: true }
lookups:
- fields: [ ID ]
description: |-
searches for template by ID
It also returns deleted templates.
- fields: [ Handle ]
filter: { DeletedAt: nil }
uniqueConstraintCheck: true
description: |-
searches for template by the handle
It returns only valid templates (not deleted)
rdbms:
alias: tpl
table: templates
customFilterConverter: true

View File

@@ -33,6 +33,7 @@ package tests
// - store/role_members.yaml
// - store/roles.yaml
// - store/settings.yaml
// - store/templates.yaml
// - store/users.yaml
//
@@ -201,6 +202,11 @@ func testAllGenerated(t *testing.T, s store.Storer) {
testSettings(t, s)
})
// Run generated tests for Templates
t.Run("Templates", func(t *testing.T) {
testTemplates(t, s)
})
// Run generated tests for Users
t.Run("Users", func(t *testing.T) {
testUsers(t, s)

View File

@@ -0,0 +1,156 @@
package tests
import (
"context"
"testing"
"time"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/pkg/rand"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/types"
_ "github.com/joho/godotenv/autoload"
"github.com/stretchr/testify/require"
)
func testTemplates(t *testing.T, s store.Templates) {
var (
ctx = context.Background()
req = require.New(t)
makeNew = func(handle string) *types.Template {
// minimum data set for new template
return &types.Template{
ID: id.Next(),
CreatedAt: time.Now(),
Handle: handle,
}
}
truncAndCreate = func(t *testing.T) (*require.Assertions, *types.Template) {
req := require.New(t)
req.NoError(s.TruncateTemplates(ctx))
res := makeNew(string(rand.Bytes(10)))
req.NoError(s.CreateTemplate(ctx, res))
return req, res
}
)
t.Run("create", func(t *testing.T) {
template := makeNew("TemplateCRUD")
req.NoError(s.CreateTemplate(ctx, template))
})
t.Run("lookup by ID", func(t *testing.T) {
req, template := truncAndCreate(t)
fetched, err := s.LookupTemplateByID(ctx, template.ID)
req.NoError(err)
req.Equal(template.Handle, fetched.Handle)
req.Equal(template.ID, fetched.ID)
req.NotNil(fetched.CreatedAt)
req.Nil(fetched.UpdatedAt)
req.Nil(fetched.DeletedAt)
})
t.Run("lookup by handle", func(t *testing.T) {
req, template := truncAndCreate(t)
fetched, err := s.LookupTemplateByHandle(ctx, template.Handle)
req.NoError(err)
req.Equal(template.Handle, fetched.Handle)
req.Equal(template.ID, fetched.ID)
req.NotNil(fetched.CreatedAt)
req.Nil(fetched.UpdatedAt)
req.Nil(fetched.DeletedAt)
})
t.Run("update", func(t *testing.T) {
req, template := truncAndCreate(t)
template.Handle = "TemplateCRUD+2"
req.NoError(s.UpdateTemplate(ctx, template))
updated, err := s.LookupTemplateByID(ctx, template.ID)
req.NoError(err)
req.Equal(template.Handle, updated.Handle)
})
t.Run("upsert", func(t *testing.T) {
t.Run("existing", func(t *testing.T) {
req, template := truncAndCreate(t)
template.Handle = "TemplateCRUD+2"
req.NoError(s.UpsertTemplate(ctx, template))
updated, err := s.LookupTemplateByID(ctx, template.ID)
req.NoError(err)
req.Equal(template.Handle, updated.Handle)
})
t.Run("new", func(t *testing.T) {
template := makeNew("upsert me")
template.Handle = "ComposeChartCRUD+2"
req.NoError(s.UpsertTemplate(ctx, template))
upserted, err := s.LookupTemplateByID(ctx, template.ID)
req.NoError(err)
req.Equal(template.Handle, upserted.Handle)
})
})
t.Run("delete", func(t *testing.T) {
t.Run("by Template", func(t *testing.T) {
req, template := truncAndCreate(t)
req.NoError(s.DeleteTemplate(ctx, template))
_, err := s.LookupTemplateByID(ctx, template.ID)
req.EqualError(err, store.ErrNotFound.Error())
})
t.Run("by ID", func(t *testing.T) {
req, template := truncAndCreate(t)
req.NoError(s.DeleteTemplateByID(ctx, template.ID))
_, err := s.LookupTemplateByID(ctx, template.ID)
req.EqualError(err, store.ErrNotFound.Error())
})
})
t.Run("search", func(t *testing.T) {
prefill := []*types.Template{
makeNew("one-one"),
makeNew("one-two"),
makeNew("two-one"),
makeNew("two-two"),
makeNew("two-deleted"),
}
count := len(prefill)
prefill[4].DeletedAt = &prefill[4].CreatedAt
valid := count - 1
req.NoError(s.TruncateTemplates(ctx))
req.NoError(s.CreateTemplate(ctx, prefill...))
// search for all valid
set, f, err := s.SearchTemplates(ctx, types.TemplateFilter{})
req.NoError(err)
req.Len(set, valid) // we've deleted one
// search for ALL
set, f, err = s.SearchTemplates(ctx, types.TemplateFilter{Deleted: filter.StateInclusive})
req.NoError(err)
req.Len(set, count) // we've deleted one
// search for deleted only
set, f, err = s.SearchTemplates(ctx, types.TemplateFilter{Deleted: filter.StateExclusive})
req.NoError(err)
req.Len(set, 1) // we've deleted one
set, f, err = s.SearchTemplates(ctx, types.TemplateFilter{Handle: "two-one"})
req.NoError(err)
req.Len(set, 1)
_ = f // dummy
})
}

View File

@@ -0,0 +1,42 @@
package renderer
import (
"bytes"
"context"
"io"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
genericHTML struct{}
genericHTMLDriver struct{}
)
func newGenericHTML() driverFactory {
return &genericHTML{}
}
func (d *genericHTML) CanRender(t types.DocumentType) bool {
return t == types.DocumentTypeHTML || t == types.DocumentTypePlain
}
func (d *genericHTML) CanProduce(t types.DocumentType) bool {
return t == types.DocumentTypeHTML
}
func (d *genericHTML) Driver() driver {
return &genericHTMLDriver{}
}
func (d *genericHTMLDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
t, err := preprocHTMLTemplate(pl)
if err != nil {
return nil, err
}
dd := &bytes.Buffer{}
err = t.Execute(dd, pl.Variables)
return bytes.NewReader(dd.Bytes()), err
}

View File

@@ -0,0 +1,47 @@
package renderer
import (
"bytes"
"context"
"io"
"regexp"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
genericTXT struct{}
genericTXTDriver struct{}
)
var (
plainTextRegex = regexp.MustCompile("text(/)?.*")
)
func newGenericTXT() driverFactory {
return &genericTXT{}
}
func (d *genericTXT) CanRender(t types.DocumentType) bool {
return t == types.DocumentTypePlain || t == types.DocumentTypeHTML
}
func (d *genericTXT) CanProduce(t types.DocumentType) bool {
return t == types.DocumentTypePlain
}
func (d *genericTXT) Driver() driver {
return &genericTXTDriver{}
}
func (d *genericTXTDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
t, err := preprocPlainTemplate(pl.Template, pl.Partials)
if err != nil {
return nil, err
}
dd := &bytes.Buffer{}
err = t.Execute(dd, pl.Variables)
return bytes.NewReader(dd.Bytes()), err
}

View File

@@ -0,0 +1,291 @@
package renderer
import (
"bytes"
"context"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"strings"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
gotenbergPDF struct {
url string
}
gotenbergPDFDriver struct {
url string
}
)
// @todo healthcheck, different input data formats
func newGotenbergPDF(url string) driverFactory {
return &gotenbergPDF{
url: url,
}
}
func (d *gotenbergPDF) CanRender(t types.DocumentType) bool {
return t == types.DocumentTypeHTML || t == types.DocumentTypePlain
}
func (d *gotenbergPDF) CanProduce(t types.DocumentType) bool {
return t == types.DocumentTypePDF
}
func (d *gotenbergPDF) Driver() driver {
return &gotenbergPDFDriver{
url: d.url,
}
}
func (d *gotenbergPDFDriver) Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error) {
// HTTP request body stuff
body := &bytes.Buffer{}
writer := multipart.NewWriter(body)
// index.html is required by the rendering container
part, err := writer.CreateFormFile("file", "index.html")
if err != nil {
return nil, err
}
err = d.prepareContent(part, pl)
if err != nil {
return nil, err
}
// Document configurations
err = d.applyOptions(writer, pl.Options)
if err != nil {
return nil, err
}
err = writer.Close()
if err != nil {
return nil, err
}
// HTTP request header stuff
// @todo make sure to use the propper endpoint when you add support
// for different inputs.
url := d.url
if n, has := pl.Options["url"]; has {
url = n
}
req, err := http.NewRequest("POST", url+"/convert/html", body)
if err != nil {
return nil, err
}
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return nil, err
}
ss, err := ioutil.ReadAll(resp.Body)
if err != nil {
return nil, err
}
defer resp.Body.Close()
return bytes.NewReader(ss), err
}
func (d *gotenbergPDFDriver) prepareContent(w io.Writer, pl *driverPayload) error {
t, err := preprocHTMLTemplate(pl)
if err != nil {
return err
}
return t.Execute(w, pl.Variables)
}
func (d *gotenbergPDFDriver) applyOptions(mw *multipart.Writer, opts map[string]string) (err error) {
if opts == nil {
return nil
}
for k, v := range opts {
switch k {
case "marginTop",
"marginBottom",
"marginLeft",
"marginRight":
err = d.addFormField(mw, k, v)
case "marginY":
err = d.addFormField(mw, "marginTop", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginBottom", v)
case "marginX":
err = d.addFormField(mw, "marginLeft", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginRight", v)
case "margin":
err = d.addFormField(mw, "marginTop", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginBottom", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginLeft", v)
if err != nil {
return err
}
err = d.addFormField(mw, "marginRight", v)
case "documentSize":
w, h := d.documentDimensions(v)
if w+h != "" {
err = d.addFormField(mw, "paperWidth", w)
if err != nil {
return err
}
err = d.addFormField(mw, "paperHeight", h)
}
case "documentWidth":
err = d.addFormField(mw, "paperWidth", v)
case "documentHeight":
err = d.addFormField(mw, "paperHeight", v)
case "contentScale":
err = d.addFormField(mw, "scale", v)
case "orientation":
if v == "landscape" {
err = d.addFormField(mw, "landscape", "true")
} else {
err = d.addFormField(mw, "landscape", "false")
}
}
if err != nil {
return err
}
}
return nil
}
func (d *gotenbergPDFDriver) addFormField(mw *multipart.Writer, k, v string) error {
w, err := mw.CreateFormField(k)
if err != nil {
return err
}
_, err = w.Write([]byte(v))
return err
}
// documentDimensions returns the ISO216 standard document dimensions in inches (Gotenberg uses inches)
func (d *gotenbergPDFDriver) documentDimensions(isoDoc string) (string, string) {
switch strings.ToLower(isoDoc) {
// A series
case "a0":
return "33.1", "46.8"
case "a1":
return "23.4", "33.1"
case "a2":
return "16.5", "23.4"
case "a3":
return "11.7", "16.5"
case "a4":
return "8.3", "11.7"
case "a5":
return "5.8", "8.3"
case "a6":
return "4.1", "5.8"
case "a7":
return "2.9", "4.1"
case "a8":
return "2.0", "2.9"
case "a9":
return "1.5", "2.0"
case "a10":
return "1.0", "1.5"
// B series
case "b0":
return "39.4", "55.7"
case "b1":
return "27.8", "39.4"
case "b2":
return "19.7", "27.8"
case "b3":
return "13.9", "19.7"
case "b4":
return "9.8", "13.9"
case "b5":
return "6.9", "9.8"
case "b6":
return "4.9", "6.9"
case "b7":
return "3.5", "4.9"
case "b8":
return "2.4", "3.5"
case "b9":
return "1.7", "2.4"
case "b10":
return "1.2", "1.7"
// C series
case "c0":
return "36.1", "51.1"
case "c1":
return "25.5", "36.1"
case "c2":
return "18.0", "25.5"
case "c3":
return "12.8", "18.0"
case "c4":
return "9.0", "12.8"
case "c5":
return "6.4", "9.0"
case "c6":
return "4.5", "6.4"
case "c7":
return "3.2", "4.5"
case "c8":
return "2.2", "3.2"
case "c9":
return "1.6", "2.2"
case "c10":
return "1.1", "1.6"
// ANSI
case "ansi a":
return "8.5", "11"
case "ansi b":
return "11", "17"
case "ansi c":
return "17", "22"
case "ansi d":
return "22", "34"
case "ansi e":
return "34", "44"
// Proprietary NA
case "junior legal":
return "8", "5"
case "letter":
return "8.5", "11"
case "legal":
return "8.5", "14"
case "tabloid":
return "11", "17"
}
// This will fallback to the default (A4)
return "", ""
}

View File

@@ -0,0 +1,49 @@
package renderer
import (
"context"
"errors"
"io"
"github.com/cortezaproject/corteza-server/pkg/options"
)
type (
renderer struct {
factories []driverFactory
}
)
func Renderer(cfg options.TemplateOpt) *renderer {
ff := make([]driverFactory, 0, 3)
ff = append(ff, newGenericTXT(), newGenericHTML())
if cfg.RendererGotenbergEnabled {
ff = append(ff, newGotenbergPDF(cfg.RendererGotenbergAddress))
}
return &renderer{
factories: []driverFactory{},
}
}
func (r *renderer) Render(ctx context.Context, pl *RendererPayload) (io.ReadSeeker, error) {
for _, f := range r.factories {
if f.CanRender(pl.TemplateType) && f.CanProduce(pl.TargetType) {
pp := make(map[string]io.Reader)
for _, prt := range pl.Partials {
pp[prt.Handle] = prt.Template
}
dpl := &driverPayload{
Template: pl.Template,
Variables: pl.Variables,
Options: pl.Options,
Partials: pp,
Attachments: pl.Attachments,
}
return f.Driver().Render(ctx, dpl)
}
}
return nil, errors.New("rendering failed: driver not found")
}

50
system/renderer/types.go Normal file
View File

@@ -0,0 +1,50 @@
package renderer
import (
"context"
"io"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
RendererPayload struct {
Template io.Reader
TemplateType types.DocumentType
TargetType types.DocumentType
Variables map[string]interface{}
Options map[string]string
Partials []*TemplatePartial
Attachments AttachmentIndex
}
TemplatePartial struct {
Handle string
Template io.Reader
TemplateType types.DocumentType
}
driverPayload struct {
Template io.Reader
Variables map[string]interface{}
Options map[string]string
Partials map[string]io.Reader
Attachments AttachmentIndex
}
AttachmentIndex map[string]*Attachment
Attachment struct {
Source io.Reader
Mime string
Name string
}
driverFactory interface {
CanRender(t types.DocumentType) bool
CanProduce(t types.DocumentType) bool
Driver() driver
}
driver interface {
Render(ctx context.Context, pl *driverPayload) (io.ReadSeeker, error)
}
)

107
system/renderer/util.go Normal file
View File

@@ -0,0 +1,107 @@
package renderer
import (
"encoding/base64"
htpl "html/template"
ttpl "html/template"
"io"
"io/ioutil"
"net/http"
"github.com/Masterminds/sprig"
)
func preprocHTMLTemplate(pl *driverPayload) (*htpl.Template, error) {
bb, err := ioutil.ReadAll(pl.Template)
if err != nil {
return nil, err
}
gtpl := htpl.New("text/html_render").
Funcs(sprig.FuncMap()).
Funcs(htpl.FuncMap{
// "attachDataURL": func(name string) htpl.URL {
// // Find the attachment
// att, has := pl.Attachments[name]
// if !has {
// return htpl.URL(fmt.Sprintf("error: attachment not found: %s", name))
// }
// // Process source
// bb, err := ioutil.ReadAll(att.Source)
// if err != nil {
// return htpl.URL(fmt.Sprintf("error: %s", err.Error()))
// }
// return htpl.URL("data:" + att.Mime + ";base64," + base64.RawStdEncoding.EncodeToString(bb))
// },
"inlineRemote": func(url string) (htpl.URL, error) {
rsp, err := http.Get(url)
if err != nil {
return "", err
}
defer rsp.Body.Close()
bb, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return "", err
}
raw := base64.RawStdEncoding.EncodeToString(bb)
return htpl.URL("data:" + rsp.Header.Get("Content-Type") + ";base64," + raw), nil
},
})
// Prep the original template
t, err := gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
// Prep partials
for _, p := range pl.Partials {
bb, err = ioutil.ReadAll(p)
if err != nil {
return nil, err
}
t, err = gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
}
return t, nil
}
func preprocPlainTemplate(tpl io.Reader, pp map[string]io.Reader) (*ttpl.Template, error) {
bb, err := ioutil.ReadAll(tpl)
if err != nil {
return nil, err
}
gtpl := ttpl.New("text/plain_render")
// Prep the original template
t, err := gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
// Prep partials
for _, p := range pp {
bb, err = ioutil.ReadAll(p)
if err != nil {
return nil, err
}
t, err = gtpl.Parse(string(bb))
if err != nil {
return nil, err
}
}
return t, nil
}

View File

@@ -1122,6 +1122,178 @@ endpoints:
type: string
required: true
title: Preview extension/format
- title: Template
path: "/template"
entrypoint: template
imports:
- github.com/cortezaproject/corteza-server/system/types
- github.com/cortezaproject/corteza-server/pkg/label
authentication:
- Client ID
- SessionID
apis:
- name: list
method: GET
title: List templates
path: "/"
parameters:
get:
- name: handle
type: string
title: Handle
- name: type
type: string
title: Type
- name: ownerID
type: uint64
title: OwnerID
- name: partial
required: false
title: Show partial templates
type: bool
- name: deleted
required: false
title: Exclude (0, default), include (1) or return only (2) deleted templates
type: uint
- name: labels
type: map[string]string
title: Labels
parser: label.ParseStrings
- name: limit
type: uint
title: Limit
- name: pageCursor
type: string
title: Page cursor
- name: sort
type: string
title: Sort items
- name: create
method: POST
title: Create template
path: "/"
parameters:
post:
- name: handle
type: string
title: Handle
- name: language
type: string
title: Language
- name: type
type: string
title: Type
- name: partial
type: bool
title: Partial
- name: meta
type: "types.TemplateMeta"
parser: "types.ParseTemplateMeta"
title: Meta
- name: template
type: string
title: Template
- name: ownerID
type: uint64
title: OwnerID
- name: labels
type: map[string]string
title: Labels
parser: label.ParseStrings
- name: read
method: GET
title: Read template
path: "/{templateID}"
parameters:
path:
- type: uint64
name: templateID
required: true
title: ID
- name: update
method: PUT
title: Update template
path: "/{templateID}"
parameters:
path:
- type: uint64
name: templateID
required: true
title: ID
post:
- name: handle
type: string
title: Handle
- name: language
type: string
title: Language
- name: type
type: string
title: Type
- name: partial
type: bool
title: Partial
- name: meta
type: "types.TemplateMeta"
parser: "types.ParseTemplateMeta"
title: Meta
- name: template
type: string
title: Template
- name: ownerID
type: uint64
title: OwnerID
- name: labels
type: map[string]string
title: Labels
parser: label.ParseStrings
- name: delete
method: DELETE
title: Delete template
path: "/{templateID}"
parameters:
path:
- type: uint64
name: templateID
required: true
title: ID
- name: undelete
path: "/{templateID}/undelete"
method: POST
title: Undelete template
parameters:
path:
- name: templateID
type: uint64
required: true
title: Template ID
- name: render
method: POST
title: Render template
path: "/{templateID}/render/{filename}.{ext}"
parameters:
path:
- type: uint64
name: templateID
required: true
title: Render template to use
- type: string
name: filename
required: true
title: Filename to use
- type: string
name: ext
required: true
title: Export format
post:
- name: variables
type: json.RawMessage
required: true
title: Variables defined by import file
- name: options
type: json.RawMessage
required: false
title: Rendering options
- title: Statistics
entrypoint: stats
path: "/stats"

View File

@@ -0,0 +1,171 @@
package handlers
// 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:
//
import (
"context"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/go-chi/chi"
"net/http"
)
type (
// Internal API interface
TemplateAPI interface {
List(context.Context, *request.TemplateList) (interface{}, error)
Create(context.Context, *request.TemplateCreate) (interface{}, error)
Read(context.Context, *request.TemplateRead) (interface{}, error)
Update(context.Context, *request.TemplateUpdate) (interface{}, error)
Delete(context.Context, *request.TemplateDelete) (interface{}, error)
Undelete(context.Context, *request.TemplateUndelete) (interface{}, error)
Render(context.Context, *request.TemplateRender) (interface{}, error)
}
// HTTP API interface
Template struct {
List func(http.ResponseWriter, *http.Request)
Create func(http.ResponseWriter, *http.Request)
Read func(http.ResponseWriter, *http.Request)
Update func(http.ResponseWriter, *http.Request)
Delete func(http.ResponseWriter, *http.Request)
Undelete func(http.ResponseWriter, *http.Request)
Render func(http.ResponseWriter, *http.Request)
}
)
func NewTemplate(h TemplateAPI) *Template {
return &Template{
List: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateList()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.List(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Create: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateCreate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Create(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Read: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateRead()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Read(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Update: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateUpdate()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Update(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Delete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateDelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Delete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Undelete: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateUndelete()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Undelete(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
Render: func(w http.ResponseWriter, r *http.Request) {
defer r.Body.Close()
params := request.NewTemplateRender()
if err := params.Fill(r); err != nil {
api.Send(w, r, err)
return
}
value, err := h.Render(r.Context(), params)
if err != nil {
api.Send(w, r, err)
return
}
api.Send(w, r, value)
},
}
}
func (h Template) MountRoutes(r chi.Router, middlewares ...func(http.Handler) http.Handler) {
r.Group(func(r chi.Router) {
r.Use(middlewares...)
r.Get("/template/", h.List)
r.Post("/template/", h.Create)
r.Get("/template/{templateID}", h.Read)
r.Put("/template/{templateID}", h.Update)
r.Delete("/template/{templateID}", h.Delete)
r.Post("/template/{templateID}/undelete", h.Undelete)
r.Post("/template/{templateID}/render/{filename}.{ext}", h.Render)
})
}

View File

@@ -0,0 +1,917 @@
package request
// 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:
//
import (
"encoding/json"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/label"
"github.com/cortezaproject/corteza-server/pkg/payload"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/go-chi/chi"
"io"
"mime/multipart"
"net/http"
"strings"
)
// dummy vars to prevent
// unused imports complain
var (
_ = chi.URLParam
_ = multipart.ErrMessageTooLarge
_ = payload.ParseUint64s
)
type (
// Internal API interface
TemplateList struct {
// Handle GET parameter
//
// Handle
Handle string
// Type GET parameter
//
// Type
Type string
// OwnerID GET parameter
//
// OwnerID
OwnerID uint64 `json:",string"`
// Partial GET parameter
//
// Show partial templates
Partial bool
// Deleted GET parameter
//
// Exclude (0, default), include (1) or return only (2) deleted templates
Deleted uint
// Labels GET parameter
//
// Labels
Labels map[string]string
// Limit GET parameter
//
// Limit
Limit uint
// PageCursor GET parameter
//
// Page cursor
PageCursor string
// Sort GET parameter
//
// Sort items
Sort string
}
TemplateCreate struct {
// Handle POST parameter
//
// Handle
Handle string
// Language POST parameter
//
// Language
Language string
// Type POST parameter
//
// Type
Type string
// Partial POST parameter
//
// Partial
Partial bool
// Meta POST parameter
//
// Meta
Meta types.TemplateMeta
// Template POST parameter
//
// Template
Template string
// OwnerID POST parameter
//
// OwnerID
OwnerID uint64 `json:",string"`
// Labels POST parameter
//
// Labels
Labels map[string]string
}
TemplateRead struct {
// TemplateID PATH parameter
//
// ID
TemplateID uint64 `json:",string"`
}
TemplateUpdate struct {
// TemplateID PATH parameter
//
// ID
TemplateID uint64 `json:",string"`
// Handle POST parameter
//
// Handle
Handle string
// Language POST parameter
//
// Language
Language string
// Type POST parameter
//
// Type
Type string
// Partial POST parameter
//
// Partial
Partial bool
// Meta POST parameter
//
// Meta
Meta types.TemplateMeta
// Template POST parameter
//
// Template
Template string
// OwnerID POST parameter
//
// OwnerID
OwnerID uint64 `json:",string"`
// Labels POST parameter
//
// Labels
Labels map[string]string
}
TemplateDelete struct {
// TemplateID PATH parameter
//
// ID
TemplateID uint64 `json:",string"`
}
TemplateUndelete struct {
// TemplateID PATH parameter
//
// Template ID
TemplateID uint64 `json:",string"`
}
TemplateRender struct {
// TemplateID PATH parameter
//
// Render template to use
TemplateID uint64 `json:",string"`
// Filename PATH parameter
//
// Filename to use
Filename string
// Ext PATH parameter
//
// Export format
Ext string
// Variables POST parameter
//
// Variables defined by import file
Variables json.RawMessage
// Options POST parameter
//
// Rendering options
Options json.RawMessage
}
)
// NewTemplateList request
func NewTemplateList() *TemplateList {
return &TemplateList{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) Auditable() map[string]interface{} {
return map[string]interface{}{
"handle": r.Handle,
"type": r.Type,
"ownerID": r.OwnerID,
"partial": r.Partial,
"deleted": r.Deleted,
"labels": r.Labels,
"limit": r.Limit,
"pageCursor": r.PageCursor,
"sort": r.Sort,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetType() string {
return r.Type
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetOwnerID() uint64 {
return r.OwnerID
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetPartial() bool {
return r.Partial
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetDeleted() uint {
return r.Deleted
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetLabels() map[string]string {
return r.Labels
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetLimit() uint {
return r.Limit
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetPageCursor() string {
return r.PageCursor
}
// Auditable returns all auditable/loggable parameters
func (r TemplateList) GetSort() string {
return r.Sort
}
// Fill processes request and fills internal variables
func (r *TemplateList) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
// GET params
tmp := req.URL.Query()
if val, ok := tmp["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["type"]; ok && len(val) > 0 {
r.Type, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["ownerID"]; ok && len(val) > 0 {
r.OwnerID, err = payload.ParseUint64(val[0]), nil
if err != nil {
return err
}
}
if val, ok := tmp["partial"]; ok && len(val) > 0 {
r.Partial, err = payload.ParseBool(val[0]), nil
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 {
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 {
return err
}
}
if val, ok := tmp["pageCursor"]; ok && len(val) > 0 {
r.PageCursor, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := tmp["sort"]; ok && len(val) > 0 {
r.Sort, err = val[0], nil
if err != nil {
return err
}
}
}
return err
}
// NewTemplateCreate request
func NewTemplateCreate() *TemplateCreate {
return &TemplateCreate{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) Auditable() map[string]interface{} {
return map[string]interface{}{
"handle": r.Handle,
"language": r.Language,
"type": r.Type,
"partial": r.Partial,
"meta": r.Meta,
"template": r.Template,
"ownerID": r.OwnerID,
"labels": r.Labels,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetLanguage() string {
return r.Language
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetType() string {
return r.Type
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetPartial() bool {
return r.Partial
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetMeta() types.TemplateMeta {
return r.Meta
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetTemplate() string {
return r.Template
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetOwnerID() uint64 {
return r.OwnerID
}
// Auditable returns all auditable/loggable parameters
func (r TemplateCreate) GetLabels() map[string]string {
return r.Labels
}
// Fill processes request and fills internal variables
func (r *TemplateCreate) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["language"]; ok && len(val) > 0 {
r.Language, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["type"]; ok && len(val) > 0 {
r.Type, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["partial"]; ok && len(val) > 0 {
r.Partial, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["meta[]"]; ok {
r.Meta, err = types.ParseTemplateMeta(val)
if err != nil {
return err
}
} else if val, ok := req.Form["meta"]; ok {
r.Meta, err = types.ParseTemplateMeta(val)
if err != nil {
return err
}
}
if val, ok := req.Form["template"]; ok && len(val) > 0 {
r.Template, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["ownerID"]; ok && len(val) > 0 {
r.OwnerID, err = payload.ParseUint64(val[0]), nil
if err != nil {
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
}
}
}
return err
}
// NewTemplateRead request
func NewTemplateRead() *TemplateRead {
return &TemplateRead{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRead) Auditable() map[string]interface{} {
return map[string]interface{}{
"templateID": r.TemplateID,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRead) GetTemplateID() uint64 {
return r.TemplateID
}
// Fill processes request and fills internal variables
func (r *TemplateRead) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
var val string
// path params
val = chi.URLParam(req, "templateID")
r.TemplateID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewTemplateUpdate request
func NewTemplateUpdate() *TemplateUpdate {
return &TemplateUpdate{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) Auditable() map[string]interface{} {
return map[string]interface{}{
"templateID": r.TemplateID,
"handle": r.Handle,
"language": r.Language,
"type": r.Type,
"partial": r.Partial,
"meta": r.Meta,
"template": r.Template,
"ownerID": r.OwnerID,
"labels": r.Labels,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetTemplateID() uint64 {
return r.TemplateID
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetHandle() string {
return r.Handle
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetLanguage() string {
return r.Language
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetType() string {
return r.Type
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetPartial() bool {
return r.Partial
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetMeta() types.TemplateMeta {
return r.Meta
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetTemplate() string {
return r.Template
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetOwnerID() uint64 {
return r.OwnerID
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUpdate) GetLabels() map[string]string {
return r.Labels
}
// Fill processes request and fills internal variables
func (r *TemplateUpdate) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["handle"]; ok && len(val) > 0 {
r.Handle, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["language"]; ok && len(val) > 0 {
r.Language, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["type"]; ok && len(val) > 0 {
r.Type, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["partial"]; ok && len(val) > 0 {
r.Partial, err = payload.ParseBool(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["meta[]"]; ok {
r.Meta, err = types.ParseTemplateMeta(val)
if err != nil {
return err
}
} else if val, ok := req.Form["meta"]; ok {
r.Meta, err = types.ParseTemplateMeta(val)
if err != nil {
return err
}
}
if val, ok := req.Form["template"]; ok && len(val) > 0 {
r.Template, err = val[0], nil
if err != nil {
return err
}
}
if val, ok := req.Form["ownerID"]; ok && len(val) > 0 {
r.OwnerID, err = payload.ParseUint64(val[0]), nil
if err != nil {
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
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "templateID")
r.TemplateID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewTemplateDelete request
func NewTemplateDelete() *TemplateDelete {
return &TemplateDelete{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateDelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"templateID": r.TemplateID,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateDelete) GetTemplateID() uint64 {
return r.TemplateID
}
// Fill processes request and fills internal variables
func (r *TemplateDelete) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
var val string
// path params
val = chi.URLParam(req, "templateID")
r.TemplateID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewTemplateUndelete request
func NewTemplateUndelete() *TemplateUndelete {
return &TemplateUndelete{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUndelete) Auditable() map[string]interface{} {
return map[string]interface{}{
"templateID": r.TemplateID,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateUndelete) GetTemplateID() uint64 {
return r.TemplateID
}
// Fill processes request and fills internal variables
func (r *TemplateUndelete) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
var val string
// path params
val = chi.URLParam(req, "templateID")
r.TemplateID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
}
return err
}
// NewTemplateRender request
func NewTemplateRender() *TemplateRender {
return &TemplateRender{}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRender) Auditable() map[string]interface{} {
return map[string]interface{}{
"templateID": r.TemplateID,
"filename": r.Filename,
"ext": r.Ext,
"variables": r.Variables,
"options": r.Options,
}
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRender) GetTemplateID() uint64 {
return r.TemplateID
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRender) GetFilename() string {
return r.Filename
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRender) GetExt() string {
return r.Ext
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRender) GetVariables() json.RawMessage {
return r.Variables
}
// Auditable returns all auditable/loggable parameters
func (r TemplateRender) GetOptions() json.RawMessage {
return r.Options
}
// Fill processes request and fills internal variables
func (r *TemplateRender) Fill(req *http.Request) (err error) {
if strings.ToLower(req.Header.Get("content-type")) == "application/json" {
err = json.NewDecoder(req.Body).Decode(r)
switch {
case err == io.EOF:
err = nil
case err != nil:
return fmt.Errorf("error parsing http request body: %w", err)
}
}
{
if err = req.ParseForm(); err != nil {
return err
}
// POST params
if val, ok := req.Form["variables"]; ok && len(val) > 0 {
r.Variables, err = json.RawMessage(val[0]), nil
if err != nil {
return err
}
}
if val, ok := req.Form["options"]; ok && len(val) > 0 {
r.Options, err = json.RawMessage(val[0]), nil
if err != nil {
return err
}
}
}
{
var val string
// path params
val = chi.URLParam(req, "templateID")
r.TemplateID, err = payload.ParseUint64(val), nil
if err != nil {
return err
}
val = chi.URLParam(req, "filename")
r.Filename, err = val, nil
if err != nil {
return err
}
val = chi.URLParam(req, "ext")
r.Ext, err = val, nil
if err != nil {
return err
}
}
return err
}

View File

@@ -34,6 +34,7 @@ func MountRoutes(r chi.Router) {
handlers.NewRole(Role{}.New()).MountRoutes(r)
handlers.NewPermissions(Permissions{}.New()).MountRoutes(r)
handlers.NewApplication(Application{}.New()).MountRoutes(r)
handlers.NewTemplate(Template{}.New()).MountRoutes(r)
handlers.NewSettings(Settings{}.New()).MountRoutes(r)
handlers.NewStats(Stats{}.New()).MountRoutes(r)
handlers.NewReminder(Reminder{}.New()).MountRoutes(r)

216
system/rest/template.go Normal file
View File

@@ -0,0 +1,216 @@
package rest
import (
"context"
"encoding/json"
"io"
"net/http"
"net/url"
"strings"
"time"
"github.com/cortezaproject/corteza-server/pkg/api"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/system/rest/request"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/pkg/errors"
)
var _ = errors.Wrap
type (
Template struct {
renderer service.TemplateService
ac templateAccessController
}
templateSetPayload struct {
Filter types.TemplateFilter `json:"filter"`
Set []*templatePayload `json:"set"`
}
templatePayload struct {
*types.Template
CanGrant bool `json:"canGrant"`
CanUpdateTemplate bool `json:"canUpdateTemplate"`
CanDeleteTemplate bool `json:"canDeleteTemplate"`
}
templateAccessController interface {
CanGrant(context.Context) bool
CanAccess(context.Context) bool
CanCreateTemplate(context.Context) bool
CanReadTemplate(context.Context, *types.Template) bool
CanUpdateTemplate(context.Context, *types.Template) bool
CanDeleteTemplate(context.Context, *types.Template) bool
}
)
func (Template) New() *Template {
return &Template{
renderer: service.DefaultRenderer,
ac: service.DefaultAccessControl,
}
}
func (ctrl *Template) Read(ctx context.Context, r *request.TemplateRead) (interface{}, error) {
tpl, err := ctrl.renderer.FindByID(ctx, r.TemplateID)
return ctrl.makePayload(ctx, tpl, err)
}
func (ctrl *Template) List(ctx context.Context, r *request.TemplateList) (interface{}, error) {
var (
err error
f = types.TemplateFilter{
Handle: r.Handle,
Type: r.Type,
OwnerID: r.OwnerID,
Partial: r.Partial,
Deleted: filter.State(r.Deleted),
}
)
if f.Paging, err = filter.NewPaging(r.Limit, r.PageCursor); err != nil {
return nil, err
}
if f.Sorting, err = filter.NewSorting(r.Sort); err != nil {
return nil, err
}
set, filter, err := ctrl.renderer.Search(ctx, f)
return ctrl.makeFilterPayload(ctx, set, filter, err)
}
func (ctrl *Template) Create(ctx context.Context, r *request.TemplateCreate) (interface{}, error) {
var (
err error
app = &types.Template{
Handle: r.Handle,
Language: r.Language,
Type: types.DocumentType(r.Type),
Partial: r.Partial,
Meta: r.Meta,
Template: r.Template,
OwnerID: r.OwnerID,
}
)
app, err = ctrl.renderer.Create(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Template) Update(ctx context.Context, r *request.TemplateUpdate) (interface{}, error) {
var (
err error
app = &types.Template{
ID: r.TemplateID,
Handle: r.Handle,
Language: r.Language,
Type: types.DocumentType(r.Type),
Partial: r.Partial,
Meta: r.Meta,
Template: r.Template,
OwnerID: r.OwnerID,
}
)
app, err = ctrl.renderer.Update(ctx, app)
return ctrl.makePayload(ctx, app, err)
}
func (ctrl *Template) Delete(ctx context.Context, r *request.TemplateDelete) (interface{}, error) {
return api.OK(), ctrl.renderer.DeleteByID(ctx, r.TemplateID)
}
func (ctrl *Template) Undelete(ctx context.Context, r *request.TemplateUndelete) (interface{}, error) {
return api.OK(), ctrl.renderer.UndeleteByID(ctx, r.TemplateID)
}
func (ctrl *Template) Render(ctx context.Context, r *request.TemplateRender) (interface{}, error) {
vars := make(map[string]interface{})
err := json.Unmarshal(r.Variables, &vars)
if err != nil {
return nil, err
}
opts := make(map[string]string)
if r.Options != nil {
err = json.Unmarshal(r.Options, &opts)
if err != nil {
return nil, err
}
}
ct := ctrl.getDestinationType(r.Ext)
doc, err := ctrl.renderer.Render(ctx, r.TemplateID, ct, vars, opts)
return ctrl.serve(doc, ct, r, err)
}
// Utilities
func (ctrl Template) makeFilterPayload(ctx context.Context, nn types.TemplateSet, f types.TemplateFilter, err error) (*templateSetPayload, error) {
if err != nil {
return nil, err
}
msp := &templateSetPayload{Filter: f, Set: make([]*templatePayload, len(nn))}
for i := range nn {
msp.Set[i], _ = ctrl.makePayload(ctx, nn[i], nil)
}
return msp, nil
}
func (ctrl Template) makePayload(ctx context.Context, tpl *types.Template, err error) (*templatePayload, error) {
if err != nil || tpl == nil {
return nil, err
}
pl := &templatePayload{
Template: tpl,
CanGrant: ctrl.ac.CanGrant(ctx),
CanUpdateTemplate: ctrl.ac.CanUpdateTemplate(ctx, tpl),
CanDeleteTemplate: ctrl.ac.CanDeleteTemplate(ctx, tpl),
}
return pl, nil
}
func (ctrl *Template) serve(doc io.ReadSeeker, ct string, r *request.TemplateRender, err error) (interface{}, error) {
if err != nil {
return nil, err
}
return func(w http.ResponseWriter, req *http.Request) {
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
name := url.QueryEscape(strings.TrimSpace(r.Filename) + "." + strings.TrimSpace(r.Ext))
w.Header().Add("Content-Disposition", "attachment; filename="+name)
w.Header().Add("Content-Type", ct+"; charset=utf-8")
http.ServeContent(w, req, name, time.Now(), doc)
}, nil
}
func (ctrl *Template) getDestinationType(ext string) string {
switch ext {
case "txt":
return "text/plain"
case "html":
return "text/html"
case "pdf":
return "application/pdf"
}
return "text/plain"
}

View File

@@ -43,6 +43,7 @@ func (svc accessControl) Effective(ctx context.Context) (ee rbac.EffectiveSet) {
ee.Push(types.SystemRBACResource, "settings.read", svc.CanReadSettings(ctx))
ee.Push(types.SystemRBACResource, "settings.manage", svc.CanManageSettings(ctx))
ee.Push(types.SystemRBACResource, "application.create", svc.CanCreateApplication(ctx))
ee.Push(types.SystemRBACResource, "template.create", svc.CanCreateTemplate(ctx))
ee.Push(types.SystemRBACResource, "role.create", svc.CanCreateRole(ctx))
return
@@ -76,6 +77,10 @@ func (svc accessControl) CanCreateApplication(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "application.create")
}
func (svc accessControl) CanCreateTemplate(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "template.create")
}
func (svc accessControl) CanAssignReminder(ctx context.Context) bool {
return svc.can(ctx, types.SystemRBACResource, "reminder.assign")
}
@@ -119,6 +124,22 @@ func (svc accessControl) CanDeleteApplication(ctx context.Context, app *types.Ap
return svc.can(ctx, app.RBACResource(), "delete")
}
func (svc accessControl) CanReadTemplate(ctx context.Context, tpl *types.Template) bool {
return svc.can(ctx, tpl.RBACResource(), "read", rbac.Allowed)
}
func (svc accessControl) CanUpdateTemplate(ctx context.Context, tpl *types.Template) bool {
return svc.can(ctx, tpl.RBACResource(), "update")
}
func (svc accessControl) CanDeleteTemplate(ctx context.Context, tpl *types.Template) bool {
return svc.can(ctx, tpl.RBACResource(), "delete")
}
func (svc accessControl) CanRenderTemplate(ctx context.Context, tpl *types.Template) bool {
return svc.can(ctx, tpl.RBACResource(), "render", rbac.Allowed)
}
func (svc accessControl) CanReadUser(ctx context.Context, u *types.User) bool {
return svc.can(ctx, u.RBACResource(), "read")
}
@@ -225,6 +246,7 @@ func (svc accessControl) Whitelist() rbac.Whitelist {
"role.create",
"user.create",
"application.create",
"template.create",
"reminder.assign",
)
@@ -235,6 +257,14 @@ func (svc accessControl) Whitelist() rbac.Whitelist {
"delete",
)
wl.Set(
types.TemplateRBACResource,
"read",
"update",
"delete",
"render",
)
wl.Set(
types.UserRBACResource,
"read",

View File

@@ -4,8 +4,9 @@ import (
"bytes"
"context"
"fmt"
htpl "html/template"
"github.com/cortezaproject/corteza-server/system/types"
"html/template"
"go.uber.org/zap"
"go.uber.org/zap/zapcore"
@@ -30,11 +31,11 @@ type (
EmailAddress string
URL string
BaseURL string
Logo template.URL
Logo htpl.URL
SignatureName string
SignatureEmail string
EmailHeaderEn template.HTML
EmailFooterEn template.HTML
EmailHeaderEn htpl.HTML
EmailFooterEn htpl.HTML
}
)
@@ -84,7 +85,7 @@ func (svc authNotification) send(ctx context.Context, name, lang string, payload
ntf = svc.newMail()
)
payload.Logo = template.URL(svc.settings.General.Mail.Logo)
payload.Logo = htpl.URL(svc.settings.General.Mail.Logo)
payload.BaseURL = svc.settings.Auth.Frontend.Url.Base
payload.SignatureName = svc.settings.Auth.Mail.FromName
payload.SignatureEmail = svc.settings.Auth.Mail.FromAddress
@@ -93,11 +94,11 @@ func (svc authNotification) send(ctx context.Context, name, lang string, payload
if tmp, err = svc.render(svc.settings.General.Mail.Header, payload); err != nil {
return fmt.Errorf("failed to render svc.settings.General.Mail.Header: %w", err)
}
payload.EmailHeaderEn = template.HTML(tmp)
payload.EmailHeaderEn = htpl.HTML(tmp)
if tmp, err = svc.render(svc.settings.General.Mail.Footer, payload); err != nil {
return fmt.Errorf("failed to render svc.settings.General.Mail.Footer: %w", err)
}
payload.EmailFooterEn = template.HTML(tmp)
payload.EmailFooterEn = htpl.HTML(tmp)
ntf.SetAddressHeader("To", payload.EmailAddress, "")
// @todo translations
@@ -139,11 +140,11 @@ func (svc authNotification) send(ctx context.Context, name, lang string, payload
func (svc authNotification) render(source string, payload interface{}) (string, error) {
var (
err error
tpl *template.Template
tpl *htpl.Template
buf = bytes.Buffer{}
)
tpl, err = template.New("").Parse(source)
tpl, err = htpl.New("").Parse(source)
if err != nil {
return "", fmt.Errorf("could not parse template: %w", err)
}

View File

@@ -3,9 +3,10 @@ package service
import (
"context"
"errors"
"github.com/cortezaproject/corteza-server/pkg/logger"
"time"
"github.com/cortezaproject/corteza-server/pkg/logger"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
intAuth "github.com/cortezaproject/corteza-server/pkg/auth"
"github.com/cortezaproject/corteza-server/pkg/eventbus"
@@ -30,6 +31,7 @@ type (
Config struct {
ActionLog options.ActionLogOpt
Storage options.ObjectStoreOpt
Template options.TemplateOpt
}
permitChecker interface {
@@ -86,6 +88,7 @@ var (
DefaultApplication *application
DefaultReminder ReminderService
DefaultAttachment AttachmentService
DefaultRenderer TemplateService
DefaultStatistics *statistics
@@ -176,6 +179,7 @@ func Initialize(ctx context.Context, log *zap.Logger, s store.Storer, c Config)
DefaultSink = Sink()
DefaultStatistics = Statistics()
DefaultAttachment = Attachment(DefaultObjectStore)
DefaultRenderer = Renderer(c.Template)
return
}
@@ -211,3 +215,19 @@ func unwrapGeneric(err error) error {
return err
}
}
// Data is stale when new date does not match updatedAt or createdAt (before first update)
//
// @todo This is the same as in compose.service; do we want to make an util thing?
func isStale(new *time.Time, updatedAt *time.Time, createdAt time.Time) bool {
if new == nil {
// Change to true for stale-data-check
return false
}
if updatedAt != nil {
return !new.Equal(*updatedAt)
}
return new.Equal(createdAt)
}

467
system/service/template.go Normal file
View File

@@ -0,0 +1,467 @@
package service
import (
"bytes"
"context"
"io"
"strconv"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/handle"
"github.com/cortezaproject/corteza-server/pkg/label"
"github.com/cortezaproject/corteza-server/pkg/options"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/renderer"
"github.com/cortezaproject/corteza-server/system/types"
)
type (
template struct {
actionlog actionlog.Recorder
store store.Storer
ac templateAccessController
renderer rendererService
}
templateAccessController interface {
CanAccess(context.Context) bool
CanCreateTemplate(context.Context) bool
CanReadTemplate(context.Context, *types.Template) bool
CanUpdateTemplate(context.Context, *types.Template) bool
CanDeleteTemplate(context.Context, *types.Template) bool
CanRenderTemplate(context.Context, *types.Template) bool
}
rendererService interface {
Render(ctx context.Context, p *renderer.RendererPayload) (io.ReadSeeker, error)
}
TemplateService interface {
FindByID(ctx context.Context, ID uint64) (*types.Template, error)
FindByHandle(ct context.Context, handle string) (*types.Template, error)
FindByAny(ctx context.Context, identifier interface{}) (*types.Template, error)
Search(context.Context, types.TemplateFilter) (types.TemplateSet, types.TemplateFilter, error)
Create(ctx context.Context, tpl *types.Template) (*types.Template, error)
Update(ctx context.Context, tpl *types.Template) (*types.Template, error)
DeleteByID(ctx context.Context, ID uint64) error
UndeleteByID(ctx context.Context, ID uint64) error
Render(ctx context.Context, templateID uint64, dstType string, variables map[string]interface{}, options map[string]string) (io.ReadSeeker, error)
}
)
func Renderer(cfg options.TemplateOpt) TemplateService {
return (&template{
actionlog: DefaultActionlog,
store: DefaultStore,
ac: DefaultAccessControl,
renderer: renderer.Renderer(cfg),
})
}
func (svc template) FindByID(ctx context.Context, ID uint64) (tpl *types.Template, err error) {
var (
tplProps = &templateActionProps{template: &types.Template{ID: ID}}
)
err = func() error {
if ID == 0 {
return TemplateErrInvalidID()
}
if tpl, err = store.LookupTemplateByID(ctx, svc.store, ID); err != nil {
return TemplateErrInvalidID().Wrap(err)
}
tplProps.setTemplate(tpl)
if !svc.ac.CanReadTemplate(ctx, tpl) {
return TemplateErrNotAllowedToRead()
}
return nil
}()
return tpl, svc.recordAction(ctx, tplProps, TemplateActionLookup, err)
}
func (svc template) FindByHandle(ctx context.Context, h string) (tpl *types.Template, err error) {
var (
tplProps = &templateActionProps{template: &types.Template{Handle: h}}
)
err = func() error {
if h == "" || !handle.IsValid(h) {
return TemplateErrInvalidHandle()
}
if tpl, err = store.LookupTemplateByHandle(ctx, svc.store, h); err != nil {
return TemplateErrInvalidHandle().Wrap(err)
}
tplProps.setTemplate(tpl)
if !svc.ac.CanReadTemplate(ctx, tpl) {
return TemplateErrNotAllowedToRead()
}
return nil
}()
return tpl, svc.recordAction(ctx, tplProps, TemplateActionLookup, err)
}
func (svc template) FindByAny(ctx context.Context, identifier interface{}) (tpl *types.Template, err error) {
if ID, ok := identifier.(uint64); ok {
tpl, err = svc.FindByID(ctx, ID)
} else if strIdentifier, ok := identifier.(string); ok {
if ID, _ := strconv.ParseUint(strIdentifier, 10, 64); ID > 0 {
tpl, err = svc.FindByID(ctx, ID)
} else {
tpl, err = svc.FindByHandle(ctx, strIdentifier)
}
} else {
err = TemplateErrInvalidID()
}
if err != nil {
return
}
return
}
func (svc template) Search(ctx context.Context, filter types.TemplateFilter) (set types.TemplateSet, f types.TemplateFilter, err error) {
var (
aProps = &templateActionProps{filter: &filter}
)
// For each fetched item, store backend will check if it is valid or not
filter.Check = func(res *types.Template) (bool, error) {
if !svc.ac.CanReadTemplate(ctx, res) {
return false, nil
}
return true, nil
}
err = func() error {
if len(filter.Labels) > 0 {
filter.LabeledIDs, err = label.Search(
ctx,
svc.store,
types.Template{}.LabelResourceKind(),
filter.Labels,
)
if err != nil {
return err
}
}
if set, f, err = store.SearchTemplates(ctx, svc.store, filter); err != nil {
return err
}
if err = label.Load(ctx, svc.store, toLabeledTemplates(set)...); err != nil {
return err
}
return nil
}()
return set, f, svc.recordAction(ctx, aProps, TemplateActionSearch, err)
}
func (svc template) Create(ctx context.Context, new *types.Template) (tpl *types.Template, err error) {
var (
tplProps = &templateActionProps{new: new}
)
err = func() (err error) {
if !svc.ac.CanCreateTemplate(ctx) {
return TemplateErrNotAllowedToCreate()
}
// @todo corredor?
// Set new values after beforeCreate events are emitted
new.ID = nextID()
new.CreatedAt = *now()
if err = store.CreateTemplate(ctx, svc.store, new); err != nil {
return
}
if err = label.Create(ctx, svc.store, new); err != nil {
return
}
tpl = new
return nil
}()
return tpl, svc.recordAction(ctx, tplProps, TemplateActionCreate, err)
}
func (svc template) Update(ctx context.Context, upd *types.Template) (tpl *types.Template, err error) {
var (
tplProps = &templateActionProps{update: upd}
)
err = func() (err error) {
if upd.ID == 0 {
return TemplateErrInvalidID()
}
if tpl, err = store.LookupTemplateByID(ctx, svc.store, upd.ID); err != nil {
return
}
tplProps.setTemplate(tpl)
if !svc.ac.CanUpdateTemplate(ctx, tpl) {
return TemplateErrNotAllowedToUpdate()
}
// @todo corredor?
tpl.Handle = upd.Handle
tpl.Language = upd.Language
tpl.Type = upd.Type
tpl.Partial = upd.Partial
tpl.Meta = upd.Meta
tpl.Template = upd.Template
tpl.OwnerID = upd.OwnerID
tpl.UpdatedAt = now()
if err = store.UpdateTemplate(ctx, svc.store, tpl); err != nil {
return err
}
if label.Changed(tpl.Labels, upd.Labels) {
if err = label.Update(ctx, svc.store, upd); err != nil {
return
}
tpl.Labels = upd.Labels
}
return nil
}()
return tpl, svc.recordAction(ctx, tplProps, TemplateActionUpdate, err)
}
func (svc template) DeleteByID(ctx context.Context, ID uint64) (err error) {
var (
tplProps = &templateActionProps{}
tpl *types.Template
)
err = func() (err error) {
if ID == 0 {
return TemplateErrInvalidID()
}
if tpl, err = store.LookupTemplateByID(ctx, svc.store, ID); err != nil {
return
}
tplProps.setTemplate(tpl)
if !svc.ac.CanDeleteTemplate(ctx, tpl) {
return TemplateErrNotAllowedToDelete()
}
// @todo corredor?
tpl.DeletedAt = now()
if err = store.UpdateTemplate(ctx, svc.store, tpl); err != nil {
return
}
return nil
}()
return svc.recordAction(ctx, tplProps, TemplateActionDelete, err)
}
func (svc template) UndeleteByID(ctx context.Context, ID uint64) (err error) {
var (
tplProps = &templateActionProps{}
tpl *types.Template
)
err = func() (err error) {
if ID == 0 {
return TemplateErrInvalidID()
}
if tpl, err = store.LookupTemplateByID(ctx, svc.store, ID); err != nil {
return
}
tplProps.setTemplate(tpl)
if !svc.ac.CanDeleteTemplate(ctx, tpl) {
return TemplateErrNotAllowedToUndelete()
}
// @todo corredor?
tpl.DeletedAt = nil
if err = store.UpdateTemplate(ctx, svc.store, tpl); err != nil {
return
}
return nil
}()
return svc.recordAction(ctx, tplProps, TemplateActionUndelete, err)
}
func (svc template) Render(ctx context.Context, templateID uint64, dstType string, variables map[string]interface{}, options map[string]string) (document io.ReadSeeker, err error) {
var (
tplProps = &templateActionProps{}
tpl *types.Template
)
err = func() (err error) {
tpl, err = svc.FindByID(ctx, templateID)
if err != nil {
return err
}
if tpl == nil {
return TemplateErrNotFound()
}
if tpl.Partial {
return TemplateErrCannotRenderPartial()
}
tplProps.setTemplate(tpl)
if !svc.ac.CanRenderTemplate(ctx, tpl) {
return TemplateErrNotAllowedToRender()
}
// Prepare partials
//
// @todo Make this more sophisticated by inspecting the template or
// by requiring users to "import" (specify) what partials to use.
pp, err := svc.getPartials(ctx, tpl)
if err != nil {
return err
}
att, err := svc.getAttachments(ctx, tpl)
if err != nil {
return err
}
// Prepare payload
p := &renderer.RendererPayload{
Template: svc.getSource(tpl),
TemplateType: tpl.Type,
TargetType: types.DocumentType(dstType),
Variables: variables,
Options: options,
Partials: pp,
Attachments: att,
}
// Render the doc
document, err = svc.renderer.Render(ctx, p)
if err != nil {
return err
}
return nil
}()
return document, svc.recordAction(ctx, tplProps, TemplateActionRender, err)
}
// Util things
func (svc template) getSource(tpl *types.Template) io.Reader {
return bytes.NewBuffer([]byte(tpl.Template))
}
func (svc template) getPartials(ctx context.Context, tpl *types.Template) ([]*renderer.TemplatePartial, error) {
pp := make([]*renderer.TemplatePartial, 0, 20)
set, _, err := svc.Search(ctx, types.TemplateFilter{
Partial: true,
})
if err != nil {
return nil, err
}
// @todo inspect original template to filter partials
// @todo do some filtering based on partial type and main template type
for _, t := range set {
pp = append(pp, &renderer.TemplatePartial{
Handle: t.Handle,
Template: bytes.NewBuffer([]byte(t.Template)),
TemplateType: t.Type,
})
}
return pp, nil
}
// @todo...
func (svc template) getAttachments(ctx context.Context, tpl *types.Template) (renderer.AttachmentIndex, error) {
return make(renderer.AttachmentIndex), nil
// fpath := "..."
// att := make(renderer.AttachmentIndex)
// return att, filepath.Walk(fpath, func(fpath string, info os.FileInfo, err error) error {
// if err != nil {
// return err
// }
// if info.IsDir() {
// return nil
// }
// f, err := os.Open(fpath)
// if err != nil {
// return err
// }
// defer f.Close()
// bb := make([]byte, info.Size())
// _, err = f.Read(bb)
// if err != nil {
// return err
// }
// att[info.Name()] = &renderer.Attachment{
// Source: bytes.NewBuffer(bb),
// // @todo proper implementation!!!
// Mime: "image/png",
// Name: info.Name(),
// }
// return nil
// })
}
// toLabeledTemplates converts to []label.LabeledResource
func toLabeledTemplates(set []*types.Template) []label.LabeledResource {
if len(set) == 0 {
return nil
}
ll := make([]label.LabeledResource, len(set))
for i := range set {
ll[i] = set[i]
}
return ll
}

View File

@@ -0,0 +1,826 @@
package service
// 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:
// system/service/template_actions.yaml
import (
"context"
"fmt"
"github.com/cortezaproject/corteza-server/pkg/actionlog"
"github.com/cortezaproject/corteza-server/pkg/errors"
"github.com/cortezaproject/corteza-server/system/types"
"strings"
"time"
)
type (
templateActionProps struct {
template *types.Template
new *types.Template
update *types.Template
filter *types.TemplateFilter
}
templateAction struct {
timestamp time.Time
resource string
action string
log string
severity actionlog.Severity
// prefix for error when action fails
errorMessage string
props *templateActionProps
}
templateLogMetaKey struct{}
templatePropsMetaKey struct{}
)
var (
// just a placeholder to cover template cases w/o fmt package use
_ = fmt.Println
)
// *********************************************************************************************************************
// *********************************************************************************************************************
// Props methods
// setTemplate updates templateActionProps's template
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *templateActionProps) setTemplate(template *types.Template) *templateActionProps {
p.template = template
return p
}
// setNew updates templateActionProps's new
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *templateActionProps) setNew(new *types.Template) *templateActionProps {
p.new = new
return p
}
// setUpdate updates templateActionProps's update
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *templateActionProps) setUpdate(update *types.Template) *templateActionProps {
p.update = update
return p
}
// setFilter updates templateActionProps's filter
//
// Allows method chaining
//
// This function is auto-generated.
//
func (p *templateActionProps) setFilter(filter *types.TemplateFilter) *templateActionProps {
p.filter = filter
return p
}
// Serialize converts templateActionProps to actionlog.Meta
//
// This function is auto-generated.
//
func (p templateActionProps) Serialize() actionlog.Meta {
var (
m = make(actionlog.Meta)
)
if p.template != nil {
m.Set("template.handle", p.template.Handle, true)
m.Set("template.type", p.template.Type, true)
m.Set("template.ID", p.template.ID, true)
}
if p.new != nil {
m.Set("new.handle", p.new.Handle, true)
m.Set("new.type", p.new.Type, true)
m.Set("new.ID", p.new.ID, true)
}
if p.update != nil {
m.Set("update.handle", p.update.Handle, true)
m.Set("update.type", p.update.Type, true)
m.Set("update.ID", p.update.ID, true)
}
if p.filter != nil {
m.Set("filter.templateID", p.filter.TemplateID, true)
m.Set("filter.handle", p.filter.Handle, true)
m.Set("filter.type", p.filter.Type, true)
m.Set("filter.ownerID", p.filter.OwnerID, true)
m.Set("filter.deleted", p.filter.Deleted, true)
m.Set("filter.sort", p.filter.Sort, true)
}
return m
}
// tr translates string and replaces meta value placeholder with values
//
// This function is auto-generated.
//
func (p templateActionProps) Format(in string, err error) string {
var (
pairs = []string{"{err}"}
// first non-empty string
fns = func(ii ...interface{}) string {
for _, i := range ii {
if s := fmt.Sprintf("%v", i); len(s) > 0 {
return s
}
}
return ""
}
)
if err != nil {
pairs = append(pairs, err.Error())
} else {
pairs = append(pairs, "nil")
}
if p.template != nil {
// replacement for "{template}" (in order how fields are defined)
pairs = append(
pairs,
"{template}",
fns(
p.template.Handle,
p.template.Type,
p.template.ID,
),
)
pairs = append(pairs, "{template.handle}", fns(p.template.Handle))
pairs = append(pairs, "{template.type}", fns(p.template.Type))
pairs = append(pairs, "{template.ID}", fns(p.template.ID))
}
if p.new != nil {
// replacement for "{new}" (in order how fields are defined)
pairs = append(
pairs,
"{new}",
fns(
p.new.Handle,
p.new.Type,
p.new.ID,
),
)
pairs = append(pairs, "{new.handle}", fns(p.new.Handle))
pairs = append(pairs, "{new.type}", fns(p.new.Type))
pairs = append(pairs, "{new.ID}", fns(p.new.ID))
}
if p.update != nil {
// replacement for "{update}" (in order how fields are defined)
pairs = append(
pairs,
"{update}",
fns(
p.update.Handle,
p.update.Type,
p.update.ID,
),
)
pairs = append(pairs, "{update.handle}", fns(p.update.Handle))
pairs = append(pairs, "{update.type}", fns(p.update.Type))
pairs = append(pairs, "{update.ID}", fns(p.update.ID))
}
if p.filter != nil {
// replacement for "{filter}" (in order how fields are defined)
pairs = append(
pairs,
"{filter}",
fns(
p.filter.TemplateID,
p.filter.Handle,
p.filter.Type,
p.filter.OwnerID,
p.filter.Deleted,
p.filter.Sort,
),
)
pairs = append(pairs, "{filter.templateID}", fns(p.filter.TemplateID))
pairs = append(pairs, "{filter.handle}", fns(p.filter.Handle))
pairs = append(pairs, "{filter.type}", fns(p.filter.Type))
pairs = append(pairs, "{filter.ownerID}", fns(p.filter.OwnerID))
pairs = append(pairs, "{filter.deleted}", fns(p.filter.Deleted))
pairs = append(pairs, "{filter.sort}", fns(p.filter.Sort))
}
return strings.NewReplacer(pairs...).Replace(in)
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action methods
// String returns loggable description as string
//
// This function is auto-generated.
//
func (a *templateAction) String() string {
var props = &templateActionProps{}
if a.props != nil {
props = a.props
}
return props.Format(a.log, nil)
}
func (e *templateAction) ToAction() *actionlog.Action {
return &actionlog.Action{
Resource: e.resource,
Action: e.action,
Severity: e.severity,
Description: e.String(),
Meta: e.props.Serialize(),
}
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Action constructors
// TemplateActionSearch returns "system:template.search" action
//
// This function is auto-generated.
//
func TemplateActionSearch(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "search",
log: "searched for templates",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TemplateActionLookup returns "system:template.lookup" action
//
// This function is auto-generated.
//
func TemplateActionLookup(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "lookup",
log: "looked-up for a {template}",
severity: actionlog.Info,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TemplateActionCreate returns "system:template.create" action
//
// This function is auto-generated.
//
func TemplateActionCreate(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "create",
log: "created {template}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TemplateActionUpdate returns "system:template.update" action
//
// This function is auto-generated.
//
func TemplateActionUpdate(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "update",
log: "updated {template}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TemplateActionDelete returns "system:template.delete" action
//
// This function is auto-generated.
//
func TemplateActionDelete(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "delete",
log: "deleted {template}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TemplateActionUndelete returns "system:template.undelete" action
//
// This function is auto-generated.
//
func TemplateActionUndelete(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "undelete",
log: "undeleted {template}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// TemplateActionRender returns "system:template.render" action
//
// This function is auto-generated.
//
func TemplateActionRender(props ...*templateActionProps) *templateAction {
a := &templateAction{
timestamp: time.Now(),
resource: "system:template",
action: "render",
log: "rendered {template}",
severity: actionlog.Notice,
}
if len(props) > 0 {
a.props = props[0]
}
return a
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// Error constructors
// TemplateErrGeneric returns "system:template.generic" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrGeneric(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("failed to complete request due to internal error", nil),
errors.Meta("type", "generic"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "{err}"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotFound returns "system:template.notFound" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotFound(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("template not found", nil),
errors.Meta("type", "notFound"),
errors.Meta("resource", "system:template"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrInvalidID returns "system:template.invalidID" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrInvalidID(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid ID", nil),
errors.Meta("type", "invalidID"),
errors.Meta("resource", "system:template"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrInvalidHandle returns "system:template.invalidHandle" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrInvalidHandle(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("invalid handle", nil),
errors.Meta("type", "invalidHandle"),
errors.Meta("resource", "system:template"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrCannotRenderPartial returns "system:template.cannotRenderPartial" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrCannotRenderPartial(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("cannot render partial templates", nil),
errors.Meta("type", "cannotRenderPartial"),
errors.Meta("resource", "system:template"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToRead returns "system:template.notAllowedToRead" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToRead(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to read this template", nil),
errors.Meta("type", "notAllowedToRead"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to read {template.handle}; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToListTemplates returns "system:template.notAllowedToListTemplates" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToListTemplates(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to list templates", nil),
errors.Meta("type", "notAllowedToListTemplates"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to list template; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToCreate returns "system:template.notAllowedToCreate" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToCreate(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to create templates", nil),
errors.Meta("type", "notAllowedToCreate"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to create template; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToUpdate returns "system:template.notAllowedToUpdate" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToUpdate(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to update this template", nil),
errors.Meta("type", "notAllowedToUpdate"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to update {template.handle}; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToDelete returns "system:template.notAllowedToDelete" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToDelete(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to delete this template", nil),
errors.Meta("type", "notAllowedToDelete"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to delete {template.handle}; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToUndelete returns "system:template.notAllowedToUndelete" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToUndelete(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to undelete this template", nil),
errors.Meta("type", "notAllowedToUndelete"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to undelete {template.handle}; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// TemplateErrNotAllowedToRender returns "system:template.notAllowedToRender" as *errors.Error
//
//
// This function is auto-generated.
//
func TemplateErrNotAllowedToRender(mm ...*templateActionProps) *errors.Error {
var p = &templateActionProps{}
if len(mm) > 0 {
p = mm[0]
}
var e = errors.New(
errors.KindInternal,
p.Format("not allowed to render this template", nil),
errors.Meta("type", "notAllowedToRender"),
errors.Meta("resource", "system:template"),
// action log entry; no formatting, it will be applied inside recordAction fn.
errors.Meta(templateLogMetaKey{}, "failed to render {template.handle}; insufficient permissions"),
errors.Meta(templatePropsMetaKey{}, p),
errors.StackSkip(1),
)
if len(mm) > 0 {
}
return e
}
// *********************************************************************************************************************
// *********************************************************************************************************************
// recordAction is a service helper function wraps function that can return error
//
// It will wrap unrecognized/internal errors with generic errors.
//
// This function is auto-generated.
//
func (svc template) recordAction(ctx context.Context, props *templateActionProps, actionFn func(...*templateActionProps) *templateAction, err error) error {
if svc.actionlog == nil || actionFn == nil {
// action log disabled or no action fn passed, return error as-is
return err
} else if err == nil {
// action completed w/o error, record it
svc.actionlog.Record(ctx, actionFn(props).ToAction())
return nil
}
a := actionFn(props).ToAction()
// Extracting error information and recording it as action
a.Error = err.Error()
switch c := err.(type) {
case *errors.Error:
m := c.Meta()
a.Error = err.Error()
a.Severity = actionlog.Severity(m.AsInt("severity"))
a.Description = props.Format(m.AsString(templateLogMetaKey{}), err)
if p, has := m[templatePropsMetaKey{}]; has {
a.Meta = p.(*templateActionProps).Serialize()
}
svc.actionlog.Record(ctx, a)
default:
svc.actionlog.Record(ctx, a)
}
// Original error is passed on
return err
}

View File

@@ -0,0 +1,95 @@
# List of loggable service actions
resource: system:template
service: template
# Default sensitivity for actions
defaultActionSeverity: notice
# default severity for errors
defaultErrorSeverity: error
import:
- github.com/cortezaproject/corteza-server/system/types
props:
- name: template
type: "*types.Template"
fields: [ handle, type, ID ]
- name: new
type: "*types.Template"
fields: [ handle, type, ID ]
- name: update
type: "*types.Template"
fields: [ handle, type, ID ]
- name: filter
type: "*types.TemplateFilter"
fields: [ templateID, handle, type, ownerID, deleted, sort ]
actions:
- action: search
log: "searched for templates"
severity: info
- action: lookup
log: "looked-up for a {template}"
severity: info
- action: create
log: "created {template}"
- action: update
log: "updated {template}"
- action: delete
log: "deleted {template}"
- action: undelete
log: "undeleted {template}"
- action: render
log: "rendered {template}"
errors:
- error: notFound
message: "template not found"
severity: warning
- error: invalidID
message: "invalid ID"
severity: warning
- error: invalidHandle
message: "invalid handle"
severity: warning
- error: cannotRenderPartial
message: "cannot render partial templates"
- error: notAllowedToRead
message: "not allowed to read this template"
log: "failed to read {template.handle}; insufficient permissions"
- error: notAllowedToListTemplates
message: "not allowed to list templates"
log: "failed to list template; insufficient permissions"
- error: notAllowedToCreate
message: "not allowed to create templates"
log: "failed to create template; insufficient permissions"
- error: notAllowedToUpdate
message: "not allowed to update this template"
log: "failed to update {template.handle}; insufficient permissions"
- error: notAllowedToDelete
message: "not allowed to delete this template"
log: "failed to delete {template.handle}; insufficient permissions"
- error: notAllowedToUndelete
message: "not allowed to undelete this template"
log: "failed to undelete {template.handle}; insufficient permissions"
- error: notAllowedToRender
message: "not allowed to render this template"
log: "failed to render {template.handle}; insufficient permissions"

16
system/types/parsers.go Normal file
View File

@@ -0,0 +1,16 @@
package types
import "encoding/json"
func ParseTemplateMeta(ss []string) (p TemplateMeta, err error) {
p = TemplateMeta{}
return p, parseStringsInput(ss, p)
}
func parseStringsInput(ss []string, p interface{}) (err error) {
if len(ss) == 0 {
return
}
return json.Unmarshal([]byte(ss[0]), &p)
}

View File

@@ -6,6 +6,7 @@ import (
const SystemRBACResource = rbac.Resource("system")
const ApplicationRBACResource = rbac.Resource("system:application:")
const TemplateRBACResource = rbac.Resource("system:template:")
const OrganisationRBACResource = rbac.Resource("system:organisation:")
const UserRBACResource = rbac.Resource("system:user:")
const RoleRBACResource = rbac.Resource("system:role:")

103
system/types/template.go Normal file
View File

@@ -0,0 +1,103 @@
package types
import (
"database/sql/driver"
"encoding/json"
"time"
"github.com/cortezaproject/corteza-server/pkg/filter"
"github.com/cortezaproject/corteza-server/pkg/rbac"
"github.com/pkg/errors"
)
type (
DocumentType string
Template struct {
ID uint64 `json:"templateID,string"`
Handle string `json:"handle"`
// Language specifies the language the template is written for; leave empty for default
Language string `json:"language"`
Type DocumentType `json:"type"`
// Partial templates can be used to construct larger templates; for example headers and footers
Partial bool `json:"partial"`
// use int so JS can handle it normally
//
// @todo We'll handle this at a later point
// Revision int `json:"revision,string"`
Meta TemplateMeta `json:"meta"`
Template string `json:"template"`
Labels map[string]string `json:"labels,omitempty"`
OwnerID uint64 `json:"ownerID,string"`
CreatedAt time.Time `json:"createdAt,omitempty"`
UpdatedAt *time.Time `json:"updatedAt,omitempty"`
DeletedAt *time.Time `json:"deletedAt,omitempty"`
LastUsedAt *time.Time `json:"lastUsedAt,omitempty"`
}
TemplateMeta struct {
Short string `json:"short"`
Description string `json:"description,omitempty"`
}
TemplateFilter struct {
TemplateID []uint64 `json:"templateID"`
Handle string `json:"handle"`
Type string `json:"type"`
OwnerID uint64 `json:"ownerID,string"`
Partial bool `json:"partial"`
LabeledIDs []uint64 `json:"-"`
Labels map[string]string `json:"labels,omitempty"`
// Check fn is called by store backend for each resource found function can
// modify the resource and return false if store should not return it
//
// Store then loads additional resources to satisfy the paging parameters
Check func(*Template) (bool, error) `json:"-"`
Deleted filter.State `json:"deleted"`
// Standard helpers for paging and sorting
filter.Sorting
filter.Paging
}
)
const (
DocumentTypePlain DocumentType = "text/plain"
DocumentTypeHTML = "text/html"
DocumentTypePDF = "application/pdf"
)
func (t *TemplateMeta) Scan(value interface{}) error {
//lint:ignore S1034 This typecast is intentional, we need to get []byte out of a []uint8
switch value.(type) {
case nil:
*t = TemplateMeta{}
case []uint8:
b := value.([]byte)
if err := json.Unmarshal(b, t); err != nil {
return errors.Wrapf(err, "Can not scan '%v' into TemplateMeta", string(b))
}
}
return nil
}
func (t TemplateMeta) Value() (driver.Value, error) {
return json.Marshal(t)
}
func (t Template) Clone() *Template {
c := &t
return c
}
func (r Template) RBACResource() rbac.Resource {
return TemplateRBACResource.AppendID(r.ID)
}

View File

@@ -56,6 +56,30 @@ func (m Role) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *Template) 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 Template) GetLabels() map[string]string {
return m.Labels
}
// GetLabels adds new label to label map
func (Template) LabelResourceKind() string {
return "template"
}
// GetLabels adds new label to label map
func (m Template) LabelResourceID() uint64 {
return m.ID
}
// SetLabel adds new label to label map
func (m *User) SetLabel(key string, value string) {
if m.Labels == nil {

View File

@@ -45,6 +45,11 @@ type (
// This type is auto-generated.
SettingValueSet []*SettingValue
// TemplateSet slice of Template
//
// This type is auto-generated.
TemplateSet []*Template
// UserSet slice of User
//
// This type is auto-generated.
@@ -391,6 +396,62 @@ func (set SettingValueSet) Filter(f func(*SettingValue) (bool, error)) (out Sett
return
}
// Walk iterates through every slice item and calls w(Template) err
//
// This function is auto-generated.
func (set TemplateSet) Walk(w func(*Template) error) (err error) {
for i := range set {
if err = w(set[i]); err != nil {
return
}
}
return
}
// Filter iterates through every slice item, calls f(Template) (bool, err) and return filtered slice
//
// This function is auto-generated.
func (set TemplateSet) Filter(f func(*Template) (bool, error)) (out TemplateSet, err error) {
var ok bool
out = TemplateSet{}
for i := range set {
if ok, err = f(set[i]); err != nil {
return
} else if ok {
out = append(out, set[i])
}
}
return
}
// FindByID finds items from slice by its ID property
//
// This function is auto-generated.
func (set TemplateSet) FindByID(ID uint64) *Template {
for i := range set {
if set[i].ID == ID {
return set[i]
}
}
return nil
}
// IDs returns a slice of uint64s from all items in the set
//
// This function is auto-generated.
func (set TemplateSet) IDs() (IDs []uint64) {
IDs = make([]uint64, len(set))
for i := range set {
IDs[i] = set[i].ID
}
return
}
// Walk iterates through every slice item and calls w(User) err
//
// This function is auto-generated.

View File

@@ -576,6 +576,96 @@ func TestSettingValueSetFilter(t *testing.T) {
}
}
func TestTemplateSetWalk(t *testing.T) {
var (
value = make(TemplateSet, 3)
req = require.New(t)
)
// check walk with no errors
{
err := value.Walk(func(*Template) error {
return nil
})
req.NoError(err)
}
// check walk with error
req.Error(value.Walk(func(*Template) error { return fmt.Errorf("walk error") }))
}
func TestTemplateSetFilter(t *testing.T) {
var (
value = make(TemplateSet, 3)
req = require.New(t)
)
// filter nothing
{
set, err := value.Filter(func(*Template) (bool, error) {
return true, nil
})
req.NoError(err)
req.Equal(len(set), len(value))
}
// filter one item
{
found := false
set, err := value.Filter(func(*Template) (bool, error) {
if !found {
found = true
return found, nil
}
return false, nil
})
req.NoError(err)
req.Len(set, 1)
}
// filter error
{
_, err := value.Filter(func(*Template) (bool, error) {
return false, fmt.Errorf("filter error")
})
req.Error(err)
}
}
func TestTemplateSetIDs(t *testing.T) {
var (
value = make(TemplateSet, 3)
req = require.New(t)
)
// construct objects
value[0] = new(Template)
value[1] = new(Template)
value[2] = new(Template)
// set ids
value[0].ID = 1
value[1].ID = 2
value[2].ID = 3
// Find existing
{
val := value.FindByID(2)
req.Equal(uint64(2), val.ID)
}
// Find non-existing
{
val := value.FindByID(4)
req.Nil(val)
}
// List IDs from set
{
val := value.IDs()
req.Equal(len(val), len(value))
}
}
func TestUserSetWalk(t *testing.T) {
var (
value = make(UserSet, 3)

View File

@@ -12,3 +12,5 @@ types:
Attachment: {}
SettingValue:
noIdField: true
Template:
labelResourceType: template

View File

@@ -3,9 +3,10 @@ package helpers
import (
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"strings"
"github.com/davecgh/go-spew/spew"
"github.com/pkg/errors"
"github.com/cortezaproject/corteza-server/compose/types"
@@ -108,13 +109,10 @@ func AssertRecordValueError(exp ...*types.RecordValueError) assertFn {
// Dump can be put into Assert()
func Dump(rsp *http.Response, _ *http.Request) (err error) {
spew.Dump(rsp.Status)
spew.Dump(rsp.Header)
var payload interface{}
if err = DecodeBody(rsp, &payload); err != nil {
return err
}
spew.Dump(payload)
return nil
}
@@ -137,3 +135,19 @@ func AssertError(expectedError string) assertFn {
return nil
}
}
// AssertBody compares the raw body to the provided string
func AssertBody(expected string) assertFn {
return func(rsp *http.Response, _ *http.Request) (err error) {
bb, err := ioutil.ReadAll(rsp.Body)
if err != nil {
return err
}
got := strings.Trim(string(bb), " \n")
if expected != got {
return errors.Errorf("Expecting: %v, got: %v", expected, got)
}
return nil
}
}

View File

@@ -3,6 +3,9 @@ package system
import (
"context"
"errors"
"os"
"testing"
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/pkg/api/server"
"github.com/cortezaproject/corteza-server/pkg/auth"
@@ -27,8 +30,6 @@ import (
"github.com/steinfletcher/apitest"
"github.com/stretchr/testify/require"
"go.uber.org/zap"
"os"
"testing"
)
type (
@@ -177,3 +178,7 @@ func (h helper) setLabel(res label.LabeledResource, name, value string) {
Value: value,
}))
}
func (h helper) clearTemplates() {
h.noError(store.TruncateTemplates(context.Background(), service.DefaultStore))
}

View File

@@ -0,0 +1,266 @@
package system
import (
"context"
"fmt"
"net/http"
"testing"
"time"
"github.com/cortezaproject/corteza-server/pkg/id"
"github.com/cortezaproject/corteza-server/store"
"github.com/cortezaproject/corteza-server/system/service"
"github.com/cortezaproject/corteza-server/system/types"
"github.com/cortezaproject/corteza-server/tests/helpers"
jsonpath "github.com/steinfletcher/apitest-jsonpath"
)
func (h helper) repoMakeTemplate(ss ...string) *types.Template {
var res = &types.Template{
ID: id.Next(),
CreatedAt: time.Now(),
}
if len(ss) > 0 {
res.Handle = ss[0]
} else {
res.Handle = "n_" + rs()
}
if len(ss) > 1 {
res.Template = ss[1]
}
if len(ss) > 2 {
res.Type = types.DocumentType(ss[2])
}
h.a.NoError(store.CreateTemplate(context.Background(), service.DefaultStore, res))
return res
}
func (h helper) lookupTemplateByID(ID uint64) *types.Template {
res, err := store.LookupTemplateByID(context.Background(), service.DefaultStore, ID)
h.noError(err)
return res
}
func TestTemplateRead(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
u := h.repoMakeTemplate()
h.apiInit().
Get(fmt.Sprintf("/template/%d", u.ID)).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Equal(`$.response.handle`, u.Handle)).
Assert(jsonpath.Equal(`$.response.templateID`, fmt.Sprintf("%d", u.ID))).
End()
}
func TestTemplateList(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.repoMakeTemplate(rs())
h.repoMakeTemplate(rs())
h.apiInit().
Get("/template/").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.Len(`$.response.set`, 2)).
End()
}
func TestTemplateList_filterForbidden(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
// @todo this can be a problematic test because it leaves
// behind templates that are not denied this context
// db purge might be needed
h.repoMakeTemplate("template")
f := h.repoMakeTemplate()
h.deny(types.TemplateRBACResource.AppendID(f.ID), "read")
h.apiInit().
Get("/template/").
Query("handle", f.Handle).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
Assert(jsonpath.NotPresent(fmt.Sprintf(`$.response.set[? @.handle=="%s"]`, f.Handle))).
End()
}
func TestTemplateCreateForbidden(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.apiInit().
Post("/template/").
Header("Accept", "application/json").
FormData("handle", rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to create templates")).
End()
}
func TestTemplateCreate(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.allow(types.SystemRBACResource, "template.create")
h.apiInit().
Post("/template/").
FormData("handle", rs()).
FormData("handle", "handle_"+rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
}
func TestTemplateUpdateForbidden(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
u := h.repoMakeTemplate()
h.apiInit().
Put(fmt.Sprintf("/template/%d", u.ID)).
Header("Accept", "application/json").
FormData("handle", rs()).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to update this template")).
End()
}
func TestTemplateUpdate(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
res := h.repoMakeTemplate()
h.allow(types.TemplateRBACResource.AppendWildcard(), "update")
newHandle := "updated-" + rs()
h.apiInit().
Put(fmt.Sprintf("/template/%d", res.ID)).
Header("Accept", "application/json").
FormData("handle", newHandle).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupTemplateByID(res.ID)
h.a.NotNil(res)
h.a.Equal(newHandle, res.Handle)
}
func TestTemplateDeleteForbidden(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
u := h.repoMakeTemplate()
h.apiInit().
Delete(fmt.Sprintf("/template/%d", u.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to delete this template")).
End()
}
func TestTemplateDelete(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.allow(types.TemplateRBACResource.AppendWildcard(), "delete")
res := h.repoMakeTemplate()
h.apiInit().
Delete(fmt.Sprintf("/template/%d", res.ID)).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertNoErrors).
End()
res = h.lookupTemplateByID(res.ID)
h.a.NotNil(res)
h.a.NotNil(res.DeletedAt)
}
func TestTemplateRenderForbiden(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.deny(types.TemplateRBACResource.AppendWildcard(), "render")
res := h.repoMakeTemplate("rendering", "Hello, {{.interpolate}}", "text/plain")
h.apiInit().
Post(fmt.Sprintf("/template/%d/render/testing.txt", res.ID)).
JSON(`{"variables": {"interpolate": "world!"}}`).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("not allowed to render this template")).
End()
}
func TestTemplateRenderDriverUndefined(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.allow(types.TemplateRBACResource.AppendWildcard(), "render")
res := h.repoMakeTemplate("rendering", "Hello, {{.interpolate}}", "text/notexisting")
h.apiInit().
Post(fmt.Sprintf("/template/%d/render/testing.txt", res.ID)).
JSON(`{"variables": {"interpolate": "world!"}}`).
Header("Accept", "application/json").
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertError("rendering failed: driver not found")).
End()
}
func TestTemplateRenderPlain(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.allow(types.TemplateRBACResource.AppendWildcard(), "render")
res := h.repoMakeTemplate("rendering", "Hello, {{.interpolate}}", "text/plain")
h.apiInit().
Post(fmt.Sprintf("/template/%d/render/testing.txt", res.ID)).
JSON(`{"variables": {"interpolate": "world!"}}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertBody("Hello, world!")).
End()
}
func TestTemplateRenderHTML(t *testing.T) {
h := newHelper(t)
h.clearTemplates()
h.allow(types.TemplateRBACResource.AppendWildcard(), "render")
res := h.repoMakeTemplate("rendering", "<h1>Hello, {{.interpolate}}</h1>", "text/html")
h.apiInit().
Post(fmt.Sprintf("/template/%d/render/testing.html", res.ID)).
JSON(`{"variables": {"interpolate": "world!"}}`).
Expect(t).
Status(http.StatusOK).
Assert(helpers.AssertBody("<h1>Hello, world!</h1>")).
End()
}