snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
547
server/system/service/template.go
Normal file
547
server/system/service/template.go
Normal file
@@ -0,0 +1,547 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/cortezaproject/corteza/server/pkg/errors"
|
||||
|
||||
"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 {
|
||||
CanCreateTemplate(context.Context) bool
|
||||
CanSearchTemplates(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)
|
||||
Drivers() []renderer.DriverDefinition
|
||||
}
|
||||
|
||||
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
|
||||
|
||||
Drivers() []renderer.DriverDefinition
|
||||
Render(ctx context.Context, templateID uint64, dstType string, variables map[string]interface{}, options map[string]string, aux types.TemplateRenderAux) (io.ReadSeeker, error)
|
||||
}
|
||||
)
|
||||
|
||||
func Renderer(cfg options.TemplateOpt) *template {
|
||||
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 tpl, err = loadTemplate(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 TemplateErrNotFound().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 !svc.ac.CanSearchTemplates(ctx) {
|
||||
return TemplateErrNotAllowedToSearch()
|
||||
}
|
||||
|
||||
if len(filter.Labels) > 0 {
|
||||
filter.LabeledIDs, err = label.Search(
|
||||
ctx,
|
||||
svc.store,
|
||||
types.Template{}.LabelResourceKind(),
|
||||
filter.Labels,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// labels specified but no labeled resources found
|
||||
if len(filter.LabeledIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
if set, f, err = store.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 !handle.IsValid(new.Handle) {
|
||||
return TemplateErrInvalidHandle()
|
||||
}
|
||||
|
||||
if new.Meta.Short == "" {
|
||||
return TemplateErrMissingShort()
|
||||
}
|
||||
|
||||
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 !handle.IsValid(upd.Handle) {
|
||||
return TemplateErrInvalidHandle()
|
||||
}
|
||||
|
||||
if upd.Meta.Short == "" {
|
||||
return TemplateErrMissingShort()
|
||||
}
|
||||
|
||||
if tpl, err = loadTemplate(ctx, svc.store, upd.ID); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
tplProps.setTemplate(tpl)
|
||||
|
||||
if !svc.ac.CanUpdateTemplate(ctx, tpl) {
|
||||
return TemplateErrNotAllowedToUpdate()
|
||||
}
|
||||
|
||||
// Test if stale (update has an older version of data)
|
||||
if isStale(upd.UpdatedAt, tpl.UpdatedAt, tpl.CreatedAt) {
|
||||
return TemplateErrStaleData()
|
||||
}
|
||||
|
||||
// @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 tpl, err = loadTemplate(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 tpl, err = loadTemplate(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) Drivers() []renderer.DriverDefinition {
|
||||
return svc.renderer.Drivers()
|
||||
}
|
||||
|
||||
func (svc template) Render(ctx context.Context, templateID uint64, dstType string, variables map[string]interface{}, options map[string]string, aux types.TemplateRenderAux) (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
|
||||
}
|
||||
|
||||
// Optional header/footer templates, used by drivers that support them
|
||||
header, err := svc.getAuxTemplateSource(ctx, firstTemplateID(aux.HeaderTemplateID, tpl.Meta.HeaderTemplateID))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
footer, err := svc.getAuxTemplateSource(ctx, firstTemplateID(aux.FooterTemplateID, tpl.Meta.FooterTemplateID))
|
||||
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,
|
||||
Header: header,
|
||||
Footer: footer,
|
||||
}
|
||||
|
||||
// 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 {
|
||||
tpl := t.Template
|
||||
if !strings.HasPrefix(tpl, "{{define") {
|
||||
tpl = fmt.Sprintf(`{{define "%s"}}%s{{end}}`, t.Handle, tpl)
|
||||
}
|
||||
|
||||
pp = append(pp, &renderer.TemplatePartial{
|
||||
Handle: t.Handle,
|
||||
Template: bytes.NewBuffer([]byte(tpl)),
|
||||
TemplateType: t.Type,
|
||||
})
|
||||
}
|
||||
|
||||
return pp, nil
|
||||
}
|
||||
|
||||
// getAuxTemplateSource loads raw source of the referenced header/footer
|
||||
// template; (nil, nil) when ID is unset
|
||||
// firstTemplateID returns the first non-zero template ID
|
||||
func firstTemplateID(IDs ...uint64) uint64 {
|
||||
for _, ID := range IDs {
|
||||
if ID > 0 {
|
||||
return ID
|
||||
}
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (svc template) getAuxTemplateSource(ctx context.Context, ID uint64) (io.Reader, error) {
|
||||
if ID == 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
tpl, err := svc.FindByID(ctx, ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return svc.getSource(tpl), 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
|
||||
// })
|
||||
}
|
||||
|
||||
func loadTemplate(ctx context.Context, s store.Templates, ID uint64) (res *types.Template, err error) {
|
||||
if ID == 0 {
|
||||
return nil, TemplateErrInvalidID()
|
||||
}
|
||||
|
||||
if res, err = store.LookupTemplateByID(ctx, s, ID); errors.IsNotFound(err) {
|
||||
return nil, TemplateErrNotFound()
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user