Define base templating system
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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(),
|
||||
|
||||
37
pkg/options/template.gen.go
Normal file
37
pkg/options/template.gen.go
Normal 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
11
pkg/options/template.yaml
Normal 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.
|
||||
@@ -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
|
||||
}
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
634
store/rdbms/templates.gen.go
Normal file
634
store/rdbms/templates.gen.go
Normal 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
36
store/rdbms/templates.go
Normal 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
85
store/templates.gen.go
Normal 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
34
store/templates.yaml
Normal 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
|
||||
@@ -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)
|
||||
|
||||
156
store/tests/templates_test.go
Normal file
156
store/tests/templates_test.go
Normal 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
|
||||
})
|
||||
}
|
||||
42
system/renderer/genericHTML.go
Normal file
42
system/renderer/genericHTML.go
Normal 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
|
||||
}
|
||||
47
system/renderer/genericTXT.go
Normal file
47
system/renderer/genericTXT.go
Normal 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
|
||||
}
|
||||
291
system/renderer/gotenbergPDF.go
Normal file
291
system/renderer/gotenbergPDF.go
Normal 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 "", ""
|
||||
}
|
||||
49
system/renderer/renderer.go
Normal file
49
system/renderer/renderer.go
Normal 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
50
system/renderer/types.go
Normal 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
107
system/renderer/util.go
Normal 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
|
||||
}
|
||||
172
system/rest.yaml
172
system/rest.yaml
@@ -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"
|
||||
|
||||
171
system/rest/handlers/template.go
Normal file
171
system/rest/handlers/template.go
Normal 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)
|
||||
})
|
||||
}
|
||||
917
system/rest/request/template.go
Normal file
917
system/rest/request/template.go
Normal 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
|
||||
}
|
||||
@@ -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
216
system/rest/template.go
Normal 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"
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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
467
system/service/template.go
Normal 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
|
||||
}
|
||||
826
system/service/template_actions.gen.go
Normal file
826
system/service/template_actions.gen.go
Normal 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
|
||||
}
|
||||
95
system/service/template_actions.yaml
Normal file
95
system/service/template_actions.yaml
Normal 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
16
system/types/parsers.go
Normal 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)
|
||||
}
|
||||
@@ -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
103
system/types/template.go
Normal 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)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -12,3 +12,5 @@ types:
|
||||
Attachment: {}
|
||||
SettingValue:
|
||||
noIdField: true
|
||||
Template:
|
||||
labelResourceType: template
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
266
tests/system/template_test.go
Normal file
266
tests/system/template_test.go
Normal 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()
|
||||
}
|
||||
Reference in New Issue
Block a user