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

This commit is contained in:
upstream-mirror
2026-09-18 01:22:23 +05:00
commit 7e385fc126
8391 changed files with 1766236 additions and 0 deletions

2
server/codegen/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/build
tmp*.cue

26
server/codegen/Makefile Normal file
View File

@@ -0,0 +1,26 @@
.PHONY: *.cue
DEF_ALL = *.cue
DEF_SERVER = server.*.cue
DEF_DOCS = docs.*.cue
server: $(DEF_SERVER)
docs: $(DEF_DOCS)
all: $(DEF_SERVER) $(DEF_DOCS)
include ../Makefile.inc
$(DEF_SERVER): $(CUE) $(JSONTPLEXEC)
@ echo "$(COLOUR_GREEN)Generating server files from $@$(COLOUR_END)"
@ $(CUE) eval $@ --out json | $(JSONTPLEXEC) -v -p $(ROOT_DIR)/codegen/assets/templates -b $(ROOT_DIR)
ifeq ("${DOCS_DIR}", "")
$(DEF_DOCS):
@ echo "$(COLOUR_YELLOW)Skipping docs generation: DOCS_DIR is not set.$(COLOUR_END)"
@ echo "$Run task like this: make "$@" DOCS_DIR=/path/to/corteza-docs"
@ echo ""
else
$(DEF_DOCS): $(CUE) $(JSONTPLEXEC)
@ echo "$(COLOUR_GREEN)Generating doc files from $@$(COLOUR_END) $(COLOUR_BLUE)(dst: $(DOCS_DIR))$(COLOUR_END)"
@ $(CUE) eval $@ --out json | $(JSONTPLEXEC) -v -p $(ROOT_DIR)/codegen/assets/templates -b $(DOCS_DIR)
endif

View File

@@ -0,0 +1,38 @@
###############################################################################
###############################################################################
#
# This file is auto-generated.
#
# Copy it to .env and uncomment set or adjust any options you need.
#
# There are three ways how this options are loaded when Corteza starts:
# 1. Environmental variables are always loaded and can not be overrided
# by alternative sources (below) (*)
# 2. If one or more --env-file flag is provided corteza looks in the provided
# locations (either file or directory containing .env)
# 3. Directory where the corteza binary is located is scanned for .env file)
#
# (*) You can use mechanisms from docker or docker-compose to set or load
# environmental variables as well
{{- range .groups }}
###############################################################################
###############################################################################
{{ .title }}
#
{{- if .intro }}
{{ .intro }}
#
{{ end -}}
{{ range .options }}
###############################################################################
{{- if .description }}
{{ .description }}
{{- end }}
# Type: {{ .type }}
# Default: {{ .defaultValue }}
# {{ .env }}={{ .defaultValue }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,39 @@
//
// This file is auto-generated.
//
include::ROOT:partial$variables.adoc[]
{{- range .groups }}
= {{ .title }}
{{- if .intro }}
{{ .intro }}
#
{{ end -}}
{{ range .options }}
== *{{ .env }}*
=== Type
`{{ .type }}`
{{ if .defaultValue }}
=== Default
[source]
----
{{ .defaultValue }}
----
{{- end }}
{{- if .description }}
=== Description
{{ .description }}
{{- end }}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,13 @@
= {{ .label }}
[cols="1s,5a,5a"]
|===
| Operation| Description | Default
{{ range .operations }}
| [#{{ .slug }}]#<<{{ .slug }},{{ .label }}>>#
| {{ .description }}
| Deny
{{ end }}
|===

View File

@@ -0,0 +1,13 @@
= {{ .label }}
[cols="1s,5a,5a"]
|===
| Operation| Description | Default
{{ range .operations }}
| [#{{ .slug }}]#<<{{ .slug }},{{ .label }}>>#
| {{ .description }}
| Deny
{{ end }}
|===

View File

@@ -0,0 +1,11 @@
= {{ .label }}
:leveloffset: +1
include::./component.gen.adoc[]
{{ range .resources }}
include::./resource.{{ . }}.gen.adoc[]
{{ end }}
:leveloffset: -1

View File

@@ -0,0 +1,26 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
"github.com/cortezaproject/corteza/server/pkg/dal"
{{- range .imports }}
{{ . }}
{{- end }}
)
type (
modelReplacer interface {
ReplaceModel(ctx context.Context, model *dal.Model) (err error)
}
)
var (
models []*dal.Model
)
func Models() dal.ModelSet {
return models
}

View File

@@ -0,0 +1,90 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"github.com/cortezaproject/corteza/server/pkg/dal"
{{- range .imports }}
{{ . }}
{{- end }}
)
{{ range .models }}
var {{ .var }} = &dal.Model{
Ident: {{ printf "%q" .ident }},
ResourceType: {{ .resType }},
Attributes: dal.AttributeSet{
{{- range .attributes }}
&dal.Attribute{
Ident: {{ printf "%q" .expIdent }},
{{- if .primaryKey }}PrimaryKey: true, {{ end }}
{{- if .sortable }}Sortable: true, {{ end }}
{{- if .filterable }}Filterable: true, {{ end }}
Type: &{{ .dal.fqType }}{
{{- if .dal.nullable }}Nullable: true,{{ end }}
{{- if .dal.hasDefault }}HasDefault: true, {{ end }}
{{- if .dal.quotedDefault }}
DefaultValue: {{ printf "%q" .dal.quotedDefault }},
{{- else if .dal.defaultEmptyObject }}
DefaultValue: "{}",
{{- else if .dal.defaultCurrentTimestamp }}
DefaultCurrentTimestamp: true,
{{- else if .dal.hasDefault }}
DefaultValue: {{.dal.default }},
{{ end -}}
{{- if .dal.timezone }} Timezone: {{ .dal.timezone }}, {{ end }}
{{- if .dal.timestampPrecision }} TimezonePrecision: true, {{ end }}
{{- if .dal.precision }} Precision: {{ .dal.precision }}, {{ end }}
{{- if .dal.scale }} Scale: {{ .dal.scale }}, {{ end }}
{{- if .dal.length }} Length: {{ .dal.length }}, {{ end }}
{{- if .dal.meta }} Meta: {{ printf "%#v" .dal.meta }}, {{ end }}
{{- if eq .dal.type "Ref" }}
RefAttribute: {{ printf "%q" .dal.attribute }},
RefModel: &dal.ModelRef{
ResourceType: {{ printf "%q" .dal.refModelResType }},
},
{{- end }}
},
Store: &dal.CodecAlias{Ident: {{ printf "%q" .storeIdent }}},
},
{{ end -}}
},
Indexes: dal.IndexSet{
{{- range .indexes }}
&dal.Index{
Ident: {{ printf "%q" .ident }},
Type: {{ printf "%q" .type }},
{{ if .unique }}Unique: true,{{ end }}
{{ if .predicate }}Predicate: {{ printf "%q" .predicate }},{{ end }}
Fields: []*dal.IndexField{
{{- range .fields }}
{
AttributeIdent: {{ printf "%q" .attribute }},
{{- if .modifiers }}
Modifiers: []dal.IndexFieldModifier{ {{- range .modifiers }}{{ printf "%q" . }},{{- end }} },
{{- end }}
{{- if eq .sort "ASC" }}Sort: dal.IndexFieldSortAsc, {{ end }}
{{- if eq .sort "DESC" }}Sort: dal.IndexFieldSortDesc, {{ end }}
{{- if eq .nulls "FIRST" }}Nulls: dal.IndexFieldNullsFirst,{{ end }}
{{- if eq .nulls "LAST" }}Nulls: dal.IndexFieldNullsLast, {{ end }}
},
{{ end -}}
},
},
{{ end -}}
},
}
{{ end }}
func init () {
models = append(
models,
{{- range .models }}
{{ .var }},
{{- end }}
)
}

View File

@@ -0,0 +1,32 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
{{- range .imports }}
{{ . }}
{{- end }}
)
{{- range .resources }}
// {{ .rbacRefFunc }} generates RBAC references
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func {{ .rbacRefFunc }}({{- range .references }}{{ .param }} string, {{- end }}) (res *Ref, pp []*Ref, err error) {
{{- range .references }}
{{- if eq .refField "ID" }}
if {{ .param }} != "*" {
res = &Ref{ResourceType: types.{{ .expIdent }}ResourceType, Identifiers: MakeIdentifiers({{ .param }})}
}
{{- else }}
if {{ .param }} != "*" {
pp = append(pp, &Ref{ResourceType: types.{{ .expIdent }}ResourceType, Identifiers: MakeIdentifiers({{ .param }})})
}
{{- end }}
{{- end }}
return
}
{{- end }}

View File

@@ -0,0 +1,74 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"fmt"
"strings"
{{- range .imports }}
{{ . }}
{{- end }}
)
// Parse generates resource setting logic for each resource
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ParseRule(res string) (string, *Ref, []*Ref, error) {
if res == "" {
return "", nil, nil, fmt.Errorf("empty resource")
}
sp := "/"
res = strings.TrimSpace(res)
res = strings.TrimRight(res, sp)
rr := strings.Split(res, sp)
// only service defined (corteza::system, corteza::compose, ...)
if len(rr) == 1 {
return res, nil, nil, nil
}
// full thing
resourceType, path := rr[0], rr[1:]
for p := 1; p < len(path); p++ {
if path[p] != "*" && path[p-1] == "*" {
return "", nil, nil, fmt.Errorf("invalid path wildcard combination for '%s'", res)
}
}
// @todo this is to support res. tr. resources also.
// Split it into a separate function and remove this.
if !strings.HasPrefix(resourceType, "corteza::") {
resourceType = "corteza::" + resourceType
}
// make the resource provide the slice of parent resources we should nest under
switch resourceType {
{{- range .resources }}
case {{ .typeConst }}:
if len(path) != {{ len .references }} {
return "", nil, nil, fmt.Errorf("expecting {{ len .references }} reference components in path, got %d", len(path))
}
{{- if gt (len .references) 0 }}
ref, pp, err := {{ .rbacRefFunc }}(
{{- range $i, $r := .references }}
path[{{ $i }}],
{{- end }}
)
return resourceType, ref, pp, err
{{ else }}
// Component resource, no path
return resourceType, nil, nil, nil
{{- end }}
{{- end}}
}
// return unhandled resource as-is
return resourceType, nil, nil, nil
}

View File

@@ -0,0 +1,25 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
systemTypes "github.com/cortezaproject/corteza/server/system/types"
)
{{- range .resources }}
func (r *{{ .expIdent }}) EncodeTranslations() ([]*ResourceTranslation, error) {
out := make([]*ResourceTranslation, 0, 10)
rr := r.Res.EncodeTranslations()
rr.SetLanguage(defaultLanguage)
res, ref, pp := r.ResourceTranslationParts()
out = append(out, NewResourceTranslation(systemTypes.FromLocale(rr), res, ref, pp...))
{{ if .extended }}
tmp, err := r.encodeTranslations()
return append(out, tmp...), err
{{ else }}
return out, nil
{{- end }}
}
{{- end }}

View File

@@ -0,0 +1,69 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"fmt"
"strings"
{{- range .imports }}
{{ . }}
{{- end }}
)
// ParseResourceTranslation generates resource setting logic for each resource
//
// Resources with "envoy: false" are skipped
//
// This function is auto-generated
func ParseResourceTranslation(res string) (string, *Ref, []*Ref, error) {
if res == "" {
return "", nil, nil, fmt.Errorf("empty resource")
}
sp := "/"
if strings.Index(res, "corteza::") == 0 {
res = res[9:]
}
res = strings.TrimSpace(res)
res = strings.TrimRight(res, sp)
rr := strings.Split(res, sp)
// only service defined (corteza::system, corteza::compose, ...)
if len(rr) == 1 {
return "", nil, nil, fmt.Errorf("only service defined: %s", res)
}
// full thing
resourceType, path := rr[0], rr[1:]
for p := 1; p < len(path); p++ {
if path[p] == "*" {
return "", nil, nil, fmt.Errorf("path wildcard not allowed for locale resources: '%s'", res)
}
}
// make the resource provide the slice of parent resources we should nest under
switch resourceType {
{{- range .resources }}
case {{ .typeConst }}:
if len(path) != {{ len .references }} {
return "", nil, nil, fmt.Errorf("expecting {{ len .references }} reference components in path, got %d", len(path))
}
ref, pp, err := {{ .resTrRefFunc }}(
{{- range $i, $r := .references }}
path[{{ $i }}],
{{- end }}
)
return {{ .typeConst }}, ref, pp, err
{{ else }}
// Component resource, no path
return {{ .typeConst }}, nil, nil, nil
{{- end }}
}
// return unhandled resource as-is
return resourceType, nil, nil, nil
}

View File

@@ -0,0 +1,24 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
{{- range .imports }}
{{ . }}
{{- end }}
)
{{- range .resources }}
// {{ .resTrRefFunc }} generates Locale references
//
// This function is auto-generated
func {{ .resTrRefFunc }}({{- range .references }}{{ .param }} string, {{- end }} self string) (res *Ref, pp []*Ref, err error) {
res = &Ref{ResourceType: types.{{ .expIdent }}ResourceType, Identifiers: MakeIdentifiers(self)}
{{- range .references }}
pp = append(pp, &Ref{ResourceType: types.{{ .expIdent }}ResourceType, Identifiers: MakeIdentifiers({{ .param }})})
{{- end }}
return
}
{{- end }}

View File

@@ -0,0 +1,401 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
"fmt"
"strings"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
{{- range .imports }}
"{{ . }}"
{{- end }}
)
type (
// StoreDecoder is responsible for generating Envoy nodes from already stored
// resources which can then be managed by Envoy and imported via an encoder.
StoreDecoder struct{}
filterWrap struct {
rt string
f envoyx.ResourceFilter
}
)
const (
paramsKeyStorer = "storer"
paramsKeyDAL = "dal"
)
var (
// @todo temporary fix to make unused pkg/id not throw errors
_ = id.Next
)
// Decode returns a set of envoy nodes based on the provided params
//
// StoreDecoder expects the DecodeParam of `storer` and `dal` which conform
// to the store.Storer and dal.FullService interfaces.
func (d StoreDecoder) Decode(ctx context.Context, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
// @todo we can optionally not require them based on what we're doing
s, err := d.getStorer(p)
if err != nil {
return
}
dl, err := d.getDal(p)
if err != nil {
return
}
return d.decode(ctx, s, dl, p)
}
func (d StoreDecoder) decode(ctx context.Context, s store.Storer, dl dal.FullService, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
// Preprocessing and basic filtering (to omit what this decoder can't handle)
wrappedFilters := d.prepFilters(p.Filter)
// Get all scoped nodes
scopedNodes, err := d.getScopeNodes(ctx, s, dl, wrappedFilters)
if err != nil {
return
}
// Get all reference nodes
refNodes, refRefs, err := d.getReferenceNodes(ctx, s, dl, wrappedFilters)
if err != nil {
return
}
// Process filters to get the envoy nodes
err = func() (err error) {
var aux envoyx.NodeSet
for i, wf := range wrappedFilters {
switch wf.rt {
{{ range .resources -}}
{{- if .envoy.omit}}{{continue}}{{ end -}}
case types.{{.expIdent}}ResourceType:
aux, err = d.decode{{.expIdent}}(ctx, s, dl, d.make{{.expIdent}}Filter(scopedNodes[i], refNodes[i], wf.f))
if err != nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
{{ end }}
default:
aux, err = d.extendDecoder(ctx, s, dl, p, wf.rt, refNodes[i], wf.f)
if err!= nil {
return
}
for _, a := range aux {
a.Identifiers = a.Identifiers.Merge(wf.f.Identifiers)
a.References = envoyx.MergeRefs(a.References, refRefs[i])
}
out = append(out, aux...)
}
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode filters")
return
}
return
}
{{- range .resources }}
{{- if .envoy.omit}}
{{continue}}
{{ end -}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource {{.ident}}
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) decode{{.expIdent}}(ctx context.Context, s store.Storer, dl dal.FullService, f types.{{.expIdent}}Filter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.Search{{.store.expIdentPlural}}(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = {{.expIdent}}ToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
{{ if .envoy.store.extendedDecoder -}}
aux, err := d.extended{{.expIdent}}Decoder(ctx, s, dl, f, out)
if err != nil {
return
}
out = append(out, aux...)
{{- end }}
return
}
func {{.expIdent}}ToEnvoyNode(r *types.{{.expIdent}}) (node *envoyx.Node, err error) {
// Identifiers
ii := envoyx.MakeIdentifiers(
{{- range .model.attributes -}}
{{- if not .envoy.identifier -}}
{{continue}}
{{ end }}
r.{{.expIdent}},
{{- end }}
)
// Handle references
// Omit any non-defined values
refs := map[string]envoyx.Ref{}
{{- range .model.attributes -}}
{{- if eq .dal.type "Ref" }}
if r.{{.expIdent}} > 0 {
refs["{{ .expIdent }}"] = envoyx.Ref{
ResourceType: "{{ .dal.refModelResType }}",
Identifiers: envoyx.MakeIdentifiers(r.{{.expIdent}}),
}
}
{{- end }}
{{- end }}
{{ if .envoy.store.extendedRefDecoder }}
refs = envoyx.MergeRefs(refs, decode{{.expIdent}}Refs(r))
{{ end }}
var scope envoyx.Scope
{{if and .envoy.scoped .parents}}
scope = envoyx.Scope{
ResourceType: refs["{{(index .parents 0).refField}}"].ResourceType,
Identifiers: refs["{{(index .parents 0).refField}}"].Identifiers,
}
for k, ref := range refs {
// @todo temporary solution to not needlessly scope resources.
// Optimally, this would be selectively handled by codegen.
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
{{end}}
{{if and .envoy.scoped (not .parents)}}
scope = envoyx.Scope{
ResourceType: types.{{ .expIdent }}ResourceType,
Identifiers: ii,
}
{{end}}
node = &envoyx.Node{
Resource: r,
ResourceType: types.{{.expIdent}}ResourceType,
Identifiers: ii,
References: refs,
Scope: scope,
}
return
}
{{ if not .envoy.store.customFilterBuilder }}
func (d StoreDecoder) make{{.expIdent}}Filter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.{{.expIdent}}Filter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.{{.expIdent}}ID = id.Strings(ids...)
{{ if .envoy.store.handleField }}
if len(hh) > 0 {
out.{{ .envoy.store.handleField }} = hh[0]
}
{{ end }}
// Refs
var (
ar *envoyx.Node
ok bool
)
_ = ar
_ = ok
{{ range .model.attributes }}
{{- if .envoy.store.omitRefFilter }}{{continue}}{{ end }}
{{ if eq .dal.type "Ref" }}
ar, ok = refs["{{ .expIdent }}"]
if ok {
{{ if .envoy.store.filterRefField -}}
out.{{ .envoy.store.filterRefField }} = ar.Resource.GetID()
{{- else -}}
out.{{ .expIdent }} = ar.Resource.GetID()
{{- end }}
}
{{ end }}
{{ end }}
{{- if .envoy.store.extendedFilterBuilder }}
out = d.extend{{.expIdent}}Filter(scope, refs, auxf, out)
{{ end -}}
return
}
{{ else }}
// Resource should define a custom filter builder
{{ end }}
{{end}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d StoreDecoder) getStorer(p envoyx.DecodeParams) (s store.Storer, err error) {
aux, ok := p.Params[paramsKeyStorer]
if ok {
s, ok = aux.(store.Storer)
if ok {
return
}
}
err = errors.Errorf("store decoder expects a storer conforming to store.Storer interface")
return
}
func (d StoreDecoder) getDal(p envoyx.DecodeParams) (dl dal.FullService, err error) {
aux, ok := p.Params[paramsKeyDAL]
if ok {
dl, ok = aux.(dal.FullService)
if ok {
return
}
}
err = errors.Errorf("store decoder expects a DAL conforming to dal.FullService interface")
return
}
func (d StoreDecoder) prepFilters(ff map[string]envoyx.ResourceFilter) (out []filterWrap) {
out = make([]filterWrap, 0, len(ff))
for rt, f := range ff {
// Handle resources that don't belong to this decoder
if !strings.HasPrefix(rt, "corteza::{{.componentIdent}}") {
continue
}
out = append(out, filterWrap{rt: rt, f: f})
}
return
}
func (d StoreDecoder) getScopeNodes(ctx context.Context, s store.Storer, dl dal.FullService, ff []filterWrap) (scopes envoyx.NodeSet, err error) {
// Get all requested scopes
scopes = make(envoyx.NodeSet, len(ff))
{{ if eq .componentIdent "compose" }}
err = func() (err error) {
for i, fw := range ff {
if fw.f.Scope.ResourceType == "" {
continue
}
// For now the scope can only point to namespace so this will do
var nn envoyx.NodeSet
nn, err = d.decodeNamespace(ctx, s, dl, d.makeNamespaceFilter(nil, nil, envoyx.ResourceFilter{Identifiers: fw.f.Scope.Identifiers}))
if err != nil {
return
}
if len(nn) > 1 {
err = fmt.Errorf("ambiguous scope %v: matches multiple resources", fw.f.Scope)
return
}
if len(nn) == 0 {
err = fmt.Errorf("invalid scope %v: resource not found", fw.f)
return
}
scopes[i] = nn[0]
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode node scopes")
return
}
{{ else }}
// @note skipping scope logic since it's currently only supported within
// Compose resources.
{{ end }}
return
}
// getReferenceNodes returns all of the nodes referenced by the nodes defined by the filters
//
// The nodes are provided as a slice (the same order as the filters) and as a map for easier lookups.
func (d StoreDecoder) getReferenceNodes(ctx context.Context, s store.Storer, dl dal.FullService, ff []filterWrap) (nodes []map[string]*envoyx.Node, refs []map[string]envoyx.Ref, err error) {
nodes = make([]map[string]*envoyx.Node, len(ff))
refs = make([]map[string]envoyx.Ref, len(ff))
err = func() (err error) {
for i, a := range ff {
if len(a.f.Refs) == 0 {
continue
}
auxr := make(map[string]*envoyx.Node, len(a.f.Refs))
auxa := make(map[string]envoyx.Ref)
for field, ref := range a.f.Refs {
f := ref.ResourceFilter()
aux, err := d.decode(ctx, s, dl, envoyx.DecodeParams{
Type: envoyx.DecodeTypeStore,
Filter: f,
})
if err != nil {
return err
}
// @todo consider changing this.
// Currently it's required because the .decode may return some
// nested nodes as well.
// Consider a flag or a new function.
aux = envoyx.NodesForResourceType(ref.ResourceType, aux...)
if len(aux) == 0 {
return fmt.Errorf("invalid reference %v", ref)
}
if len(aux) > 1 {
return fmt.Errorf("ambiguous reference: too many resources returned %v", a.f)
}
auxr[field] = aux[0]
auxa[field] = aux[0].ToRef()
}
nodes[i] = auxr
refs[i] = auxa
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode node references")
return
}
return
}

View File

@@ -0,0 +1,513 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/expr"
"github.com/cortezaproject/corteza/server/pkg/id"
"github.com/cortezaproject/corteza/server/store"
"github.com/pkg/errors"
{{- range .imports }}
"{{ . }}"
{{- end }}
)
type (
// StoreEncoder is responsible for encoding Corteza resources into the
// database via the Storer or the DAL interface
//
// @todo consider having a different encoder for the DAL resources
StoreEncoder struct{}
)
{{ $rootRes := .resources }}
// Prepare performs some initial processing on the resource before it can be encoded
//
// Preparation runs validation, default value initialization, matching with
// already existing instances, ...
//
// The prepare function receives a set of nodes grouped by the resource type.
// This enables some batching optimization and simplifications when it comes to
// matching with existing resources.
//
// Prepare does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e StoreEncoder) Prepare(ctx context.Context, p envoyx.EncodeParams, rt string, nn envoyx.NodeSet) (err error) {
s, err := e.grabStorer(p)
if err != nil {
return
}
switch rt {
{{- range .resources }}
{{- if .envoy.omit}}
{{continue}}
{{end -}}
case types.{{.expIdent}}ResourceType:
return e.prepare{{.expIdent}}(ctx, p, s, nn)
{{ end -}}
default:
return e.prepare(ctx, p, s, rt, nn)
}
return
}
// Encode encodes the given Corteza resources into the primary store
//
// Encoding should not do any additional processing apart from matching with
// dependencies and runtime validation
//
// The Encode function is called for every resource type where the resource
// appears at the root of the dependency tree.
// All of the root-level resources for that resource type are passed into the function.
// The encoding function must traverse the branches to encode all of the dependencies.
//
// This flow is used to simplify the flow of how resources are encoded into YAML
// (and other documents) as well as to simplify batching.
//
// Encode does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e StoreEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tree envoyx.Traverser) (err error) {
s, err := e.grabStorer(p)
if err != nil {
return
}
switch rt {
{{- range .resources }}
{{- if .envoy.omit -}}
{{continue}}
{{end}}
case types.{{.expIdent}}ResourceType:
return e.encode{{.expIdent}}s(ctx, p, s, nodes, tree)
{{ end -}}
default:
return e.encode(ctx, p, s, rt, nodes, tree)
}
}
{{- range .resources }}
{{- if .envoy.omit}}
{{continue}}
{{end}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource {{.ident}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// prepare{{.expIdent}} prepares the resources of the given type for encoding
func (e StoreEncoder) prepare{{.expIdent}}(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet) (err error) {
// Grab an index of already existing resources of this type
// @note since these resources should be fairly low-volume and existing for
// a short time (and because we batch by resource type); fetching them all
// into memory shouldn't hurt too much.
// @todo do some benchmarks and potentially implement some smarter check such as
// a bloom filter or something similar.
// Get node scopes
scopedNodes, err := e.getScopeNodes(ctx, s, nn)
if err != nil {
err = errors.Wrap(err, "failed to get scope nodes")
return
}
// Initializing the index here (and using a hashmap) so it's not escaped to the heap
existing := make(map[int]types.{{.expIdent}}, len(nn))
err = e.matchup{{.expIdent}}s(ctx, s, existing, scopedNodes, nn)
if err != nil {
err = errors.Wrap(err, "failed to matchup existing {{.expIdent}}s")
return
}
for i, n := range nn {
if n.Resource == nil {
panic("unexpected state: cannot call prepare{{.expIdent}} with nodes without a defined Resource")
}
res, ok := n.Resource.(*types.{{.expIdent}})
if !ok {
panic("unexpected resource type: node expecting type of {{.ident}}")
}
existing, hasExisting := existing[i]
// Run expressions on the nodes
err = e.runEvals(ctx, hasExisting, n)
if err != nil {
return
}
if hasExisting {
// On existing, we don't need to re-do identifiers and references; simply
// changing up the internal resource is enough.
//
// In the future, we can pass down the tree and re-do the deps like that
switch n.Config.MergeAlg {
case envoyx.OnConflictPanic:
err = errors.Errorf("resource %v already exists", n.Identifiers.Slice)
return
case envoyx.OnConflictReplace:
// Replace; simple ID change should do the trick
res.ID = existing.ID
case envoyx.OnConflictSkip:
// Replace the node's resource with the fetched one
res = &existing
// @todo merging
}
} else {
// @todo actually a bottleneck. As per sonyflake docs, it can at most
// generate up to 2**8 (256) IDs per 10ms in a single thread.
// How can we improve this?
res.ID = id.Next()
}
// We can skip validation/defaults when the resource is overwritten by
// the one already stored (the panic one errors out anyway) since it
// should already be ok.
if !hasExisting || n.Config.MergeAlg != envoyx.OnConflictSkip {
err = e.set{{.expIdent}}Defaults(res)
if err != nil {
return err
}
err = e.validate{{.expIdent}}(res)
if err != nil {
return err
}
}
n.Resource = res
}
return
}
// encode{{.expIdent}}s encodes a set of resource into the database
func (e StoreEncoder) encode{{.expIdent}}s(ctx context.Context, p envoyx.EncodeParams, s store.Storer, nn envoyx.NodeSet, tree envoyx.Traverser) (err error) {
for _, n := range nn {
err = e.encode{{.expIdent}}(ctx, p, s, n, tree)
if err != nil {
return
}
}
{{- if .envoy.store.postSetEncoder }}
err = e.post{{.expIdent}}sEncode(ctx, p, s, tree, nn)
{{end }}
return
}
// encode{{.expIdent}} encodes the resource into the database
func (e StoreEncoder) encode{{.expIdent}}(ctx context.Context, p envoyx.EncodeParams, s store.Storer, n *envoyx.Node, tree envoyx.Traverser) (err error) {
// Grab dependency references
var auxID uint64
err = func() (err error) {
for fieldLabel, ref := range n.References {
auxID = safeParentID(tree, n, ref)
if auxID == 0 {
continue
}
err = n.Resource.SetValue(fieldLabel, 0, auxID)
if err != nil {
return
}
}
return
}()
if err != nil {
err = errors.Wrap(err, fmt.Sprintf("failed to set dependency references for %s %v", n.ResourceType, n.Identifiers.Slice))
return
}
{{ if .envoy.store.sanitizeBeforeSave }}
// Custom resource sanitization before saving.
// This can be used to cleanup arbitrary config fields.
e.sanitize{{.expIdent}}BeforeSave(n.Resource.(*types.{{.expIdent}}))
{{ end }}
// Flush to the DB
if !n.Evaluated.Skip {
err = store.Upsert{{.store.expIdent}}(ctx, s, n.Resource.(*types.{{.expIdent}}))
if err != nil {
err = errors.Wrap(err, "failed to upsert {{.expIdent}}")
return
}
}
{{ $a := . }}
// Handle resources nested under it
//
// @todo how can we remove the OmitPlaceholderNodes call the same way we did for
// the root function calls?
{{/*
@note this setup will not duplicate encode calls since we only take the
most specific parent resource.
*/}}
{{ $extendedEncoder := .envoy.store.extendedEncoder}}
{{ if $extendedEncoder }}
nested := make(envoyx.NodeSet, 0, 10)
{{ end }}
err = func() (err error) {
for rt, nn := range envoyx.NodesByResourceType(tree.Children(n)...) {
nn = envoyx.OmitPlaceholderNodes(nn...)
switch rt {
{{- range $cmp := $rootRes }}
{{ if $cmp.envoy.omit }}
{{continue}}
{{ end }}
{{ if not $cmp.parents }}
{{continue}}
{{ end }}
{{ $p := index $cmp.parents (sub (len $cmp.parents) 1)}}
{{ if not (eq $p.handle $a.ident) }}
{{continue}}
{{ end }}
case types.{{$cmp.expIdent}}ResourceType:
err = e.encode{{$cmp.expIdent}}s(ctx, p, s, nn, tree)
if err != nil {
return
}
{{ if $extendedEncoder }}
nested = append(nested, nn...)
{{ end }}
{{- end }}
}
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to encode nested resources")
return
}
{{ if .envoy.store.extendedEncoder }}
err = e.encode{{.expIdent}}Extend(ctx, p, s, n, nested, tree)
if err != nil {
err = errors.Wrap(err, "post encode logic failed with errors")
return
}
{{ end }}
{{ if .envoy.store.extendedSubResources }}
err = e.encode{{.expIdent}}ExtendSubResources(ctx, p, s, n, tree)
if err != nil {
err = errors.Wrap(err, "failed to encode extended sub resources")
return
}
{{ end }}
return
}
// matchup{{.expIdent}}s returns an index with indicates what resources already exist
func (e StoreEncoder) matchup{{.expIdent}}s(ctx context.Context, s store.Storer, uu map[int]types.{{.expIdent}}, scopes envoyx.NodeSet, nn envoyx.NodeSet) (err error) {
// @todo might need to do it smarter then this.
// Most resources won't really be that vast so this should be acceptable for now.
aa, _, err := store.Search{{.store.expIdentPlural}}(ctx, s, types.{{.expIdent}}Filter{})
if err != nil {
return
}
idMap := make(map[uint64]*types.{{.expIdent}}, len(aa))
strMap := make(map[string]*types.{{.expIdent}}, len(aa))
for _, a := range aa {
{{ range .model.attributes }}
{{- if not .envoy.identifier -}}
{{continue}}
{{- end -}}
{{- if eq .goType "uint64" -}}
idMap[a.{{.expIdent}}] = a
{{- else -}}
strMap[a.{{.expIdent}}] = a
{{- end}}
{{ end }}
}
var aux *types.{{.expIdent}}
var ok bool
for i, n := range nn {
{{ if eq .component "compose" }}
scope := scopes[i]
if scope == nil {
continue
}
{{ end }}
for _, idf := range n.Identifiers.Slice {
if id, err := strconv.ParseUint(idf, 10, 64); err == nil {
aux, ok = idMap[id]
if ok {
uu[i] = *aux
// When any identifier matches we can end it
break
}
}
aux, ok = strMap[idf]
if ok {
uu[i] = *aux
// When any identifier matches we can end it
break
}
}
}
return
}
{{end}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility functions
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e *StoreEncoder) grabStorer(p envoyx.EncodeParams) (s store.Storer, err error) {
auxs, ok := p.Params[paramsKeyStorer]
if !ok {
err = errors.Errorf("store encoder expects a store conforming to store.Storer interface")
return
}
s, ok = auxs.(store.Storer)
if !ok {
err = errors.Errorf("store encoder expects a store conforming to store.Storer interface")
return
}
return
}
func (e *StoreEncoder) runEvals(ctx context.Context, existing bool, n *envoyx.Node) (err error) {
// Skip if
if n.Config.SkipIfEval == nil {
return
}
aux, err := expr.EmptyVars().Cast(map[string]any{
"missing": !existing,
})
if err != nil {
return
}
n.Evaluated.Skip, err = n.Config.SkipIfEval.Test(ctx, aux.(*expr.Vars))
return
}
func (e StoreEncoder) getScopeNodes(ctx context.Context, s store.Storer, nn envoyx.NodeSet) (scopes envoyx.NodeSet, err error) {
// Get all requested scopes
scopes = make(envoyx.NodeSet, len(nn))
{{ if eq .componentIdent "compose" }}
err = func() (err error) {
for i, n := range nn {
if n.Scope.ResourceType == "" {
continue
}
// For now the scope can only point to namespace so this will do
var nn envoyx.NodeSet
nn, err = e.decodeNamespace(ctx, s, e.makeNamespaceFilter(nil, nil, envoyx.ResourceFilter{Identifiers: n.Scope.Identifiers}))
if err != nil {
return
}
if len(nn) > 1 {
err = fmt.Errorf("ambiguous scope %v: matches multiple resources", n.Scope)
return
}
// when encoding, it could be missing
if len(nn) == 0 {
return
}
scopes[i] = nn[0]
}
return
}()
if err != nil {
err = errors.Wrap(err, "failed to decode node scopes")
return
}
{{ else }}
// @note skipping scope logic since it's currently only supported within
// Compose resources.
{{ end }}
return
}
{{ if eq .componentIdent "compose" }}
func (e StoreEncoder) decodeNamespace(ctx context.Context, s store.Storer, f types.NamespaceFilter) (out envoyx.NodeSet, err error) {
// @todo this might need to be improved.
// Currently, no resource is vast enough to pose a problem.
rr, _, err := store.SearchComposeNamespaces(ctx, s, f)
if err != nil {
return
}
for _, r := range rr {
var n *envoyx.Node
n, err = NamespaceToEnvoyNode(r)
if err != nil {
return
}
out = append(out, n)
}
return
}
func (e StoreEncoder) makeNamespaceFilter(scope *envoyx.Node, refs map[string]*envoyx.Node, auxf envoyx.ResourceFilter) (out types.NamespaceFilter) {
out.Limit = auxf.Limit
ids, hh := auxf.Identifiers.Idents()
_ = ids
_ = hh
out.NamespaceID = id.Strings(ids...)
if len(hh) > 0 {
out.Slug = hh[0]
}
if scope == nil {
return
}
if scope.ResourceType == "" {
return
}
// Overwrite it
out.NamespaceID = id.Strings(scope.Resource.GetID())
return
}
{{ end }}
func safeParentID(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) (out uint64) {
rn := tt.ParentForRef(n, ref)
if rn == nil {
return
}
return rn.Resource.GetID()
}

View File

@@ -0,0 +1,92 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"strings"
{{- range .imports }}
"{{ . }}"
{{- end }}
)
// SplitResourceIdentifier takes an identifier string and splices it into path
// identifiers as defined by the resource
func SplitResourceIdentifier(ref string) (out map[string]Ref) {
out = make(map[string]Ref, 3)
ref = strings.TrimRight(ref, "/")
pp := strings.Split(ref, "/")
rt := pp[0]
pp = pp[1:]
// @todo this is to support res. tr. resources also.
// Split it into a separate function and remove this.
if !strings.HasPrefix(rt, "corteza::") {
rt = "corteza::" + rt
}
gRef := func(pp []string, i int) string {
if pp[i] == "*" {
return ""
}
return pp[i]
}
switch rt {
{{ range .components }}
{{ $rootCmp := . }}
{{range .resources}}
{{ $a := . }}
case "{{.fqrt}}":
scope := Scope{}
{{$res := .}}
{{range $i, $p := .parents}}
if gRef(pp, {{$i}}) == "" {
return
}
{{- range $cmp := $rootCmp.resources }}
{{ if $cmp.envoy.omit }}
{{continue}}
{{ end }}
{{ if eq $p.handle $cmp.ident }}
{{ if and (eq $rootCmp.ident "compose") (eq $i 0) }}
aux := gRef(pp, {{ $i }})
if aux != "" {
scope.ResourceType = "corteza::compose:namespace"
scope.Identifiers = MakeIdentifiers(aux)
}
{{ end }}
out["Path.{{$i}}"] = Ref{
ResourceType: "{{$cmp.fqrt}}",
Identifiers: MakeIdentifiers(gRef(pp, {{ $i }})),
Scope: scope,
}
{{break}}
{{ end }}
{{- end }}
{{end}}
if gRef(pp, {{len .parents}}) == "" {
return
}
{{if eq .ident "namespace"}}
scope.ResourceType = "{{.fqrt}}"
scope.Identifiers = MakeIdentifiers(gRef(pp, {{len .parents}}))
{{end}}
out["Path.{{len .parents}}"] = Ref{
ResourceType: "{{.fqrt}}",
Identifiers: MakeIdentifiers(gRef(pp, {{len .parents}})),
Scope: scope,
}
{{ end }}
{{ end }}
}
return
}

View File

@@ -0,0 +1,33 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
{{- range .imports }}
"{{ . }}"
{{- end }}
)
var (
// needyResources is a list of resources that require a parent resource
//
// This list is primarily used when figuring out what nodes the dep. graph
// should return when traversing.
needyResources = map[string]bool{
{{- range .components -}}
{{- range .resources -}}
{{- if not .parents }}
{{continue}}
{{- end }}
"{{ .fqrt }}": true,
{{- end }}
{{- end }}
"corteza::compose:record-datasource": true,
}
// superNeedyResources is the second level of filtering in case the first
// pass removes everything
superNeedyResources = map[string]bool{
"corteza::compose:module-field": true,
}
)

View File

@@ -0,0 +1,872 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"strings"
"context"
"io"
"os"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"golang.org/x/text/language"
"github.com/spf13/cast"
"gopkg.in/yaml.v3"
"github.com/pkg/errors"
{{- range .imports }}
"{{ . }}"
{{- end }}
)
{{$cmpIdent := .componentIdent}}
{{$rootRes := .resources}}
type (
// YamlDecoder is responsible for decoding YAML documents into Corteza resources
// which are then managed by envoy and imported via an encoder.
YamlDecoder struct{}
// documentContext provides a bit of metadata to the decoder such as
// root-level reference definitions (such as the namespace)
documentContext struct {
// references holds any references defined on the root of the document
//
// Example of defining a namespace reference:
// namespace: test_import_namespace
// modules:
// - name: Test Import Module
// handle: test_import_module
references map[string]string
// parentIdent holds the identifier of the parent resource (if nested).
// The parent ident can be handy if the resource requires the info before the
// original handling is finished (such as record datasource nodes).
parentIdent envoyx.Identifiers
}
// auxYamlDoc is a helper struct that registers custom YAML decoders
// to assist the package
auxYamlDoc struct {
nodes envoyx.NodeSet
}
)
const (
paramsKeyStream = "stream"
)
// CanFile returns true if the provided file can be decoded with this decoder
func (d YamlDecoder) CanFile(f *os.File) (ok bool) {
// @todo improve this check; for now a simple extension check should do the trick
return d.CanExt(f.Name())
}
// CanExt returns true if the provided file extension can be decoded with this decoder
func (d YamlDecoder) CanExt(name string) (ok bool) {
var (
pt = strings.Split(name, ".")
ext = strings.TrimSpace(pt[len(pt)-1])
)
return ext == "yaml" || ext == "yml"
}
// Decode returns a set of envoy nodes based on the provided params
//
// YamlDecoder expects the DecodeParam of `stream` which conforms
// to the io.Reader interface.
func (d YamlDecoder) Decode(ctx context.Context, p envoyx.DecodeParams) (out envoyx.NodeSet, err error) {
r, err := d.getReader(ctx, p)
if err != nil {
return
}
// Offload decoding to the aux document
doc := &auxYamlDoc{}
err = yaml.NewDecoder(r).Decode(doc)
if err != nil {
err = errors.Wrap(err, "{{$cmpIdent}} yaml decoder: failed to decode document")
return
}
return doc.nodes, nil
}
func (d *auxYamlDoc) UnmarshalYAML(n *yaml.Node) (err error) {
// Get the document context from the root level
dctx, err := d.getDocumentContext(n)
if err != nil {
return
}
var aux envoyx.NodeSet
return y7s.EachMap(n, func(k, v *yaml.Node) error {
kv := strings.ToLower(k.Value)
switch kv {
// Decode all resources under the {{$cmpIdent}} component
{{- range .resources -}}
{{- if .envoy.omit -}}
{{continue}}
{{- end -}}
{{ $identKeys := .envoy.yaml.identKeys }}
case {{ range $i, $l := $identKeys -}}
"{{ $l }}"{{if not (eq $i (sub (len $identKeys) 1))}},{{end}}
{{- end}}:
{{- if .envoy.yaml.supportMappedInput }}
if y7s.IsMapping(v) {
aux, err = d.unmarshal{{.expIdent}}Map(dctx, v)
d.nodes = append(d.nodes, aux...)
}
{{ else }}
// @note {{ .ident }} doesn't support mapped inputs. This can be
// changed in the .cue definition under the
// .envoy.yaml.supportMappedInput field.
{{- end }}
if y7s.IsSeq(v) {
aux, err = d.unmarshal{{.expIdent}}Seq(dctx, v)
d.nodes = append(d.nodes, aux...)
}
if err != nil {
err = errors.Wrap(err, "failed to unmarshal node: {{.ident}}")
}
return err
{{ end }}
{{/* @todo this should be improved and offloaded to the cue instead of template sys */}}
{{- if eq .componentIdent "system" }}
// Decode access control nodes
case "allow":
aux, err = unmarshalAllowNode(v)
d.nodes = append(d.nodes, aux...)
if err != nil {
err = errors.Wrap(err, "failed to unmarshal node: RBAC allow")
}
case "deny":
aux, err = unmarshalDenyNode(v)
d.nodes = append(d.nodes, aux...)
if err != nil {
err = errors.Wrap(err, "failed to unmarshal node: RBAC deny")
}
// Resource translation nodes
case "locale", "translation", "translations", "i18n":
aux, err = unmarshalLocaleNode(v)
d.nodes = append(d.nodes, aux...)
if err != nil {
err = errors.Wrap(err, "failed to unmarshal node: locale")
}
{{ end }}
// Offload to custom handlers
default:
aux, err = d.unmarshalYAML(kv, v)
d.nodes = append(d.nodes, aux...)
if err != nil {
err = errors.Wrap(err, "failed to unmarshal node")
}
}
return nil
})
}
{{- range .resources }}
{{- if .envoy.omit}}
{{continue}}
{{ end -}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource {{.ident}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// unmarshal{{.expIdent}}Seq unmarshals {{.expIdent}} when provided as a sequence node
func (d *auxYamlDoc) unmarshal{{.expIdent}}Seq(dctx documentContext, n *yaml.Node) (out envoyx.NodeSet, err error) {
var aux envoyx.NodeSet
err = y7s.EachSeq(n, func(n *yaml.Node) error {
aux, err = d.unmarshal{{ .expIdent }}Node(dctx, n)
if err != nil {
return err
}
out = append(out, aux...)
return nil
})
return
}
// unmarshal{{.expIdent}}Map unmarshals {{.expIdent}} when provided as a mapping node
//
// When map encoded, the map key is used as a preset identifier.
// The identifier is passed to the node function as a meta node
{{- if not .envoy.yaml.supportMappedInput }}
// @note this resource does not support map encoding.
// Refer to the corresponding definition files to adjust if needed.
{{ else }}
func (d *auxYamlDoc) unmarshal{{ .expIdent }}Map(dctx documentContext, n *yaml.Node) (out envoyx.NodeSet, err error) {
var aux envoyx.NodeSet
err = y7s.EachMap(n, func(k, n *yaml.Node) error {
aux, err = d.unmarshal{{ .expIdent }}Node(dctx, n, k)
if err != nil {
return err
}
out = append(out, aux...)
return nil
})
return
}
{{ end }}
{{ range .envoy.yaml.extendedResourceDecoders -}}
// unmarshal{{.expIdent}}ExtendedSeq unmarshals {{.expIdent}} when provided as a sequence node
func (d *auxYamlDoc) unmarshalExtended{{.expIdent}}Seq(dctx documentContext, n *yaml.Node) (out envoyx.NodeSet, err error) {
var aux envoyx.NodeSet
err = y7s.EachSeq(n, func(n *yaml.Node) error {
aux, err = d.unmarshal{{ .expIdent }}ExtendedNode(dctx, n)
if err != nil {
return err
}
out = append(out, aux...)
return nil
})
return
}
{{if not .supportMappedInput}}{{continue}}{{end}}
// unmarshal{{.expIdent}}ExtendedMap unmarshals {{.expIdent}} when provided as a mapping node
//
// When map encoded, the map key is used as a preset identifier.
// The identifier is passed to the node function as a meta node
func (d *auxYamlDoc) unmarshalExtended{{ .expIdent }}Map(dctx documentContext, n *yaml.Node) (out envoyx.NodeSet, err error) {
var aux envoyx.NodeSet
err = y7s.EachMap(n, func(k, n *yaml.Node) error {
aux, err = d.unmarshal{{ .expIdent }}ExtendedNode(dctx, n, k)
if err != nil {
return err
}
out = append(out, aux...)
return nil
})
return
}
{{ end }}
// unmarshal{{ .expIdent }}Node is a cookie-cutter function to unmarshal
// the yaml node into the corresponding Corteza type & Node
func (d *auxYamlDoc) unmarshal{{ .expIdent }}Node(dctx documentContext, n *yaml.Node, meta ...*yaml.Node) (out envoyx.NodeSet, err error) {
var r *types.{{ .expIdent }}
// @todo we're omitting errors because there will be a bunch due to invalid
// resource field types. This might be a bit unstable as other errors may
// also get ignored.
//
// A potential fix would be to firstly unmarshal into an any, check errors
// and then unmarshal into the resource while omitting errors.
n.Decode(&r)
// Identifiers are determined manually when iterating the yaml node.
// This is to help assure there are no duplicates and everything
// was accounted for especially when working with aliases such as
// user_name instead of userName.
ii := envoyx.Identifiers{}
{{ if .envoy.yaml.supportMappedInput}}
// When a resource supports mapped input, the key is passed as meta which
// needs to be registered as an identifier (since it is)
if len(meta) > 0 {
y7s.DecodeScalar(meta[0], "{{ .envoy.yaml.mappedField }}", &r.{{ .envoy.yaml.mappedField }})
ii = ii.Add(r.{{ .envoy.yaml.mappedField }})
}
{{ end }}
var (
refs = make(map[string]envoyx.Ref)
auxOut envoyx.NodeSet
nestedNodes envoyx.NodeSet
scope envoyx.Scope
envoyConfig envoyx.EnvoyConfig
{{- if .rbac }}
rbacNodes envoyx.NodeSet
{{- end }}
)
_ = auxOut
_ = refs
err = y7s.EachMap(n, func(k, n *yaml.Node) error {
var auxNodeValue any
_ = auxNodeValue
switch strings.ToLower(k.Value) {
{{ $resource := . }}
{{/*
Iterate over all model attributes and handle
- attribute aliases
- identifiers
- reference
- custom decoding logic
Generic field decoding is already handled at the top with the generic
node.Decode(...) function call.
*/}}
{{- range $attr := .model.attributes }}
{{/*
We can skip the attributes when...
- there aren't any ident key aliases
- not an identifier
- not a reference
- no custom logic
*/}}
{{
if and
(or (not $attr.envoy.yaml.identKeyAlias) (eq (len $attr.envoy.yaml.identKeyAlias) 0))
(not $attr.envoy.identifier)
(not (eq $attr.dal.type "Ref"))
(not $attr.envoy.yaml.customDecoder)
}}
{{continue}}
{{ end }}
{{ $identKeys := .envoy.yaml.identKeys }}
case {{ range $i, $l := $identKeys -}}
"{{ $l }}"{{if not (eq $i (sub (len $identKeys) 1))}},{{end}}
{{- end}}:
{{- if and $attr.envoy.yaml.identKeyAlias (gt (len $attr.envoy.yaml.identKeyAlias) 0) (not (eq $attr.dal.type "Ref")) }}
// Handle field alias
//
// @todo consider adding an is empty check before overwriting
err = y7s.DecodeScalar(n, "{{ $attr.ident }}", &r.{{ $attr.expIdent }})
if err != nil {
return err
}
{{- end }}
{{- if $attr.envoy.identifier }}
// Handle identifiers
err = y7s.DecodeScalar(n, "{{ $attr.ident }}", &auxNodeValue)
if err != nil {
return err
}
ii = ii.Add(auxNodeValue)
{{- end }}
{{- if eq $attr.dal.type "Ref" }}
// Handle references
err = y7s.DecodeScalar(n, "{{ $attr.ident }}", &auxNodeValue)
if err != nil {
return err
}
// Omit if not defined
tmp := cast.ToString(auxNodeValue)
if tmp == "0" || tmp == "" {
break
}
refs["{{ $attr.expIdent }}"] = envoyx.Ref{
ResourceType: "{{ $attr.dal.refModelResType }}",
Identifiers: envoyx.MakeIdentifiers(auxNodeValue),
}
{{- end }}
{{ if $attr.envoy.yaml.customDecoder }}
// Handle custom node decoder
//
// The decoder may update the passed resource with arbitrary values
// as well as provide additional references and identifiers for the node.
var (
auxRefs map[string]envoyx.Ref
auxIdents envoyx.Identifiers
)
auxRefs, auxIdents, err = d.unmarshal{{ $resource.expIdent }}{{ $attr.expIdent }}Node(r, n)
if err != nil {
return err
}
refs = envoyx.MergeRefs(refs, auxRefs)
ii = ii.Merge(auxIdents)
{{ end }}
break
{{- end }}
{{ if .rbac }}
// Handle RBAC rules
case "allow":
auxOut, err = unmarshalAllowNode(n)
if err != nil {
return err
}
rbacNodes = append(rbacNodes, auxOut...)
auxOut = nil
case "deny":
auxOut, err = unmarshalDenyNode(n)
if err != nil {
return err
}
rbacNodes = append(rbacNodes, auxOut...)
auxOut = nil
{{- end }}
case "(envoy)":
envoyConfig = d.decodeEnvoyConfig(n)
}
return nil
})
if err != nil {
return
}
// Make parent identifiers available through the dctx
dctx.parentIdent = ii
{{ if eq $cmpIdent "compose" }}
// Handle global namespace reference which can be provided as the doc. context
//
// @todo this is a temporary solution and should be extended when the document
// context needs to be extended.
// Limit this only to the compose resource since that is the only scenario
// the previous implementation supports.
if ref, ok := dctx.references["namespace"]; ok {
refs["NamespaceID"] = envoyx.Ref{
ResourceType: types.NamespaceResourceType,
Identifiers: envoyx.MakeIdentifiers(ref),
}
}
{{ end }}
{{- if and .envoy.scoped .parents}}
// Define the scope
//
// This resource is scoped to the first parent (generally the namespace)
// when talking about Compose resources (the only supported scenario at the moment).
scope = envoyx.Scope{
ResourceType: refs["{{(index .parents 0).refField}}"].ResourceType,
Identifiers: refs["{{(index .parents 0).refField}}"].Identifiers,
}
{{end}}
{{if and .envoy.scoped (not .parents)}}
// Define the scope
//
// This resource is scoped with no parent resources so this resource is the
// root itself (generally the namespace -- the only currently supported scenario).
scope = envoyx.Scope{
ResourceType: types.{{ .expIdent }}ResourceType,
Identifiers: ii,
}
{{end}}
// Apply the scope to all of the references of the same type
for k, ref := range refs {
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
refs[k] = ref
}
// Handle any resources that could be inserted under {{.ident}} such as a module inside a namespace
//
// This operation is done in the second pass of the document so we have
// the complete context of the current resource; such as the identifier,
// references, and scope.
var auxNestedNodes envoyx.NodeSet
err = y7s.EachMap(n, func(k, n *yaml.Node) error {
nestedNodes = nil
switch strings.ToLower(k.Value) {
{{/*
@note each resource can only nest resources from the same component.
Iterate resources of the current component and if this one appears as any
of the parent resources, attempt to unmarshal it.
@todo consider limiting the supported set to only the root parent
(like we do with store encoders) to limit strange cases and reduce
potential problems.
*/}}
{{ $a := . }}
{{- range $cmp := $rootRes }}
{{ if $cmp.envoy.omit }}
{{continue}}
{{ end }}
{{- range $p := $cmp.parents }}
{{ if not (eq $p.handle $a.ident) }}
{{continue}}
{{ end }}
{{ $identKeys := $cmp.envoy.yaml.identKeys }}
case {{ range $i, $l := $identKeys -}}
"{{ $l }}"{{if not (eq $i (sub (len $identKeys) 1))}},{{end}}
{{- end}}:
if y7s.IsSeq(n) {
nestedNodes, err = d.unmarshal{{$cmp.expIdent}}Seq(dctx, n)
if err != nil {
return err
}
} else {
nestedNodes, err = d.unmarshal{{$cmp.expIdent}}Map(dctx, n)
if err != nil {
return err
}
}
break
{{/* As long as one parent matches we're golden; avoid potential duplicates */}}
{{break}}
{{- end }}
{{- end -}}
{{- range .envoy.yaml.extendedResourceDecoders }}
{{ $identKeys := .identKeys }}
case {{ range $i, $l := $identKeys -}}
"{{ $l }}"{{if not (eq $i (sub (len $identKeys) 1))}},{{end}}
{{- end}}:
if y7s.IsSeq(n) {
nestedNodes, err = d.unmarshalExtended{{.expIdent}}Seq(dctx, n)
if err != nil {
return err
}
} {{- if .supportMappedInput }} else {
nestedNodes, err = d.unmarshalExtended{{.expIdent}}Map(dctx, n)
if err != nil {
return err
}
}{{ end }}
break
{{ end -}}
}
// Iterate nested nodes and update their reference to the current resource
//
// Any reference to the parent resource from the child resource is overwritten
// to avoid potential user-error edge cases.
for _, a := range nestedNodes {
// @note all nested resources fall under the same component and the same scope.
// Simply assign the same scope to all -- if it shouldn't be scoped
// the parent won't have it (saving CPU ticks :)
a.Scope = scope
if a.References == nil {
a.References = make(map[string]envoyx.Ref)
}
if _, ok := a.References["{{if .envoy.yaml.extendedResourceRefIdent}}{{.envoy.yaml.extendedResourceRefIdent}}{{else}}{{.expIdent}}ID{{end}}"]; !ok {
a.References["{{if .envoy.yaml.extendedResourceRefIdent}}{{.envoy.yaml.extendedResourceRefIdent}}{{else}}{{.expIdent}}ID{{end}}"] = envoyx.Ref{
ResourceType: types.{{ .expIdent }}ResourceType,
Identifiers: ii,
Scope: scope,
}
}
for f, ref := range a.References {
if !strings.HasPrefix(ref.ResourceType, "corteza::compose") {
continue
}
ref.Scope = scope
a.References[f] = ref
}
for f, ref := range refs {
// Only inherit root references
// @todo improve; this is a hack
if strings.Contains(f, ".") {
continue
}
// Only assume refs if they're not yet set
if _, ok := a.References[f]; ok {
continue
}
a.References[f] = ref
}
}
auxNestedNodes = append(auxNestedNodes, nestedNodes...)
return nil
})
if err != nil {
return
}
{{ if .envoy.yaml.extendedResourcePostProcess}}
auxNestedNodes, err = d.postProcessNested{{.expIdent}}Nodes(auxNestedNodes)
if err!= nil {
return
}
{{ end }}
out = append(out, auxNestedNodes...)
a := &envoyx.Node{
Resource: r,
ResourceType: types.{{ .expIdent }}ResourceType,
Identifiers: ii,
References: refs,
{{if or .envoy.scoped}}
Scope: scope,
{{end}}
Config: envoyConfig,
}
{{- if .rbac }}
// Update RBAC resource nodes with references regarding the resource
rres := r.RbacResource()
for _, rn := range rbacNodes {
aux := rn.Resource.(*rbac.Rule)
if aux.Resource == "" {
aux.Resource = rres
}
// Since the rule belongs to the resource, it will have the same
// subset of references as the parent resource.
rn.References = envoyx.MergeRefs(rn.References, a.References)
// The RBAC rule's most specific identifier is the resource itself.
// Using this we can hardcode it to point to the location after the parent resource.
//
// @todo consider using a more descriptive identifier for the position
// such as `index-%d`.
rn.References["{{len .parents}}"] = envoyx.Ref{
ResourceType: a.ResourceType,
Identifiers: a.Identifiers,
Scope: scope,
}
for _, r := range rn.References {
if r.Scope.IsEmpty() {
continue
}
rn.Scope = r.Scope
break
}
}
{{ end }}
// Put it all together...
out = append(out, a)
out = append(out, auxOut...)
{{- if .rbac }}
out = append(out, rbacNodes...)
{{ end }}
return
}
{{ end }}
// // // // // // // // // // // // // // // // // // // // // // // // //
// RBAC unmarshal logic
// // // // // // // // // // // // // // // // // // // // // // // // //
func unmarshalAllowNode(n *yaml.Node) (out envoyx.NodeSet, err error) {
return unmarshalRBACNode(n, rbac.Allow)
}
func unmarshalDenyNode(n *yaml.Node) (out envoyx.NodeSet, err error) {
return unmarshalRBACNode(n, rbac.Deny)
}
func unmarshalRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) {
if y7s.IsMapping(n.Content[1]) {
out, err = unmarshalFlatRBACNode(n, acc)
if err != nil {
return
}
} else {
out, err = unmarshalNestedRBACNode(n, acc)
if err != nil {
return
}
}
for _, o := range out {
for _, r := range o.References {
if r.Scope.IsEmpty() {
continue
}
o.Scope = r.Scope
break
}
}
return
}
// unmarshalNestedRBACNode handles RBAC rules when they are nested inside a resource
//
// The edge-case exists since the node doesn't explicitly specify the resource
// it belongs to.
//
// Example:
//
// modules:
// module1:
// name: "module 1"
// fields: ...
// allow:
// role1:
// - read
// - delete
func unmarshalNestedRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) {
return out, y7s.EachMap(n, func(role, op *yaml.Node) error {
out = append(out, &envoyx.Node{
Resource: &rbac.Rule{
Operation: op.Value,
Access: acc,
},
ResourceType: rbac.RuleResourceType,
References: map[string]envoyx.Ref{
"RoleID": {
// Providing resource type as plain text to reduce cross component references
ResourceType: "corteza::system:role",
Identifiers: envoyx.MakeIdentifiers(role.Value),
},
},
})
return nil
})
}
// unmarshalFlatRBACNode handles RBAC rules when they are provided on the root level
//
// Example:
//
// allow:
// role1:
// corteza::system/:
// - users.search
// - users.create
func unmarshalFlatRBACNode(n *yaml.Node, acc rbac.Access) (out envoyx.NodeSet, err error) {
// Handles role
return out, y7s.EachMap(n, func(role, perm *yaml.Node) error {
// Handles operations
return y7s.EachMap(perm, func(res, ops *yaml.Node) error {
// Handle operation (one RBAC rule per op)
return y7s.EachSeq(ops, func(op *yaml.Node) error {
out = append(out, &envoyx.Node{
Resource: &rbac.Rule{
Resource: res.Value,
Operation: op.Value,
Access: acc,
},
ResourceType: rbac.RuleResourceType,
References: envoyx.MergeRefs(
map[string]envoyx.Ref{"RoleID": {
// Providing resource type as plain text to reduce cross component references
ResourceType: "corteza::system:role",
Identifiers: envoyx.MakeIdentifiers(role.Value),
}},
envoyx.SplitResourceIdentifier(res.Value),
),
})
return nil
})
})
})
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// i18n unmarshal logic
// // // // // // // // // // // // // // // // // // // // // // // // //
func unmarshalLocaleNode(n *yaml.Node) (out envoyx.NodeSet, err error) {
err = y7s.EachMap(n, func(lang, loc *yaml.Node) error {
langTag := systemTypes.Lang{Tag: language.Make(lang.Value)}
return y7s.EachMap(loc, func(res, kv *yaml.Node) error {
return y7s.EachMap(kv, func(k, msg *yaml.Node) error {
out = append(out, &envoyx.Node{
Resource: &systemTypes.ResourceTranslation{
Resource: res.Value,
Lang: langTag,
K: k.Value,
Message: msg.Value,
},
// Providing resource type as plain text to reduce cross component references
ResourceType: "corteza::system:resource-translation",
References: envoyx.SplitResourceIdentifier(res.Value),
})
return nil
})
})
})
if err != nil {
return
}
for _, o := range out {
for _, r := range o.References {
if r.Scope.IsEmpty() {
continue
}
o.Scope = r.Scope
break
}
}
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Envoy config unmarshal logic
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d *auxYamlDoc) decodeEnvoyConfig(n *yaml.Node) (out envoyx.EnvoyConfig) {
y7s.EachMap(n, func(k, v *yaml.Node) (err error) {
switch strings.ToLower(k.Value) {
case "skipif", "skip":
return y7s.DecodeScalar(v, "decode skip if", &out.SkipIf)
case "onexisting", "mergealg":
out.MergeAlg = envoyx.CastMergeAlg(v.Value)
}
return nil
})
return
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utilities
// // // // // // // // // // // // // // // // // // // // // // // // //
func (d YamlDecoder) getReader(ctx context.Context, p envoyx.DecodeParams) (r io.Reader, err error) {
aux, ok := p.Params[paramsKeyStream]
if ok {
r, ok = aux.(io.Reader)
if ok {
return
}
}
// @todo consider adding support for managing files from a location
err = errors.Errorf("YAML decoder expects a stream conforming to io.Reader interface")
return
}
func (d *auxYamlDoc) getDocumentContext(n *yaml.Node) (dctx documentContext, err error) {
dctx = documentContext{
references: make(map[string]string),
}
err = y7s.EachMap(n, func(k, v *yaml.Node) error {
// @todo expand when needed. The previous implementation only supported
// namespaces on the root of the document.
if y7s.IsKind(v, yaml.ScalarNode) {
dctx.references[k.Value] = v.Value
}
return nil
})
return
}

View File

@@ -0,0 +1,305 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
"io"
"time"
"github.com/cortezaproject/corteza/server/pkg/envoyx"
"github.com/cortezaproject/corteza/server/pkg/y7s"
"gopkg.in/yaml.v3"
"github.com/pkg/errors"
{{- range .imports }}
"{{ . }}"
{{- end }}
)
{{ $rootRes := .resources }}
type (
// YamlEncoder is responsible for encoding Corteza resources into
// a YAML supported format
YamlEncoder struct{}
)
const (
paramsKeyWriter = "writer"
)
// Encode encodes the given Corteza resources into some YAML supported format
//
// Encoding should not do any additional processing apart from matching with
// dependencies and runtime validation
//
// Preparation runs validation, default value initialization, matching with
// already existing instances, ...
//
// The prepare function receives a set of nodes grouped by the resource type.
// This enables some batching optimization and simplifications when it comes to
// matching with existing resources.
//
// Prepare does not receive any placeholder nodes which are used solely
// for dependency resolution.
func (e YamlEncoder) Encode(ctx context.Context, p envoyx.EncodeParams, rt string, nodes envoyx.NodeSet, tt envoyx.Traverser) (err error) {
var (
out *yaml.Node
aux *yaml.Node
)
_ = aux
w, err := e.getWriter(p)
if err != nil {
return
}
switch rt {
{{- range .resources }}
{{- if or .envoy.omit .envoy.yaml.omitEncoder}}
{{continue}}
{{ end -}}
case types.{{.expIdent}}ResourceType:
aux, err = e.encode{{.expIdent}}s(ctx, p, nodes, tt)
if err != nil {
return
}
// Root level resources are always encoded as a map
out, err = y7s.AddMap(out, "{{.ident}}", aux)
if err != nil {
return
}
{{ end -}}
default:
out, err = e.encode(ctx, out, p, rt, nodes, tt)
if err != nil {
return
}
}
// Don't output nil values since that will produce broken yaml docs
if out == nil {
return
}
return yaml.NewEncoder(w).Encode(out)
}
{{- range .resources }}
{{- if .envoy.omit}}
{{continue}}
{{ end -}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Functions for resource {{.ident}}
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encode{{.expIdent}}s(ctx context.Context, p envoyx.EncodeParams, nodes envoyx.NodeSet, tt envoyx.Traverser) (out *yaml.Node, err error) {
var aux *yaml.Node
for _, n := range nodes {
aux, err = e.encode{{.expIdent}}(ctx, p, n, tt)
if err != nil {
return
}
out, err = y7s.AddSeq(out, aux)
if err != nil {
return
}
}
return
}
// encode{{.expIdent}} focuses on the specific resource invoked by the Encode method
func (e YamlEncoder) encode{{.expIdent}}(ctx context.Context, p envoyx.EncodeParams, node *envoyx.Node, tt envoyx.Traverser) (out *yaml.Node, err error) {
res := node.Resource.(*types.{{.expIdent}})
{{ $res := .expIdent }}
// Pre-compute some map values so we can omit error checking when encoding yaml nodes
{{ range .model.attributes -}}
{{- if eq .dal.type "Timestamp" -}}
{{- if .dal.nullable -}}
aux{{.expIdent}}, err := e.encodeTimestampNil(p, res.{{.expIdent}})
if err != nil {
return
}
{{- else -}}
aux{{.expIdent}}, err := e.encodeTimestamp(p, res.{{.expIdent}})
if err != nil {
return
}
{{- end -}}
{{- else if eq .dal.type "Ref" -}}
aux{{.expIdent}}, err := e.encodeRef(p, res.{{.expIdent}}, "{{.expIdent}}", node, tt)
if err != nil {
return
}
{{- else if .envoy.yaml.customEncoder -}}
aux{{.expIdent}}, err := e.encode{{$res}}{{.expIdent}}C(ctx, p, tt, node, res, res.{{.expIdent}})
if err != nil {
return
}
{{- end }}
{{end}}
out, err = y7s.AddMap(out,
{{ range .model.attributes -}}
{{- if .envoy.yaml.omitEncoder }}{{continue}}{{ end -}}
{{- if .envoy.yaml.customEncoder -}}
"{{.envoy.yaml.identKeyEncode}}", aux{{.expIdent}},
{{- else if eq .dal.type "Timestamp" -}}
{{- if .dal.nullable -}}
"{{.envoy.yaml.identKeyEncode}}", aux{{.expIdent}},
{{- else -}}
"{{.envoy.yaml.identKeyEncode}}", aux{{.expIdent}},
{{- end -}}
{{- else if eq .dal.type "Ref" -}}
"{{.envoy.yaml.identKeyEncode}}", aux{{.expIdent}},
{{- else -}}
"{{.envoy.yaml.identKeyEncode}}", res.{{.expIdent}},
{{- end }}
{{end}}
)
if err != nil {
return
}
// Handle nested resources
var aux *yaml.Node
_ = aux
{{ $a := . }}
{{- range $cmp := $rootRes }}
{{ if $cmp.envoy.omit }}{{continue}}{{ end }}
{{if not $cmp.parents}}{{continue}}{{end}}
{{/*
Only handle resources where the current resource would appear last
in the list of parents.
Since parents are ordered by _importance_ this removes the danger of
multiple parents decoding the same resource.
*/}}
{{ $p := (index $cmp.parents (sub (len $cmp.parents) 1)) }}
{{ if (eq $p.handle $a.ident) }}
{{ if eq $cmp.ident "moduleField"}}
// When processing module fields, we need to filter out the ones that
// don't belong to this module
//
// @todo offload this to dependency resolution; this is a hack
children := tt.ChildrenForResourceType(node, types.ModuleFieldResourceType)
var proc envoyx.NodeSet
selfRef := node.ToRef()
for _, c := range children {
if c.References["ModuleID"].Equals(selfRef) {
proc = append(proc, c)
}
}
aux, err = e.encodeModuleFields(ctx, p, proc, tt)
{{- else }}
aux, err = e.encode{{$cmp.expIdent}}s(ctx, p, tt.ChildrenForResourceType(node, types.{{$cmp.expIdent}}ResourceType), tt)
{{- end }}
if err != nil {
return
}
out, err = y7s.AddMap(out,
"{{$cmp.ident}}", aux,
)
if err != nil {
return
}
{{ end }}
{{- end }}
{{- range .envoy.yaml.extendedResourceEncoders }}
aux, err = e.encode{{.expIdent}}s(ctx, p, tt.ChildrenForResourceType(node, types.{{.expIdent}}ResourceType), tt)
if err != nil {
return
}
out, err = y7s.AddMap(out,
"{{.identKey}}", aux,
)
if err != nil {
return
}
{{- end }}
return
}
{{ end -}}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Encoding utils
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) encodeTimestamp(p envoyx.EncodeParams, t time.Time) (any, error) {
if t.IsZero() {
return nil, nil
}
tz := p.Encoder.PreferredTimezone
if tz != "" {
tzL, err := time.LoadLocation(tz)
if err != nil {
return nil, err
}
t = t.In(tzL)
}
ly := p.Encoder.PreferredTimeLayout
if ly == "" {
ly = time.RFC3339
}
return t.Format(ly), nil
}
func (e YamlEncoder) encodeTimestampNil(p envoyx.EncodeParams, t *time.Time) (any, error) {
if t == nil { return nil, nil }
return e.encodeTimestamp(p, *t)
}
func (e YamlEncoder) encodeRef(p envoyx.EncodeParams, id uint64, field string, node *envoyx.Node, tt envoyx.Traverser) (any, error) {
parent := tt.ParentForRef(node, node.References[field])
// @todo should we panic instead?
// for now gracefully fallback to the ID
if parent == nil {
return id, nil
}
return parent.Identifiers.FriendlyIdentifier(), nil
}
// // // // // // // // // // // // // // // // // // // // // // // // //
// Utility functions
// // // // // // // // // // // // // // // // // // // // // // // // //
func (e YamlEncoder) getWriter(p envoyx.EncodeParams) (out io.Writer, err error) {
aux, ok := p.Params[paramsKeyWriter]
if ok {
out, ok = aux.(io.Writer)
if ok {
return
}
}
// @todo consider adding support for managing files from a location
err = errors.Errorf("YAML encoder expects a writer conforming to io.Writer interface")
return
}
func safeParentIdentifier(tt envoyx.Traverser, n *envoyx.Node, ref envoyx.Ref) (out string) {
aux := tt.ParentForRef(n, ref)
if aux == nil {
return ref.Identifiers.FriendlyIdentifier()
}
return aux.Identifiers.FriendlyIdentifier()
}

View File

@@ -0,0 +1,5 @@
// This file is auto-generated.
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
//

View File

@@ -0,0 +1,236 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
{{- range .imports }}
{{ . }}
{{- end }}
"github.com/cortezaproject/corteza/server/pkg/actionlog"
intAuth "github.com/cortezaproject/corteza/server/pkg/auth"
"github.com/cortezaproject/corteza/server/pkg/errors"
"github.com/cortezaproject/corteza/server/pkg/filter"
"github.com/cortezaproject/corteza/server/pkg/locale"
"github.com/cortezaproject/corteza/server/pkg/options"
"github.com/cortezaproject/corteza/server/store"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
"golang.org/x/text/language"
)
type (
localeAccessControl interface {
CanManageResourceTranslations(ctx context.Context) bool
}
resourceTranslationsManager struct {
actionlog actionlog.Recorder
locale locale.Resource
store store.Storer
ac localeAccessController
}
localeAccessController interface {
CanManageResourceTranslations(context.Context) bool
}
ResourceTranslationsManagerService interface {
{{- range .resources }}
{{ .expIdent }}(ctx context.Context, {{ range .references }}{{ .param }} uint64, {{ end }}) (locale.ResourceTranslationSet, error)
{{- end }}
Upsert(context.Context, locale.ResourceTranslationSet) error
Locale() locale.Resource
}
)
var ErrNotAllowedToManageResourceTranslations = errors.Unauthorized("not allowed to manage resource translations")
func ResourceTranslationsManager(ls locale.Resource) *resourceTranslationsManager {
return &resourceTranslationsManager{
actionlog: DefaultActionlog,
store: DefaultStore,
ac: DefaultAccessControl,
locale: ls,
}
}
func (svc resourceTranslationsManager) Upsert(ctx context.Context, rr locale.ResourceTranslationSet) (err error) {
// User is allowed to manage resource translations when:
// - managed resource translation strings are all for default language
// or
// - user is allowed to manage resource translations
err = store.Tx(ctx, svc.store, func(ctx context.Context, s store.Storer) (err error) {
if rr.ContainsForeign(svc.Locale().Default().Tag) {
if !svc.ac.CanManageResourceTranslations(ctx) {
return ErrNotAllowedToManageResourceTranslations
}
}
for _, r := range rr {
r.Msg = locale.SanitizeMessage(r.Msg)
}
// @todo validation
me := intAuth.GetIdentityFromContext(ctx)
// - group by resource
localeByRes := make(map[string]locale.ResourceTranslationSet)
for _, r := range rr {
localeByRes[r.Resource] = append(localeByRes[r.Resource], r)
}
// - for each resource, fetch the current state
sysLocale := make(systemTypes.ResourceTranslationSet, 0, len(rr))
for res, rr := range localeByRes {
current, _, err := store.SearchResourceTranslations(ctx, s, systemTypes.ResourceTranslationFilter{
Resource: res,
Deleted: filter.StateInclusive,
})
if err != nil {
return err
}
// get deltas and prepare upsert accordingly
aux := current.New(rr)
aux.Walk(func(cc *systemTypes.ResourceTranslation) error {
cc.ID = nextID()
cc.CreatedAt = *now()
cc.CreatedBy = me.Identity()
return nil
})
sysLocale = append(sysLocale, aux...)
for _, diff := range current.Old(rr) {
old := diff[0]
new := diff[1]
// soft delete; restore old message
if new.Message == "" {
new.Message = old.Message
if new.DeletedAt == nil {
new.DeletedAt = now()
new.DeletedBy = me.Identity()
}
} else {
new.UpdatedAt = now()
new.UpdatedBy = me.Identity()
new.DeletedAt = nil
new.DeletedBy = 0
}
sysLocale = append(sysLocale, new)
}
}
err = store.UpsertResourceTranslation(ctx, s, sysLocale...)
if err != nil {
return err
}
return nil
})
if err != nil {
return err
}
// Reload ALL resource translations
// @todo we could probably do this more selectively and refresh only updated resources?
return locale.Global().ReloadResourceTranslations(ctx)
}
func (svc resourceTranslationsManager) Locale() locale.Resource {
return svc.locale
}
{{- range .resources }}
func (svc resourceTranslationsManager) {{ .expIdent }}(ctx context.Context, {{ range .references }}{{ .param }} uint64, {{ end }}) (locale.ResourceTranslationSet, error) {
var (
err error
out locale.ResourceTranslationSet
res *types.{{ .expIdent }}
)
res, err = svc.load{{ .expIdent }}(ctx, svc.store, {{ range .references }}{{ .param }}, {{ end }})
if err != nil {
return nil, err
}
{{ $customHandlerExist := false }}
{{range .keys}}
{{if not .customHandler}}
{{ $customHandlerExist = true }}
{{end}}
{{end}}
{{ if $customHandlerExist }}
var k types.LocaleKey
for _, tag := range svc.locale.Tags() {
{{- range .keys}}
{{- if not .customHandler }}
k = types.{{ .struct }}
out = append(out, &locale.ResourceTranslation{
Resource: res.ResourceTranslation(),
Lang: tag.String(),
Key: k.Path,
Msg: svc.locale.TResourceFor(tag, res.ResourceTranslation(), k.Path),
})
{{ end }}
{{- end}}
}
{{ end }}
{{ if .extended }}
tmp, err := svc.{{ .ident }}Extended(ctx, res)
return append(out, tmp...), err
{{- else }}
return out, nil
{{- end }}
}
{{- end }}
func updateTranslations(ctx context.Context, ac localeAccessControl, lsvc ResourceTranslationsManagerService, tt ...*locale.ResourceTranslation) error {
if lsvc == nil || lsvc.Locale() == nil || lsvc.Locale().Default() == nil {
// gracefully handle partial initializations
return nil
}
var (
// assuming options will not change after start
contentLang = lsvc.Locale().Default().Tag
)
if options.Locale().ResourceTranslationsEnabled {
contentLang = locale.GetContentLanguageFromContext(ctx)
// Resource translations enabled
if contentLang == language.Und {
// If no content-language meta (HTTP header) info was
// used, do not run update translations - we do not know
// what is the language that we're sending in
return nil
}
if !lsvc.Locale().SupportedLang(contentLang) {
// unsupported language
return errors.InvalidData("unsupported language")
}
if !ac.CanManageResourceTranslations(ctx) {
return errors.Unauthorized("not allowed to manage resource translations")
}
}
locale.ResourceTranslationSet(tt).SetLanguage(contentLang)
if err := lsvc.Upsert(ctx, tt); err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,127 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"fmt"
"strconv"
"github.com/cortezaproject/corteza/server/pkg/locale"
)
type (
LocaleKey struct {
Name string
Resource string
Path string
CustomHandler string
}
)
// Types and stuff
const (
{{- range .resources }}
{{ .const }} = "{{ .type }}"
{{- end }}
)
var (
// @todo can we remove LocaleKey struct for string constant?
{{- range .resources }}
{{- range .keys }}
{{ .struct }} = LocaleKey{ Path: {{ printf "%q" .path }} }
{{- end }}
{{- end }}
)
{{- range .resources }}
// ResourceTranslation returns string representation of Locale resource for {{ .expIdent }} by calling {{ .expIdent }}ResourceTranslation fn
//
// Locale resource is in "{{ .type }}/..." format
//
// This function is auto-generated
func (r {{ .expIdent }}) ResourceTranslation() string {
return {{ .expIdent }}ResourceTranslation({{ range .references }}r.{{ .refField }},{{ end }})
}
// {{ .expIdent }}ResourceTranslation returns string representation of Locale resource for {{ .expIdent }}
//
// Locale resource is in the {{ .type }}/{{- if .references }}...{{ end }} format
//
// This function is auto-generated
func {{ .expIdent }}ResourceTranslation({{ range .references }}{{ .refField }} uint64,{{ end }}) string {
{{- if .references }}
cpts := []interface{{"{}"}}{
{{ .expIdent }}ResourceTranslationType,
{{- range .references }}
strconv.FormatUint({{ .refField }}, 10),
{{- end }}
}
return fmt.Sprintf({{ .expIdent }}ResourceTranslationTpl(), cpts...)
{{- end }}
}
func {{ .expIdent }}ResourceTranslationTpl() string {
{{- if .references }}
return "%s
{{- range .references }}/%s{{- end }}"
{{- else }}
return "%s"
{{- end }}
}
func (r *{{ .expIdent }}) DecodeTranslations(tt locale.ResourceTranslationIndex) {
{{- $decodeFuncExist := false }}
{{- range .keys }}
{{- if not .decodeFunc }}
{{- $decodeFuncExist = true }}
{{- end }}
{{- end}}
{{- if $decodeFuncExist }}
var aux *locale.ResourceTranslation
{{- end }}
{{- range .keys }}
{{ if .decodeFunc }}
{{ if not .extended }}
r.{{ .decodeFunc }}(tt)
{{- end}}
{{ else }}
if aux = tt.FindByKey({{ .struct }}.Path); aux != nil {
r.{{ .fieldPath }} = aux.Msg
}
{{- end}}
{{- end}}
{{- if .extended }}
r.decodeTranslations(tt)
{{- end }}
}
func (r *{{ .expIdent }}) EncodeTranslations() (out locale.ResourceTranslationSet) {
out = locale.ResourceTranslationSet{}
{{- range .keys }}
{{ if .encodeFunc }}
{{ if not .extended }}
out = append(out, r.{{ .encodeFunc }}()...)
{{- end}}
{{ else }}
out = append(out, &locale.ResourceTranslation{
Resource: r.ResourceTranslation(),
Key: {{ .struct }}.Path,
Msg: locale.SanitizeMessage(r.{{ .fieldPath }}),
})
{{- end}}
{{- end}}
{{- if .extended }}
out = append(out, r.encodeTranslations()...)
{{- end }}
return out
}
{{- end }}

View File

@@ -0,0 +1,54 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
{{ if .imports }}
import (
{{- range .imports }}
{{ . }}
{{- end }}
)
{{- end }}
type (
{{ range .groups }}
{{ .struct }} struct {
{{- range .options }}
{{ .expIdent }} {{ .type }} `env:"{{ .env }}"`
{{- end }}
}
{{ end }}
)
{{ range .groups }}
// {{ .func }} initializes and returns a {{ .struct }} with default values
//
// This function is auto-generated
func {{ .func }}() (o *{{ .struct }}) {
o = &{{ .struct }}{
{{- range .options }}
{{- if or .default }}
{{ .expIdent }}: {{ .default }},
{{- end }}
{{- end }}
}
// Custom defaults
func(o interface{}) {
if def, ok := o.(interface{ Defaults() }); ok {
def.Defaults()
}
}(o)
fill(o)
// Custom cleanup
func(o interface{}) {
if def, ok := o.(interface{ Cleanup() }); ok {
def.Cleanup()
}
}(o)
return
}
{{ end }}

View File

@@ -0,0 +1,388 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"fmt"
"github.com/spf13/cast"
"strings"
"context"
"github.com/cortezaproject/corteza/server/store"
"github.com/cortezaproject/corteza/server/pkg/rbac"
"github.com/cortezaproject/corteza/server/pkg/actionlog"
systemTypes "github.com/cortezaproject/corteza/server/system/types"
internalAuth "github.com/cortezaproject/corteza/server/pkg/auth"
{{- range .imports }}
{{ . }}
{{- end }}
)
type (
rbacService interface {
Can(rbac.Session, string, rbac.Resource) bool
Trace(rbac.Session, string, rbac.Resource) *rbac.Trace
Grant(context.Context, ...*rbac.Rule) error
FindRulesByRoleID(roleID uint64) (rr rbac.RuleSet)
}
accessControl struct {
actionlog actionlog.Recorder
store store.Storer
rbac rbacService
}
)
func AccessControl(s store.Storer) *accessControl {
return &accessControl{
store: s,
rbac: rbac.Global(),
actionlog: DefaultActionlog,
}
}
func (svc accessControl) can(ctx context.Context, op string, res rbac.Resource) bool {
return svc.rbac.Can(rbac.ContextToSession(ctx), op, res)
}
// Effective returns a list of effective permissions for all given resource
//
// This function is auto-generated
func (svc accessControl) Effective(ctx context.Context, rr ... rbac.Resource) (ee rbac.EffectiveSet) {
for _, res := range rr {
r := res.RbacResource()
for op := range rbacResourceOperations(r) {
ee.Push(r, op, svc.can(ctx, op, res))
}
}
return
}
// Evaluate returns a list of permissions evaluated for the given user/roles combo
//
// This function is auto-generated
func (svc accessControl) Trace(ctx context.Context, userID uint64, roles []uint64, rr ...string) (ee []*rbac.Trace, err error) {
// Reusing the grant permission since this is who the feature is for
if !svc.CanGrant(ctx) {
// @todo should be altered to check grant permissions PER resource
return nil, AccessControlErrNotAllowedToSetPermissions()
}
var (
resource rbac.Resource
resources []rbac.Resource
members systemTypes.RoleMemberSet
)
if len(rr) > 0 {
resources = make([]rbac.Resource, 0, len(rr))
for _, r := range rr {
if err = rbacResourceValidator(r); err != nil {
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
}
resource, err = svc.resourceLoader(ctx, r)
if err != nil {
return
}
resources = append(resources, resource)
}
} else {
resources = svc.Resources()
}
// User ID specified, load its roles
if userID != 0 {
if len(roles) > 0 {
// should be prevented on the client
return nil, fmt.Errorf("userID and roles are mutually exclusive")
}
members, _, err = store.SearchRoleMembers(ctx, svc.store, systemTypes.RoleMemberFilter{Resource: fmt.Sprintf("corteza::system:user/%d", userID)})
if err != nil {
return nil, err
}
for _, m := range members {
roles = append(roles, m.RoleID)
}
for _, r := range internalAuth.AuthenticatedRoles() {
roles = append(roles, r.ID)
}
}
if len(roles) == 0 {
// should be prevented on the client
return nil, fmt.Errorf("no roles specified")
}
session := rbac.ParamsToSession(ctx, userID, roles...)
for _, res := range resources {
r := res.RbacResource()
for op := range rbacResourceOperations(r) {
ee = append(ee, svc.rbac.Trace(session, op, res))
}
}
return
}
// Resources returns list of resources
//
// This function is auto-generated
func (svc accessControl) Resources() []rbac.Resource {
return []rbac.Resource{
{{- range .resources }}
rbac.NewResource({{ .resFunc }}({{ range .references }}0,{{ end }})),
{{- end }}
}
}
// List returns list of operations on all resources
//
// This function is auto-generated
func (svc accessControl) List() (out []map[string]string) {
def := []map[string]string{
{{- range .operations }}
{
"type": {{ .const }},
"any": {{ .resFunc }}({{ range .references }}0,{{ end }}),
"op": {{ printf "%q" .op }},
},
{{- end }}
}
func(svc interface{}) {
if svc, is := svc.(interface{}).(interface{ list() []map[string]string }); is {
def = append(def, svc.list()...)
}
}(svc)
return def
}
// Grant applies one or more RBAC rules
//
// This function is auto-generated
func (svc accessControl) Grant(ctx context.Context, rr ...*rbac.Rule) error {
if !svc.CanGrant(ctx) {
// @todo should be altered to check grant permissions PER resource
return AccessControlErrNotAllowedToSetPermissions()
}
for _, r := range rr {
err := rbacResourceValidator(r.Resource, r.Operation)
if err != nil {
return err
}
}
if err := svc.rbac.Grant(ctx, rr...); err != nil {
return AccessControlErrGeneric().Wrap(err)
}
svc.logGrants(ctx, rr)
return nil
}
// This function is auto-generated
func (svc accessControl) logGrants(ctx context.Context, rr []*rbac.Rule) {
if svc.actionlog == nil {
return
}
for _, r := range rr {
g := AccessControlActionGrant(&accessControlActionProps{r})
g.log = r.String()
g.resource = r.Resource
svc.actionlog.Record(ctx, g.ToAction())
}
}
// FindRules find all rules based on filters
//
// This function is auto-generated
func (svc accessControl) FindRules(ctx context.Context, roleID uint64, rr ...string) (out rbac.RuleSet, err error) {
if !svc.CanGrant(ctx) {
return nil, AccessControlErrNotAllowedToSetPermissions()
}
out, err = svc.FindRulesByRoleID(ctx, roleID)
if err != nil {
return
}
var resources []rbac.Resource
if len(rr) > 0 {
resources = make([]rbac.Resource, 0, len(rr))
for _, r := range rr {
if err = rbacResourceValidator(r); err != nil {
return nil, fmt.Errorf("can not use resource %q: %w", r, err)
}
resources = append(resources, rbac.NewResource(r))
}
} else {
resources = svc.Resources()
}
return out.FilterResource(resources...), nil
}
// FindRulesByRoleID find all rules for a specific role
//
// This function is auto-generated
func (svc accessControl) FindRulesByRoleID(ctx context.Context, roleID uint64) (rbac.RuleSet, error) {
if !svc.CanGrant(ctx) {
return nil, AccessControlErrNotAllowedToSetPermissions()
}
return svc.rbac.FindRulesByRoleID(roleID), nil
}
{{- range .operations }}
// {{ .checkFuncName }} checks if current user can {{ lower .description }}
//
// This function is auto-generated
func (svc accessControl) {{ .checkFuncName }}(ctx context.Context{{ if not .component }}, r *{{ .goType }}{{ end }}) bool {
{{- if .component }}r := &{{ .goType }}{}{{ end }}
return svc.can(ctx, {{ printf "%q" .op }}, r)
}
{{- end }}
// rbacResourceValidator validates known component's resource by routing it to the appropriate validator
//
// This function is auto-generated
func rbacResourceValidator(r string, oo ...string) error {
switch rbac.ResourceType(r) {
{{- range .validation }}
case {{ .const }}:
return {{ .funcName }}(r, oo...)
{{- end }}
}
return fmt.Errorf("unknown resource type %q", r)
}
// resourceLoader loads resource from store
//
// # Notes
// Function assumes existence of loader functions for all resource types
//
// This function is auto-generated
func (svc accessControl) resourceLoader(ctx context.Context, resource string) (rbac.Resource, error) {
var (
hasWildcard = false
resourceType, ids = rbac.ParseResourceID(resource)
)
for _, id := range ids {
if id == 0 {
hasWildcard = true
break
}
}
switch rbac.ResourceType(resourceType) {
{{- range .loaders }}
case {{ .const }}:
if hasWildcard {
return rbac.NewResource({{ .resFunc }}({{ range $i := .refIndex }}ids[{{ $i }}],{{ end }})), nil
}
return {{ .funcName }}(ctx, svc.store {{ range $i := .refIndex }}, ids[{{ $i }}]{{ end }})
{{- end }}
case types.ComponentResourceType:
return &types.Component{}, nil
}
_ = ids
return nil, fmt.Errorf("unknown resource type %q", resourceType)
}
// rbacResourceOperations returns defined operations for a requested resource
//
// This function is auto-generated
func rbacResourceOperations(r string) map[string]bool {
switch rbac.ResourceType(r) {
{{- range .validation }}
case {{ .const }}:
return map[string]bool{
{{- range .operations }}
{{ printf "%q" . }}: true,
{{- end }}
}
{{- end }}
}
return nil
}
{{- range .validation }}
// {{ .funcName }} checks validity of RBAC resource and operations
//
// # Notes
// Can be called without operations to check for validity of resource string only
//
// This function is auto-generated
func {{ .funcName }}(r string, oo ...string) error {
{{- if .references }}
if !strings.HasPrefix(r, {{ .const }}) {
// expecting resource to always include path
return fmt.Errorf("invalid resource type")
}
{{ else }}
if r != {{ .const }} + "/" {
// expecting resource to always include path
return fmt.Errorf("invalid component resource, expecting " + {{ .const }} + "/")
}
{{ end }}
defOps := rbacResourceOperations(r)
for _, o := range oo {
if !defOps[o] {
return fmt.Errorf("invalid operation '%s' for {{ .label }} resource", o)
}
}
{{ if .references }}
const sep = "/"
var (
pp = strings.Split(strings.Trim(r[len({{ .const }}):], sep), sep)
prc = []string{
{{- range .references }}
{{ printf "%q" . }},
{{- end }}
}
)
if len(pp) != len(prc) {
return fmt.Errorf("invalid resource path structure")
}
for i := 0; i < len(pp); i++ {
if pp[i] != "*" {
if i > 0 && pp[i-1] == "*" {
return fmt.Errorf("invalid path wildcard level (%d) for {{ .label }} resource", i)
}
if _, err := cast.ToUint64E(pp[i]); err != nil {
return fmt.Errorf("invalid reference for %s: '%s'", prc[i], pp[i])
}
}
}
{{- end }}
return nil
}
{{- end }}

View File

@@ -0,0 +1,71 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"fmt"
"strconv"
)
type (
// Component struct serves as a virtual resource type for the {{ .cmpIdent }} component
//
// This struct is auto-generated
Component struct {}
)
var (
{{/*
making sure that generated code does not break
when these packages are not used
*/}}
_ = fmt.Printf
_ = strconv.FormatUint
)
{{- range .types }}
// RbacResource returns string representation of RBAC resource for {{ .goType }} by calling {{ .resFunc }} fn
//
// RBAC resource is in the {{ .type }}/... format
//
// This function is auto-generated
func (r {{ .goType }}) RbacResource() string {
return {{ .resFunc }}({{ if not .component }}{{ range .references }}r.{{ .refField }},{{ end }}{{ end }})
}
// {{ .resFunc }} returns string representation of RBAC resource for {{ .goType }}
//
// RBAC resource is in the {{ .type }}/{{- if .references }}...{{ end }} format
//
// This function is auto-generated
func {{ .resFunc }}({{ if not .component }}{{ range .references }}{{ .param }} uint64,{{ end }}{{ end }}) string {
{{- if .references }}
cpts := []interface{{"{}"}}{{"{"}}{{ .goType }}ResourceType{{"}"}}
{{- range .references }}
if {{ .param }} != 0 {
cpts = append(cpts, strconv.FormatUint({{ .param }}, 10))
} else {
cpts = append(cpts, "*")
}
{{ end }}
return fmt.Sprintf({{ .tplFunc }}(), cpts...)
{{- else }}
return {{ .goType }}ResourceType + "/"
{{- end }}
}
func {{ .tplFunc }}() string {
{{- if .references }}
return "%s
{{- range .references }}/%s{{- end }}"
{{- else }}
return "%s"
{{- end }}
}
{{- end }}

View File

@@ -0,0 +1,140 @@
package store
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
"go.uber.org/zap"
{{- range $path, $alias := .imports }}
{{ $alias }} {{ printf "%q" $path }}
{{- end }}
"github.com/cortezaproject/corteza/server/pkg/dal"
"github.com/cortezaproject/corteza/server/pkg/locale"
"golang.org/x/text/language"
)
{{ define "extraArgs" -}}
{{/*This is temporary solution until we properly implement Compose Record Store*/}}
{{- if eq . "composeRecord" }}mod *composeType.Module, {{ end -}}
{{- end }}
{{ define "extraParams" -}}
{{/*This is temporary solution until we properly implement Compose Record Store*/}}
{{- if eq . "composeRecord" }}mod, {{ end -}}
{{- end }}
type (
// Storer interface combines interfaces of all supported store interfaces
Storer interface {
// SetLogger sets new logging facility
//
// Store facility should fallback to logger.Default when no logging facility is set
//
// Intentionally closely coupled with Zap logger since this is not some public lib
// and it's highly unlikely we'll support different/multiple logging "backend"
SetLogger(*zap.Logger)
// Returns underlying store as DAL connection
ToDalConn() dal.Connection
// Tx is a transaction handler
Tx(context.Context, func(context.Context, Storer) error) error
// Upgrade store's schema to the latest version
Upgrade(context.Context) error
Healthcheck(context.Context) error
{{- range .types }}
{{ .expIdentPlural }}
{{- end }}
}
{{ range .types }}
{{ .expIdentPlural }} interface {
Search{{ .expIdentPlural }}(ctx context.Context, {{ template "extraArgs" .ident }} f {{ .goFilterType }}) ({{ .goSetType }}, {{ .goFilterType }}, error)
Create{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error
Update{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error
Upsert{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error
Delete{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error
{{ if .api.deleteByPK.expFnIdent }}
{{ .api.deleteByPK.expFnIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} {{ range .api.deleteByPK.attributes }}{{ .ident }} {{ .goType }},{{ end }}) error
{{- end }}
Truncate{{ .expIdentPlural }}(ctx context.Context, {{ template "extraArgs" .ident }}) error
{{- range .api.lookups }}
{{ .expFnIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} {{ range .args }}{{ .ident }} {{ .goType }}, {{ end }}) (*{{ .returnType }}, error)
{{- end }}
{{- range .api.functions }}
{{ .expFnIdent }}(ctx context.Context, {{ range .args }}{{ .ident }} {{ if .spread}}...{{ end }}{{ .goType }}, {{ end }}) ({{ range .return }}{{ . }}, {{ end }} error)
{{- end }}
}
{{ end }}
)
{{- range .types }}
// Search{{ .expIdentPlural }} returns all matching {{ .expIdentPlural }} from store
//
// This function is auto-generated
func Search{{ .expIdentPlural }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }} f {{ .goFilterType }}) ({{ .goSetType }}, {{ .goFilterType }}, error) {
return s.Search{{ .expIdentPlural }}(ctx, {{ template "extraParams" .ident }} f)
}
// Create{{ .expIdent }} creates one or more {{ .expIdentPlural }} in store
//
// This function is auto-generated
func Create{{ .expIdent }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error {
return s.Create{{ .expIdent }}(ctx, {{ template "extraParams" .ident }}rr...)
}
// Update{{ .expIdent }} updates one or more (existing) {{ .expIdentPlural }} in store
//
// This function is auto-generated
func Update{{ .expIdent }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error {
return s.Update{{ .expIdent }}(ctx, {{ template "extraParams" .ident }}rr...)
}
// Upsert{{ .expIdent }} creates new or updates existing one or more {{ .expIdentPlural }} in store
//
// This function is auto-generated
func Upsert{{ .expIdent }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error {
return s.Upsert{{ .expIdent }}(ctx, {{ template "extraParams" .ident }}rr...)
}
// Delete{{ .expIdent }} deletes one or more {{ .expIdentPlural }} from store
//
// This function is auto-generated
func Delete{{ .expIdent }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) error {
return s.Delete{{ .expIdent }}(ctx, {{ template "extraParams" .ident }}rr...)
}
{{ if .api.deleteByPK.expFnIdent }}
// Delete{{ .expIdent }}ByID deletes one or more {{ .expIdentPlural }} from store
//
// This function is auto-generated
func {{ .api.deleteByPK.expFnIdent }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }} {{ range .api.deleteByPK.attributes }}{{ .ident }} {{ .goType }},{{ end }}) error {
return s.{{ .api.deleteByPK.expFnIdent }}(ctx, {{ template "extraParams" .ident }}{{ range .api.deleteByPK.attributes }}{{ .ident }},{{ end }})
}
{{ end }}
// Truncate{{ .expIdentPlural }} Deletes all {{ .expIdentPlural }} from store
//
// This function is auto-generated
func Truncate{{ .expIdentPlural }}(ctx context.Context, s {{ .expIdentPlural }}, {{ template "extraArgs" .ident }}) error {
return s.Truncate{{ .expIdentPlural }}(ctx, {{ template "extraParams" .ident }})
}
{{- range .api.lookups }}
{{ if .description }}{{ .description }}{{ end }}
//
// This function is auto-generated
func {{ .expFnIdent }}(ctx context.Context, s {{ .expStoreIdent }}, {{ template "extraArgs" .ident }} {{ range .args }}{{ .ident }} {{ .goType }}, {{ end }}) (*{{ .returnType }}, error) {
return s.{{ .expFnIdent }}(ctx, {{ template "extraParams" .ident }}{{ range .args }}{{ .ident }}, {{ end }})
}
{{- end }}
{{- range .api.functions }}
{{ if .description }}{{ .description }}{{ end }}
//
// This function is auto-generated
func {{ .expFnIdent }}(ctx context.Context, s {{ .expStoreIdent }}, {{ range .args }}{{ .ident }} {{ if .spread}}...{{ end }}{{ .goType }}, {{ end }}) ({{ range .return }}{{ . }}, {{ end }} error) {
return s.{{ .expFnIdent }}(ctx, {{ range .args }}{{ .ident }}{{ if .spread}}...{{ end }}, {{ end }})
}
{{- end }}
{{- end }}

View File

@@ -0,0 +1,57 @@
package rdbms
{{ template "gocode/header-gentext.tpl" }}
import (
"time"
"github.com/cortezaproject/corteza/server/pkg/expr"
{{- range $path, $alias := .imports }}
{{ $alias }} {{ printf "%q" $path }}
{{- end }}
)
type (
{{ range .types }}
// {{ .auxIdent }} is an auxiliary structure used for transporting to/from RDBMS store
{{ .auxIdent }} struct {
{{ range .auxStruct }}
{{ .expIdent }} {{ .goType }} {{ printf "`db:%q`" .name }}
{{- end }}
}
{{ end }}
)
{{- range .types }}
// encodes {{ .expIdent }} to {{ .auxIdent }}
//
// This function is auto-generated
func (aux *{{ .auxIdent }}) encode(res *{{ .goType }}) (_ error) {
{{- range .auxStruct }}
aux.{{ .expIdent }} = res.{{ .expIdent }}
{{- end }}
return
}
// decodes {{ .expIdent }} from {{ .auxIdent }}
//
// This function is auto-generated
func (aux {{ .auxIdent }}) decode() (res *{{ .goType }}, _ error) {
res = new({{ .goType }})
{{- range .auxStruct }}
res.{{ .expIdent }} = aux.{{ .expIdent }}
{{- end }}
return
}
// scans row and fills {{ .auxIdent }} fields
//
// This function is auto-generated
func (aux *{{ .auxIdent }})scan(row scanner) (error) {
return row.Scan(
{{- range .auxStruct }}
&aux.{{ .expIdent }},
{{- end }}
)
}
{{ end }}

View File

@@ -0,0 +1,113 @@
package rdbms
{{ template "gocode/header-gentext.tpl" }}
import (
"strings"
{{- range $path, $alias := .imports }}
{{ $alias }} {{ printf "%q" $path }}
{{- end }}
"github.com/cortezaproject/corteza/server/store/adapters/rdbms/drivers"
"github.com/doug-martin/goqu/v9"
)
type (
// extendedFilters allows special per-resource
// filters to be attached to store
//
// when optional filter is set, generated filter function is NOT called automatically
// (but can be called from the optional filter)
extendedFilters struct {
// Filter extensions for search/query functions
{{ range .types }}
// optional {{ .ident }} filter function called after the generated function
{{ .expIdent }} func(*Store, {{ .goFilterType }}) ([]goqu.Expression, {{ .goFilterType }}, error)
{{ end }}
}
)
{{- range .types }}
// {{ .expIdent }}Filter returns logical expressions
//
// This function is called from Store.Query{{ .expIdentPlural }}() and can be extended
// by setting Store.Filters.{{ .expIdent }}. Extension is called after all expressions
// are generated and can choose to ignore or alter them.
//
// This function is auto-generated
func {{ .expIdent }}Filter(d drivers.Dialect, f {{ .goFilterType }})(ee []goqu.Expression, _ {{ .goFilterType }}, err error) {
{{ range .filter.byNilState }}
if expr := stateNilComparison(d,{{ printf "%q" .storeIdent }}, f.{{ .expIdent }}); expr != nil {
ee = append(ee, expr)
}
{{ end }}
{{ range .filter.byFalseState }}
if expr := stateFalseComparison(d,{{ printf "%q" .storeIdent }}, f.{{ .expIdent }}); expr != nil {
ee = append(ee, expr)
}
{{ end }}
{{ range .filter.byValue }}
{{ if eq .goType "string" }}
if val := strings.TrimSpace(f.{{ .expIdent }}); len(val) > 0 {
ee = append(ee, goqu.C({{ printf "%q" .storeIdent }}).Eq(f.{{ .expIdent }}))
}
{{ else if eq .goType "[]string" }}
if ss := trimStringSlice(f.{{ .expIdent }}); len(ss) > 0 {
ee = append(ee, goqu.C({{ printf "%q" .storeIdent }}).In(ss))
}
{{ else if eq .goType "bool" }}
if f.{{ .expIdent }} {
ee = append(ee, goqu.C({{ printf "%q" .storeIdent }}).IsTrue())
}
{{ else if eq .goType "uint64" }}
if f.{{ .expIdent }} > 0 {
ee = append(ee, goqu.C({{ printf "%q" .storeIdent }}).Eq(f.{{ .expIdent }}))
}
{{ else if eq .goType "[]uint64" }}
if len(f.{{ .expIdent }}) > 0 {
ee = append(ee, goqu.C({{ printf "%q" .storeIdent }}).In(f.{{ .expIdent }}))
}
{{ else }}
// @todo codegen warning: filtering by {{ .expIdent }} ({{ .goType }}) not supported,
// see rdbms.go.tpl and add an exception
{{ end }}
{{ end }}
{{ if .filter.byLabel }}
if len(f.LabeledIDs) > 0 {
ee = append(ee, goqu.I("id").In(f.LabeledIDs))
}
{{ end }}
{{ if .filter.byFlag }}
if len(f.FlaggedIDs) > 0 {
ee = append(ee, goqu.I("id").In(f.FlaggedIDs))
}
{{ end }}
{{ if .filter.query }}
if f.Query != "" {
ee = append(ee, goqu.Or(
{{- range .filter.query }}
goqu.C({{ printf "%q" .storeIdent }}).ILike("%" + f.Query + "%"),
{{- end }}
))
}
{{ end }}
return ee, f, err
}
{{ end }}
// trimStringSlice is a utility to trim all of the string slice elements and omit empty ones
func trimStringSlice(in []string) []string {
out := make([]string, 0, len(in))
for _, s := range in {
if t := strings.TrimSpace(s); len(t) > 0 {
out = append(out, t)
}
}
return out
}

View File

@@ -0,0 +1,114 @@
package rdbms
{{ template "gocode/header-gentext.tpl" }}
import (
"github.com/doug-martin/goqu/v9"
{{- range $path, $alias := .imports }}
{{ $alias }} {{ printf "%q" $path }}
{{- end }}
)
var (
{{- range .types }}
// {{ .ident }}Table represents {{ .identPlural }} store table
//
// This value is auto-generated
{{ .ident }}Table = goqu.T({{ printf "%q" .modelIdent }})
// {{ .ident }}SelectQuery assembles select query for fetching {{ .identPlural }}
//
// This function is auto-generated
{{ .ident }}SelectQuery = func(d goqu.DialectWrapper) *goqu.SelectDataset {
return d.Select(
{{- range .struct }}
{{ printf "%q" .storeIdent }},
{{- end }}
).From({{ .ident }}Table)
}
// {{ .ident }}InsertQuery assembles query inserting {{ .identPlural }}
//
// This function is auto-generated
{{ .ident }}InsertQuery = func(d goqu.DialectWrapper, res *{{ .goType }}) *goqu.InsertDataset {
return d.Insert({{ .ident }}Table).
Rows(goqu.Record{
{{- range .struct }}
{{ printf "%q" .storeIdent }}: res.{{ .expIdent }},
{{- end }}
})
}
// {{ .ident }}UpsertQuery assembles (insert+on-conflict) query for replacing {{ .identPlural }}
//
// This function is auto-generated
{{ .ident }}UpsertQuery = func(d goqu.DialectWrapper, res *{{ .goType }}) *goqu.InsertDataset {
var target = `
{{- range .struct -}}
{{- if .primaryKey -}}
,
{{- if .ignoreCase -}}
LOWER({{- .storeIdent -}})
{{- else -}}
{{- .storeIdent -}}
{{- end -}}
{{- end -}}
{{- end -}}`
return {{ .ident }}InsertQuery(d, res).
OnConflict(
goqu.DoUpdate(target[1:],
goqu.Record{
{{- range .struct }}
{{- if not .primaryKey }}
{{ printf "%q" .storeIdent }}: res.{{ .expIdent }},
{{- end }}
{{- end }}
},
),
)
}
// {{ .ident }}UpdateQuery assembles query for updating {{ .identPlural }}
//
// This function is auto-generated
{{ .ident }}UpdateQuery = func(d goqu.DialectWrapper, res *{{ .goType }}) *goqu.UpdateDataset {
return d.Update({{ .ident }}Table).
Set(goqu.Record{
{{- range .struct }}
{{- if not .primaryKey }}
{{ printf "%q" .storeIdent }}: res.{{ .expIdent }},
{{- end }}
{{- end }}
}).
Where({{ .ident }}PrimaryKeys(res))
}
// {{ .ident }}DeleteQuery assembles delete query for removing {{ .identPlural }}
//
// This function is auto-generated
{{ .ident }}DeleteQuery = func(d goqu.DialectWrapper, ee ...goqu.Expression) *goqu.DeleteDataset {
return d.Delete({{ .ident }}Table).Where(ee...)
}
// {{ .ident }}DeleteQuery assembles delete query for removing {{ .identPlural }}
//
// This function is auto-generated
{{ .ident }}TruncateQuery = func(d goqu.DialectWrapper) *goqu.TruncateDataset {
return d.Truncate({{ .ident }}Table)
}
// {{ .ident }}PrimaryKeys assembles set of conditions for all primary keys
//
// This function is auto-generated
{{ .ident }}PrimaryKeys = func(res *{{ .goType }}) goqu.Ex {
return goqu.Ex{
{{- range .struct }}
{{- if .primaryKey }}
{{ printf "%q" .storeIdent }}: res.{{ .expIdent }},
{{- end }}
{{- end }}
}
}
{{ end }}
)

View File

@@ -0,0 +1,798 @@
package rdbms
{{ template "gocode/header-gentext.tpl" }}
import (
"context"
"database/sql"
"fmt"
"strings"
"github.com/doug-martin/goqu/v9"
"github.com/doug-martin/goqu/v9/exp"
"github.com/modern-go/reflect2"
"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"
{{- range $path, $alias := .imports }}
{{ $alias }} {{ printf "%q" $path }}
{{- end }}
)
{{ define "extraArgs" -}}
{{/*This is temporary solution until we properly implement Compose Record Store*/}}
{{- if eq . "composeRecord" }}mod *composeType.Module, {{ end -}}
{{- end }}
{{ define "extraParams" -}}
{{/*This is temporary solution until we properly implement Compose Record Store*/}}
{{- if eq . "composeRecord" }}mod, {{ end -}}
{{- end }}
var (
{{ range .types }}
_ store.{{ .expIdentPlural }} = &Store{}
{{- end }}
)
{{- range .types }}
// Create{{ .expIdent }} creates one or more rows in {{ .ident }} collection
//
// This function is auto-generated
func (s *Store) Create{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) (err error) {
for i := range rr {
if err = s.check{{ .expIdent }}Constraints(ctx, rr[i]); err != nil {
return
}
if err = s.Exec(ctx, {{ .ident }}InsertQuery(s.Dialect.GOQU(), rr[i])); err != nil {
return
}
}
return
}
// Update{{ .expIdent }} updates one or more existing entries in {{ .ident }} collection
//
// This function is auto-generated
func (s *Store) Update{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) (err error) {
for i := range rr {
if err = s.check{{ .expIdent }}Constraints(ctx, rr[i]); err != nil {
return
}
if err = s.Exec(ctx, {{ .ident }}UpdateQuery(s.Dialect.GOQU(), rr[i])); err != nil {
return
}
}
return
}
// Upsert{{ .expIdent }} updates one or more existing entries in {{ .ident }} collection
//
// This function is auto-generated
func (s *Store) Upsert{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) (err error) {
for i := range rr {
if err = s.check{{ .expIdent }}Constraints(ctx, rr[i]); err != nil {
return
}
// @todo this solution is ok for now but could be problematic when we start
// batching together DB operations.
if s.Dialect.Nuances().TwoStepUpsert {
var rsp sql.Result
rsp, err = s.ExecR(ctx, {{ .ident }}UpdateQuery(s.Dialect.GOQU(), rr[i]))
if err != nil {
return
}
if c, err := rsp.RowsAffected(); err != nil {
return err
} else if c > 0 {
continue
}
err = s.Exec(ctx, {{ .ident }}InsertQuery(s.Dialect.GOQU(), rr[i]))
if err != nil {
return
}
} else {
err = s.Exec(ctx, {{ .ident }}UpsertQuery(s.Dialect.GOQU(), rr[i]))
if err != nil {
return
}
}
}
return
}
// Delete{{ .expIdent }} Deletes one or more entries from {{ .ident }} collection
//
// This function is auto-generated
func (s *Store) Delete{{ .expIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} rr ...*{{ .goType }}) (err error) {
for i := range rr {
if err = s.Exec(ctx, {{ .ident }}DeleteQuery(s.Dialect.GOQU(), {{ .ident }}PrimaryKeys(rr[i]))); err != nil {
return
}
}
return nil
}
{{ if .api.deleteByPK.expFnIdent }}
// {{ .api.deleteByPK.expFnIdent }} deletes single entry from {{ .ident }} collection
//
// This function is auto-generated
func (s *Store) {{ .api.deleteByPK.expFnIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} {{ range .api.deleteByPK.attributes }}{{ .ident }} {{ .goType }},{{ end }}) error {
return s.Exec(ctx, {{ .ident }}DeleteQuery(s.Dialect.GOQU(), goqu.Ex{
{{- range .api.deleteByPK.attributes }}
{{ printf "%q" .storeIdent }}: {{ .ident }},
{{- end }}
}))
}
{{ end }}
// Truncate{{ .expIdentPlural }} Deletes all rows from the {{ .ident }} collection
func (s *Store) Truncate{{ .expIdentPlural }}(ctx context.Context, {{ template "extraArgs" .ident }}) error {
return s.Exec(ctx, {{ .ident }}TruncateQuery(s.Dialect.GOQU()))
}
// Search{{ .expIdentPlural }} returns (filtered) set of {{ .expIdentPlural }}
//
// This function is auto-generated
func (s *Store) Search{{ .expIdentPlural }}(ctx context.Context, {{ template "extraArgs" .ident }} f {{ .goFilterType }}) (set {{ .goSetType }}, _ {{ .goFilterType }}, err error) {
{{ if .features.paging }}
// 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 {
if f.IncPageNavigation || f.IncTotal {
return nil, f, fmt.Errorf("not allowed to fetch page navigation or total item count with page cursor")
}
// Page cursor exists; 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
}
}
{{- $sortColumn := "id" }}
{{ if (eq .expIdentPlural "FederationModuleMappings") }}
{{ $sortColumn = "node_id" }}
{{ end }}
{{ if (eq .expIdentPlural "FederationNodeSyncs") }}
{{ $sortColumn = "rel_node" }}
{{ end }}
// Make sure results are always sorted at least by primary keys
if f.Sort.Get("{{ $sortColumn }}") == nil {
f.Sort = append(f.Sort, &filter.SortExpr{
Column: "{{ $sortColumn }}",
Descending: f.Sort.LastDescending(),
})
}
// Cloned sorting instructions for the actual sorting
// Original are passed to the etchFullPageOf{{ .expIdentPlural }} fn used for cursor creation;
// direction information it MUST keep the initial
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()
}
set, f.PrevPage, f.NextPage, err = s.fetchFullPageOf{{ .expIdentPlural }}(ctx, f, sort)
f.PageCursor = nil
if err != nil {
return nil, f, err
}
if f.IncTotal {
// Calc total from the number of items fetched
// even if we do build the page navigation
f.Total = uint(len(set))
if f.Limit > 0 && uint(len(set)) == f.Limit {
// page is full, so there may be more; count the rest
if f.Total, err = s.countOf{{ .expIdentPlural }}(ctx, f); err != nil {
return nil, f, err
}
}
}
{{ else }}
set, _, _, err = s.Query{{ .expIdentPlural }}(ctx, f)
if err != nil {
return nil, f, err
}
{{ end }}
return set, f, nil
}
{{ if .features.paging }}
// fetchFullPageOf{{ .expIdentPlural }} 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
//
// This function is auto-generated
func (s *Store) fetchFullPageOf{{ .expIdentPlural }}(
ctx context.Context,
filter {{ .goFilterType }},
sort filter.SortExprSet,
) (set []*{{ .goType }}, prev, next *filter.PagingCursor, err error) {
var (
aux []*{{ .goType }}
// 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 = filter.PageCursor != nil && filter.PageCursor.ROrder
// Copy no. of required items to limit
// Limit will change when doing subsequent queries to fill
// the set with all required items
limit = filter.Limit
reqItems = filter.Limit
// cursor to prev. page is only calculated when cursor is used
hasPrev = filter.PageCursor != nil
// next cursor is calculated when there are more pages to come
hasNext bool
tryFilter {{ .goFilterType }}
// last row the query reached, whether or not the check fn kept it;
// survives the per-try filter reset so retries continue where the previous one stopped
lastScanned *{{ .goType }}
// cursor the next try starts from
cursor = filter.PageCursor
)
set = make([]*{{ .goType }}, 0, DefaultSliceCapacity)
for try := 0; try < MaxRefetches; try++ {
// Copy filter & apply custom sorting that might be affected by cursor
tryFilter = filter
tryFilter.Sort = sort
tryFilter.PageCursor = cursor
if limit > 0 {
// fetching + 1 to peak ahead if there are more items
// we can fetch (next-page cursor)
tryFilter.Limit = limit + 1
}
if aux, hasNext, lastScanned, err = s.Query{{ .expIdentPlural }}(ctx, tryFilter); err != nil {
return nil, nil, nil, err
}
if lastScanned == nil {
// source exhausted
break
}
// advance past everything this try reached, kept or not;
// built from the effective sort, which is flipped when paging backwards
cursor = s.{{ .api.collectCursorValues.fnIdent }}(lastScanned, sort...)
cursor.LThen = sort.Reversed()
// append fetched items
set = append(set, aux...)
if reqItems == 0 || !hasNext {
// no max requested items specified, break out
break
}
collected := uint(len(set))
if reqItems > collected {
if len(aux) == 0 {
// the check fn rejected the whole batch; widen the window so a long
// run of rejected rows is crossed in a few queries, not MaxRefetches
if limit < MaxEnsureFetchLimit {
limit *= 2
}
} else {
// 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
}
}
continue
}
break
}
// never hand back more than was asked for; anything trimmed means there is another page
if reqItems > 0 && uint(len(set)) > reqItems {
set = set[:reqItems]
hasNext = true
}
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.{{ .api.collectCursorValues.fnIdent }}(set[0], filter.Sort...)
prev.ROrder = true
prev.LThen = !filter.Sort.Reversed()
}
if hasNext {
next = s.{{ .api.collectCursorValues.fnIdent }}(set[collected-1], filter.Sort...)
next.LThen = filter.Sort.Reversed()
}
return set, prev, next, nil
}
// countOf{{ .expIdentPlural }} counts all rows matching the filter
//
// Rows are counted, not collected, so the cost does not grow with the size of
// the result set. When a check fn is set it has to see every row, so rows are
// scanned and decoded one at a time; without one the database does the counting.
//
// This function is auto-generated
func (s *Store) countOf{{ .expIdentPlural }}(ctx context.Context, f {{ .goFilterType }}) (total uint, err error) {
var (
expr []goqu.Expression
)
if s.Filters.{{ .expIdent }} != nil {
// extended filter set
expr, f, err = s.Filters.{{ .expIdent }}(s, f)
} else {
// using generated filter
expr, f, err = {{ .expIdent }}Filter(s.Dialect, f)
}
if err != nil {
return 0, fmt.Errorf("could not generate filter expression for {{ .expIdent }}: %w", err)
}
{{ if .features.checkFn }}
if f.Check != nil {
var (
rows *sql.Rows
aux *{{ .auxIdent }}
res *{{ .goType }}
ok bool
)
if rows, err = s.Query(ctx, {{ .ident }}SelectQuery(s.Dialect.GOQU()).Where(expr...)); err != nil {
return 0, fmt.Errorf("could not query {{ .expIdent }}: %w", err)
}
defer func() {
closeError := rows.Close()
if err == nil {
// return error from close
err = closeError
}
}()
for rows.Next() {
if err = rows.Err(); err != nil {
return 0, fmt.Errorf("could not query {{ .expIdent }}: %w", err)
}
aux = new({{ .auxIdent }})
if err = aux.scan(rows); err != nil {
return 0, fmt.Errorf("could not scan rows for {{ .expIdent }}: %w", err)
}
if res, err = aux.decode(); err != nil {
return 0, fmt.Errorf("could not decode {{ .expIdent }}: %w", err)
}
if ok, err = f.Check(res); err != nil {
return 0, err
} else if ok {
total++
}
}
return total, rows.Err()
}
{{ end }}
var (
counted int64
countRows *sql.Rows
)
if countRows, err = s.Query(ctx, {{ .ident }}SelectQuery(s.Dialect.GOQU()).Where(expr...).Select(goqu.COUNT(goqu.Star()))); err != nil {
return 0, fmt.Errorf("could not count {{ .expIdent }}: %w", err)
}
defer func() {
closeError := countRows.Close()
if err == nil {
// return error from close
err = closeError
}
}()
if countRows.Next() {
if err = countRows.Scan(&counted); err != nil {
return 0, fmt.Errorf("could not scan count for {{ .expIdent }}: %w", err)
}
}
return uint(counted), countRows.Err()
}
{{ end }}
// Query{{ .expIdentPlural }} queries the database, converts and checks each row and returns collected set
//
// With generics, we can remove this per-resource-generated function
// and replace it with a single utility fetcher
//
// This function is auto-generated
func (s *Store) Query{{ .expIdentPlural }}(
ctx context.Context,
f {{ .goFilterType }},
) (_ []*{{ .goType }}, more bool, last *{{ .goType }}, err error) {
var (
{{ if .features.checkFn }}
ok bool
{{ end }}
set = make([]*{{ .goType }}, 0, DefaultSliceCapacity)
res *{{ .goType }}
aux *{{ .auxIdent }}
rows *sql.Rows
count uint
expr, tExpr []goqu.Expression
{{ if .features.sorting }}
sortExpr []exp.OrderedExpression
{{ end }}
)
if s.Filters.{{ .expIdent }} != nil {
// extended filter set
tExpr, f, err = s.Filters.{{ .expIdent }}(s, f)
} else {
// using generated filter
tExpr, f, err = {{ .expIdent }}Filter(s.Dialect, f)
}
if err != nil {
err = fmt.Errorf("could not generate filter expression for {{ .expIdent }}: %w", err)
return
}
expr = append(expr, tExpr...)
{{ if .features.paging }}
// paging feature is enabled
if f.PageCursor != nil {
{{- if .features.sorting }}
if tExpr, err = cursorWithSorting(f.PageCursor, s.{{ .api.sortableFields.fnIdent }}()); err != nil {
{{- else }}
if tExpr, err = cursor(f.PageCursor); err != nil {
{{- end }}
return
} else {
expr = append(expr, tExpr...)
}
}
{{ end }}
query := {{ .ident }}SelectQuery(s.Dialect.GOQU()).Where(expr...)
{{ if .features.sorting }}
// sorting feature is enabled
if sortExpr, err = order(f.Sort, s.{{ .api.sortableFields.fnIdent }}()); err != nil {
err = fmt.Errorf("could not generate order expression for {{ .expIdent }}: %w", err)
return
}
if len(sortExpr) > 0 {
query = query.Order(sortExpr...)
}
{{ end }}
if f.Limit > 0 {
query = query.Limit(f.Limit)
}
rows, err = s.Query(ctx, query)
if err != nil {
err = fmt.Errorf("could not query {{ .expIdent }}: %w", err)
return
}
if err = rows.Err(); err != nil {
err = fmt.Errorf("could not query {{ .expIdent }}: %w", err)
return
}
defer func() {
closeError := rows.Close()
if err == nil {
// return error from close
err = closeError
}
}()
for rows.Next() {
if err = rows.Err(); err != nil {
err = fmt.Errorf("could not query {{ .expIdent }}: %w", err)
return
}
aux = new({{ .auxIdent }})
if err = aux.scan(rows); err != nil {
err = fmt.Errorf("could not scan rows for {{ .expIdent }}: %w", err)
return
}
count++
if res, err = aux.decode(); err != nil {
err = fmt.Errorf("could not decode {{ .expIdent }}: %w", err)
return
}
// last scanned row, before the check fn gets a say;
// paging uses it to advance past rows the check rejects
last = res
{{ if .features.checkFn }}
// check fn set, call it and see if it passed the test
// if not, skip the item
if f.Check != nil {
if ok, err = f.Check(res); err != nil {
return
} else if !ok {
continue
}
}
{{ end }}
set = append(set, res)
}
{{ if .features.paging }}
return set, f.Limit > 0 && count >= f.Limit, last, err
{{ else }}
return set, false, last, err
{{ end }}
}
{{- range .api.lookups }}
{{ if .description }}{{ .description }}{{ end }}
//
// This function is auto-generated
func (s *Store) {{ .expFnIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} {{ range .args }}{{ .ident }} {{ .goType }}, {{ end }}) (_ *{{ .returnType }}, err error) {
var (
rows *sql.Rows
aux = new({{ .auxIdent }})
lookup = {{ .ident }}SelectQuery(s.Dialect.GOQU()).Where(
{{- range .args }}
{{- if .ignoreCase }}
s.Functions.LOWER(goqu.I({{ printf "%q" .storeIdent }})).Eq(strings.ToLower({{ .ident }})),
{{- else }}
goqu.I({{ printf "%q" .storeIdent }}).Eq({{ .ident }}),
{{- end }}
{{- end }}
{{- range .nullConstraint }}
stateNilComparison(s.Dialect, {{ printf "%q" . }}, filter.StateExcluded),
{{- end }}
).Limit(1)
)
rows, err = s.Query(ctx, lookup)
if err != nil {
return
}
defer func() {
closeError := rows.Close()
if err == nil {
// return error from close
err = closeError
}
}()
if err = rows.Err(); err != nil {
return
}
if !rows.Next() {
return nil, store.ErrNotFound.Stack(1)
}
if err = aux.scan(rows); err != nil {
return
}
return aux.decode()
}
{{- end }}
{{ with .api.sortableFields }}
// {{ .fnIdent }} returns all {{ .expIdent }} columns flagged as sortable
//
// # Notes
// With optional string arg, all columns are returned aliased
//
// This function is auto-generated
func (Store) {{ .fnIdent }}() map[string]string {
return map[string]string{
{{- range $k, $v := .fields }}
{{ printf "%q: %q" $k $v }},
{{- end }}
}
}
{{ end }}
{{ with .api.collectCursorValues }}
// {{ .fnIdent }} 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 issues:
//
// When collecting cursor values for query that sorts by unique column with partial index (ie: unique handle on
// undeleted items)
//
// This function is auto-generated
func (s *Store) {{ .fnIdent }}(res *{{ .goType }}, cc ...*filter.SortExpr) *filter.PagingCursor {
{{- if .fields }}
var (
cur = &filter.PagingCursor{LThen: filter.SortExprSet(cc).Reversed()}
hasUnique bool
{{ range .primaryKeys }}
pk{{ .expIdent }} bool
{{- end }}
collect = func(cc ...*filter.SortExpr) {
getVal := func(col string) interface{} {
switch col {
{{- range .fields }}
case {{ printf "%q" .ident }}:
{{- if .primaryKey }}
pk{{ .expIdent }} = true
{{- else if .unique }}
hasUnique = true
{{- end }}
return res.{{ .expIdent }}
{{- end }}
}
return nil
}
for _, c := range cc {
switch c.Modifier() {
case filter.COALESCE:
var val interface{}
for _, col := range c.Columns() {
if reflect2.IsNil(val) {
val = getVal(col)
}
}
cur.SetModifier(c.Column, val, c.Descending, c.Modifier(), c.Columns()...)
default:
cur.Set(c.Column, getVal(c.Column), c.Descending)
}
}
}
)
_ = hasUnique
collect(cc...)
{{- range .primaryKeys }}
if !hasUnique || !pk{{ .expIdent }} {
collect(&filter.SortExpr{Column: {{ printf "%q" .ident }}, Descending: {{ if .descending }}true{{ else }}false{{ end }}})
}
{{- end }}
return cur
{{ else }}
return nil
{{- end }}
}
{{ end }}
{{ with .api.checkConstraints }}
// {{ .fnIdent }} 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 cannot rely
// on the full support (MySQL does not support conditional indexes)
//
// This function is auto-generated
func (s *Store) {{ .fnIdent }}(ctx context.Context, res *{{ .goType }}) (err error) {
{{- range .checks }}
err = func() (err error) {
{{- range .fields }}
{{ if eq .goType "uint64" }}
if res.{{ .expIdent }} == 0 {
// skip check on empty values
return nil
}
{{ else }}
// handling string type as default
if len(res.{{ .expIdent }}) == 0 {
// skip check on empty values
return nil
}
{{ end }}
{{ end }}
{{- range .nullConstraint }}
if res.{{ .expIdent }} != nil {
// skip check if value is not nil
return nil
}
{{ end }}
ex, err := s.{{ .lookupFnIdent }}(ctx, {{ range .fields }}res.{{ .expIdent }},{{ end }})
if err == nil && ex != nil && ex.ID != res.ID {
return store.ErrNotUnique.Stack(1)
} else if !errors.IsNotFound(err) {
return err
}
return nil
}()
if err != nil {
return
}
{{ end }}
return nil
}
{{ end }}
{{ end }}

View File

@@ -0,0 +1,17 @@
package tests
{{ template "gocode/header-gentext.tpl" }}
import (
"testing"
"github.com/cortezaproject/corteza/server/store"
)
func testAllGenerated(t *testing.T, s store.Storer) {
{{ range .types }}
t.Run({{ printf "%q" .ident }}, func(t *testing.T) {
test{{ .expIdentPlural }}(t, s)
})
{{- end }}
}

View File

@@ -0,0 +1,79 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
import (
"github.com/cortezaproject/corteza/server/pkg/cast2"
)
{{ range .resources }}
{{ if .model.omitGetterSetter }}
{{continue}}
{{ end }}
func (r {{.expIdent}}) GetID() (uint64) {
{{- $ok := false -}}
{{- range .model.attributes -}}
{{- if eq .expIdent "ID" -}}
return r.ID
{{- $ok = true -}}
{{break}}
{{- end -}}
{{- end -}}
{{- if not $ok }}
// The resource does not define an ID field
return 0
{{- end -}}
}
func (r *{{.expIdent}}) GetValue(name string, pos uint) (any, error) {
if r == nil {
return nil, nil
}
switch name {
{{ range .model.attributes -}}
{{- if .omitGetter -}}
{{continue}}
{{- end -}}
{{- $identAlias := .identAlias -}}
case {{ range $i, $l := $identAlias -}}
"{{ $l }}"{{if not (eq $i (sub (len $identAlias) 1))}},{{end}}
{{- end}}:
return r.{{.expIdent}}, nil
{{ end }}
{{ if .model.defaultGetter }}
default:
return r.getValue(name, pos)
{{end}}
}
return nil, nil
}
func (r *{{.expIdent}}) SetValue(name string, pos uint, value any) (err error) {
if r == nil {
r = &{{.expIdent}}{}
}
switch name {
{{ range .model.attributes -}}
{{- if .omitSetter -}}
{{continue}}
{{- end -}}
{{ $identAlias := .identAlias -}}
case {{ range $i, $l := $identAlias -}}
"{{ $l }}"{{if not (eq $i (sub (len $identAlias) 1))}},{{end}}
{{- end}}:
return cast2.{{ .goCastFnc }}(value, &r.{{ .expIdent }})
{{ end }}
{{ if .model.defaultSetter }}
default:
return r.setValue(name, pos, value)
{{end}}
}
return nil
}
{{ end }}

View File

@@ -0,0 +1,9 @@
package {{ .package }}
{{ template "gocode/header-gentext.tpl" }}
const (
{{- range .types }}
{{ .const }} = {{ printf "%q" .type }}
{{- end }}
)

View File

@@ -0,0 +1,24 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
[...schema.#codegen] &
[
{
template: "docs/options.adoc.tpl"
output: "src/modules/generated/partials/env-options.gen.adoc"
payload: {
groups: [
for g in app.corteza.options {
title: g.title
intro?: g.intro
options: g.options
},
]
}
},
]

View File

@@ -0,0 +1,67 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
"github.com/cortezaproject/corteza/server/app"
)
#_indexPayload: {
label: string
resources: [...string]
}
#_operationsPayload: {
label: string
operations: [...{
slug: string
label: string
description: string
}]
}
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "docs/rbac.index.adoc.tpl"
output: "src/modules/generated/partials/access-control/\(cmp.handle)/index.gen.adoc"
payload: #_indexPayload & {
label: cmp.label
resources: [ for res in cmp.resources { res.handle } ]
}
}
] +
[
for cmp in app.corteza.components {
template: "docs/rbac.$component.adoc.tpl"
output: "src/modules/generated/partials/access-control/\(cmp.handle)/component.gen.adoc"
payload: #_operationsPayload & {
label: cmp.label
operations: [
for op in cmp.rbac.operations {
slug: "rbac-\(cmp.handle)-\(op.handle)"
label: op.handle
description: op.description
}
]
}
}
] +
[
for cmp in app.corteza.components for res in cmp.resources if res.rbac != _|_ {
template: "docs/rbac.$resource.adoc.tpl"
output: "src/modules/generated/partials/access-control/\(cmp.handle)/resource.\(res.handle).gen.adoc"
payload: #_operationsPayload & {
label: res.handle
operations: [
for op in res.rbac.operations {
slug: "rbac-\(res.handle)-\(op.handle)"
label: op.handle
description: op.description
}
]
}
}
]

View File

@@ -0,0 +1,19 @@
package schema
#_ioSpec: {
template: string
output: string
syntax: string | *"go"
if output =~ "\\.adoc$" {
syntax: "adoc"
}
}
#codegen: {
#_ioSpec
payload: _
} | {
bulk?: [...#_ioSpec]
payload: _
}

View File

@@ -0,0 +1,32 @@
package schema
import (
"strings"
)
#component: #_base & {
// copy field values from #_base
handle: handle, ident: ident, expIdent: expIdent
label: strings.ToTitle(ident)
platform: #baseHandle
resources: {
[key=_]: {"handle": key, "component": handle, "platform": platform} & #Resource
}
fqrt: platform + "::" + handle
envoy: {
omit: bool | *false
}
// All known RBAC operations for this component
rbac: #rbacComponent & {
operations: {
grant: {
description: "Manage \(handle) permissions"
}
}
}
}

View File

@@ -0,0 +1,54 @@
package schema
import (
"strings"
"list"
)
#locale: {
resourceExpIdent: #expIdent
// @todo we need a better name here!
skipSvc: bool | *false
extended: bool | *false
resource: {
type: string
const: string | *("\(resourceExpIdent)ResourceTranslationType")
}
keys: {
[key=_]: #localeKey & {
name: key
_resourceExpIdent: resourceExpIdent
}
}
}
#localeKey: {
name: #handle
_resourceExpIdent: #expIdent
path: [...(#ident | {part: #ident, var: bool | *false})] | *([name])
expandedPath: [ for p in path {
if (p & {"p": #ident}) != _|_ {p, var: p.var}
if (p & string) != _|_ {"part": p, var: false}
}]
_suffix: strings.Join([ for p in expandedPath {strings.ToTitle(p.part)}], "")
struct: string | *("LocaleKey" + _resourceExpIdent + _suffix)
// As soon as we use vars in the path,
// custom handler must be present
_hasVars: list.Contains([ for p in path {p.var | false}], true)
customHandler: bool | *_hasVars
if customHandler {
decodeFunc: string | *("decodeTranslations" + _suffix)
encodeFunc: string | *("encodeTranslations" + _suffix)
serviceFunc: string | *("handle" + _resourceExpIdent + _suffix)
}
}

View File

@@ -0,0 +1,337 @@
package schema
import (
"strings"
)
#Model: {
ident: string
attributes: {
[name=_]: { "name": name }
} & {
[string]: #ModelAttribute
}
omitGetterSetter: bool | *false
defaultGetter: bool | *false
defaultSetter: bool | *false
indexes: ({
[name=_]: { "name": name, "modelIdent": ident } & #ModelIndex
} & {
[string]: #ModelIndex
}) | *({})
}
// logic in struct fields is a bit different
#ModelAttribute: {
name: #ident
_words: strings.Replace(strings.Replace(name, "_", " ", -1), ".", " ", -1)
_ident: strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// Golang type (built-in or other)
goType: string | *"string"
goCastFnc: string | *strings.ToTitle(goType)
if goType == "*time.Time" {
goCastFnc: "TimePtr"
}
if goType == "time.Time" {
goCastFnc: "Time"
}
if goType == "map[string]any" {
goCastFnc: "Meta"
}
// lowercase (unexported, golang) identifier
ident: #ident | *_ident
// uppercase (exported, golang) identifier
expIdent: #expIdent | *strings.ToTitle(ident)
// store identifier
// @todo this should be moved to dal.ident
storeIdent: #ident | *name
// enable or disable store for this attribute
// @todo we should use dal prop for this, and extend it to support bool "false"
// so that it can be disabled
store: bool | *true
unique: bool | *false
sortable: bool | *false
descending: bool | *false
ignoreCase: bool | *false
// currently disabled since not used by anything
// it adds more than 4x overhead to the time it takes to generate the store code!
// #ModelAttributeJsonTag
dal?: #ModelAttributeDal
envoy: {
$attrIdent: ident
} & #attributeEnvoy
// specifies all of the identifiers this attribute may define when using getters/setters
identAlias: [...string] | *[ident, expIdent]
// enable or disable GetValue and SetValue for this attribute
omitSetter: bool | *false
omitGetter: bool | *false
}
#attributeEnvoy: {
$attrIdent: string
// controls if this attribute can be used as an identifier
identifier: bool | *false
// YAML decode/encode configs
yaml: {
identKeyLabel: string | *strings.ToLower($attrIdent)
identKeyAlias: [...string] | *[]
// identKeys defines what identifiers this attribute supports when
// decoding yaml documents
identKeys: ([identKeyLabel]+identKeyAlias)
customDecoder: bool | *false
customEncoder: bool | *false
identKeyEncode: string | *$attrIdent
omitEncoder: bool | *false
}
// store decode/encode configs
store: {
// defines a custom field identifier when constructing
// resource filters and assigning reference constraints
filterRefField: string | *""
omitRefFilter: bool | *false
}
}
#ModelAttributeDal: {
type: #ModelAttributeDalType | *"Text"
fqType: "dal.Type\(type)"
nullable: bool | *false
if type == "ID" {
generatedByStore: bool | *false
default?: 0
}
if type == "Ref" {
refModelResType: #FQRT
attribute: #handle | *"id"
default?: 0
}
if type == "Timestamp" {
timezone: bool | *false
precision: number | *(-1)
defaultCurrentTimestamp?: true
}
if type == "Time" {
timezone: bool | *false
precision: number | *(-1)
defaultCurrentTimestamp?: true
}
if type == "Date" {
defaultCurrentTimestamp?: true
}
if type == "Number" {
precision: number | *(-1)
scale: number | *(-1)
default?: number
meta?: { [string]: _ }
}
if type == "Text" {
length: number | *0
default?: string
}
if type == "Boolean" {
default?: bool
}
if type == "Enum" {
values: []
default?: string
}
if type == "Geometry" {}
if type == "JSON" {
default?: string | bytes
defaultEmptyObject?: true
}
if type == "Blob" {
default?: bytes
}
if type == "UUID" {}
}
#ModelAttributeDalType:
"ID" | "Ref" |
"Timestamp" | "Time" | "Date" |
"Number" |
"Text" |
"Boolean" |
"Enum" |
"Geometry" |
"JSON" |
"Blob" |
"UUID"
IdField: {
// Expecting ID field to always have name ID
name: "id"
expIdent: "ID"
unique: true
// @todo someday we'll replace this with the "ID" type
goType: "uint64"
dal: { type: "ID" }
envoy: #attributeEnvoy & {
identifier: true
}
}
HandleField: {
// Expecting ID field to always have name handle
name: "handle"
unique: true
ignoreCase: true
goType: "string"
dal: { type: "Text", length: 64 }
envoy: #attributeEnvoy & {
identifier: true
}
}
AttributeUserRef: {
goType: "uint64"
dal: { type: "Ref", refModelResType: "corteza::system:user", default: 0 }
envoy: {
store: {
omitRefFilter: true
}
}
}
SortableTimestampField: {
sortable: true
goType: "time.Time"
dal: { type: "Timestamp", timezone: true, nullable: false }
}
SortableTimestampNowField: {
sortable: true
goType: "time.Time"
dal: { type: "Timestamp", timezone: true, nullable: false, defaultCurrentTimestamp: true }
}
SortableTimestampNilField: {
sortable: true
goType: "*time.Time"
dal: { type: "Timestamp", timezone: true, nullable: true }
}
#ModelAttributeJsonTag: {
name: string
_specs: {field: string | *name, omitEmpty: bool | *false, "string": bool | *false}
json: string | _specs | bool | *false
jsonTag?: string
// just wrap whatever we got in json
if (json & string) != _|_ {
jsonTag: "json:\"\(json)\""
}
// json enable,d wrap with ident as a JSON prop name
if (json & bool) != _|_ && json {
// generic json tag
jsonTag: "json:\"\(name)\""
}
// full-specs
if (json & bool) == _|_ && (json & _specs) != _|_ {
_omitEmpty: string | *""
if json.omitEmpty {
_omitEmpty: ",omitempty"
}
_string: string | *""
if json.string {
_string: ",string"
}
jsonTag: "json:\"\(json.field)\(_omitEmpty)\(_string)\""
}
}
#ModelIndex: close({
name: #ident
modelIdent: #ident
_attributes: { [_]: #ModelAttribute }
_words: strings.Replace(strings.Replace(name, "_", " ", -1), ".", " ", -1)
_ident: strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// lowercase (unexported, golang) identifier
ident: #ident | *"\(modelIdent)_\(_ident)"
primary: bool | *(strings.ToLower(name) == "primary")
unique: bool | *(strings.Contains(name, "unique") || primary)
type: "BTREE" | *"BTREE"
// index predicate,
// condition that must be met for the index to be used
predicate?: string
attribute?: string
attributes?: [string, ...]
fields: [#ModelIndexField, ...]
if attribute != _|_ {
attributes: [string, ...] & [attribute]
}
if fields != _|_ && attributes != _|_ {
fields: [
for a in attributes {
{"attribute": a} & #ModelIndexField
}
]
}
})
#IndexFieldModifier: "LOWERCASE"
#ModelIndexField: close({
attribute: string
modifiers?: [#IndexFieldModifier, ...]
length?: number
sort?: "DESC" | "ASC"
nulls?: "FIRST" | "LAST"
})

View File

@@ -0,0 +1,49 @@
package schema
import (
"strings"
)
#_ENV: =~"^[A-Z][A-Z0-9_]*[A-Z0-9]?$"
//#_optName: =~ "^[a-zA-Z][a-zA-Z0-9\\s]*[a-zA-Z0-9]+$"
#optionsGroup: #_base & {
imports: [...string] | *([])
handle: #handle
title: string | *handle
description?: string
env: #_ENV | *(strings.ToUpper(strings.Replace(handle, "-", "_", -1)))
_envPrefix: env
options: {
[_opt=_]: #option & {
handle: _opt
env: #_ENV | *(_envPrefix + "_" + strings.ToUpper(strings.Replace(handle, " ", "_", -1)))
}
}
}
#option: {
handle: #handle
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
// lowercased (unexported, golang) identifier
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// upercased (exported, golang) identifier
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
type: string | *"string"
description?: string
// Default expression to be used
defaultGoExpr?: string
env?: #_ENV
// Plain default value to use when generating .env.example
defaultValue?: string
}

View File

@@ -0,0 +1,21 @@
package schema
#platform: {
ident: #baseHandle | *"corteza"
options: [...#optionsGroup]
components: [...{platform: ident} & #component]
resources: {
[key=#handle]: #Resource & {
"handle": key,
"platform": ident
}
}
// automation: {
// types: ....
// function ....
// }
}

View File

@@ -0,0 +1,61 @@
package schema
import (
"strings"
)
#rbacComponent: {
resource: {
type: string
}
operations: {
[key=_]: #rbacOperation & {
handle: key
}
}
}
#rbacResource: {
resourceExpIdent: #expIdent
operations: {
[key=_]: #rbacOperation & {
handle: key
_resourceExpIdent: resourceExpIdent
}
}
}
#rbacOperation: {
handle: #handle
description: string | *handle
_resourceExpIdent?: string
// Some string manipulation that will result in
// more pronouncable access-control check function name
// When check function name is not explicitly defined we try
// to use resource and operation name and generate easy-to-read name
//
// <res> + <op> => Can<Op><Res>
// <res> + <op:foo.bar.verb> => Can<Verb><Foo><Bar>On<Res>
_operation: strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1)
_opSplit: strings.Split(_operation, ".")
_opFlip: [_opSplit[len(_opSplit)-1]] + _opSplit[0:len(_opSplit)-1]
_opFinal: strings.Replace(strings.ToTitle(strings.Join(_opFlip, " ")), " ", "", -1)
if _resourceExpIdent == _|_ {
checkFuncName: #expIdent | *"Can\(_opFinal)"
}
if len(_opSplit) > 1 && _resourceExpIdent != _|_ {
checkFuncName: #expIdent | *"Can\(_opFinal)On\(_resourceExpIdent)"
}
if len(_opSplit) <= 1 && _resourceExpIdent != _|_ {
checkFuncName: #expIdent | *"Can\(_opFinal)\(_resourceExpIdent)"
}
}

View File

@@ -0,0 +1,232 @@
package schema
import (
"strings"
)
// fully qualified resource type
#FQRT: =~ "^corteza::(compose|system|federation|automation):[a-z][a-z0-9-]*$"
#Resource: {
#_base
// type: #resourceType | *""
imports: [...{ import: string }]
// copy field values from #_base
handle: handle, ident: ident, expIdent: expIdent
component: #baseHandle | *"component"
platform: #baseHandle | *"corteza"
// Fully qualified resource name
fqrt: #FQRT | *(platform + "::" + component + ":" + handle)
model: #Model & {
// use resource handle (plural) as model ident as default
// model ident represents a db table or a container name
ident: string | *"\(strings.Replace(handle, "-", "_", -1))s"
}
filter: {
"expIdent": #expIdent | *"\(expIdent)Filter"
struct: {
[name=_]: {"name": name} & #ModelAttribute
}
// generate filtering by-nil-state for the specified fields
"byNilState": [...string]
// generate filtering by-false-state for the specified fields
"byFalseState": [...string]
// generate query filter for the specified fields
"query": [...string]
// filter resources by fields (eq)
"byValue": [...string]
}
// operations: #Operations
features: {
// filtering by label
labels: bool | *true
// filtering by flag
flags: bool | *false
// support pagination
paging: bool | *true
// support sorting
sorting: bool | *true
// support resource check function
checkFn: bool | *true
}
// All parent resources
parents: [... #_base & {
// copy field values from #_base
handle: handle, ident: ident, expIdent: expIdent
refField: #expIdent | *(expIdent + "ID")
param: #ident | *(ident + "ID")
}]
// All known RBAC operations for this resource
rbac?: #rbacResource & {
resourceExpIdent: expIdent
}
locale?: #locale & {
resourceExpIdent: expIdent
resource: {
// @todo can we merge this with RBAC type (FQRN?)
type: component + ":" + handle
}
}
envoy?: #resourceEnvoy & {
omit: bool | *false
$resourceIdent: ident
}
store?: {
// how is this resource represented (prefixed/suffixed functions) in the store
"ident": #ident | *ident
"identPlural": #ident | *"\(store.ident)s"
"expIdent": #expIdent | *strings.ToTitle(store.ident)
"expIdentPlural": #expIdent | *"\(store.expIdent)s"
api?: {
lookups: [...{
_expFields: [ for f in fields {strings.ToTitle(model.attributes[f].expIdent)}]
"expIdent": "Lookup\(store.expIdent)By" + strings.Join(_expFields, "")
description: string | *""
// fields used for the lookup (must exist in the struct)
fields: [...string]
// Skip null constraints
nullConstraint: [...string]
constraintCheck: bool | *false
}]
functions: [...{
expIdent: string
description: string | *""
args: [...{ident: #ident, goType: string, spread: bool | *false}]
return: [...string]
}]
}
}
}
#storeFunction: {
expIdent: #expIdent
args: [...string]
return: [...string]
}
#PkgResource: #Resource & {
package: {
ident: #ident
import: string
}
}
#resourceEnvoy: {
$resourceIdent: string
omit: bool
// Scoped resources prioritize matching with resources in the same scope.
// This is useful when we want to import multiple namespaces at the same time.
scoped: bool | *false
// YAML decode/encode configs
yaml: {
// supportMappedInput controls whether the resource can be presented in
// a mapping node where the key of the map is an identifier.
//
// An example:
//
// modules:
// module1:
// name: module 1 name
//
// Where a resource with no mapping would look like:
//
// modules:
// - handle: module1
// name: module 1 name
supportMappedInput: bool | *true
// mappedField controls what identifier the map key represents
// @todo this can probably be inferred so consider removing it.
mappedField: string | *""
identKeyLabel: string | *strings.ToLower($resourceIdent)
identKeyAlias: [...string] | *[]
// identKeys defines all of the identifiers that can be used when
// referencing this resource
identKeys: [...string] | *([identKeyLabel]+identKeyAlias)
omitEncoder: bool | *false
extendedResourcePostProcess: bool | *false
extendedResourceDecoders: [...{
ident: string
expIdent: string
identKeys: [...string]
supportMappedInput: bool | *true
mappedField: string | *""
}] | *[]
extendedResourceRefIdent: string | *""
// enable or disable offloading unhandled nodes onto a custom default decoder
defaultDecoder: bool | *false
extendedResourceEncoders: [...{
ident: string
expIdent: string
identKey: string | *""
}] | *[]
}
// store decode/encode configs
store: {
// enable or disable custom logic after the resource is imported
extendedEncoder: bool | *false
extendedSubResources: bool | *false
// temporary until I figure something better.
// the idea here is that after all of the modules (or resources x) are imported
// only then we should do something over them (like import records).
postSetEncoder: bool | *false
// enable or disable additional custom processing for determining
// resource references
extendedRefDecoder: bool | *false
handleField: string | *"Handle"
customFilterBuilder: bool | *false
// extendedFilterBuilder is called after the built-in which you can use
// to append additional constraints to.
//
// Does nothing if customFilterBuilder is set to true
extendedFilterBuilder: bool | *false
// extendedDecoder is called after the built-in which you can use
// to append additional nodes into.
extendedDecoder: bool | *false
// the resource calls a sanitizer function before saving to the database
sanitizeBeforeSave: bool | *false
}
}

View File

@@ -0,0 +1,39 @@
package schema
import (
"strings"
)
// Identifier
#ident: =~"^[a-z][a-zA-Z0-9_]*$"
// Exported identifier
#expIdent: =~"^[A-Z][a-zA-Z0-9_]*$"
// More liberal then identifier, allows underscores and dots
#handle: =~"^[A-Za-z][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9]+$"
// More liberal then identifier
#baseHandle: =~"^[a-z][a-z0-9-]*[a-z0-9]+$"
#_base: {
// lowercase dash-separated words
// used to build ident and exported identifiers
handle: #baseHandle
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
// lowercase (unexported, golang) identifier
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// plural
identPlural: #ident | *"\(ident)s"
// uppercase (exported, golang) identifier
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
// plural exported
expIdentPlural: #expIdent | *"\(expIdent)s"
...
}

View File

@@ -0,0 +1,151 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
_dalModelFn: {
res="res": schema.#Resource
"output": {
"var": string | *"\(res.expIdent)"
"resType": string | *"types.\(res.expIdent)ResourceType"
"ident": res.model.ident
"attributes": [
for attr in res.model.attributes if attr.dal != _|_ {
attr
"dal": {
attr.dal
// Only these field types support "has-default" flag
if (attr.dal.type & ( "ID" | "Ref" | "Number" | "Boolean" | "Enum" | "JSON" )) != _|_ {
"hasDefault": attr.dal.default != _|_
}
if attr.dal.default != _|_ {
"quotedDefault": attr.dal.type == "String"
}
if attr.dal.meta != _|_ {
"meta": attr.dal.meta
}
}
}
]
"indexes": {[string]: close({
"ident": string
"type": string
"unique"?: bool
"predicate"?: string
"fields": [{
"attribute": string
"modifers"?: [string, ...]
"sort"?: string
"nulls"?: string
}, ...]
})} | *null
if res.model.indexes != _|_ {
"indexes": {
for index in res.model.indexes {
"\(index.ident)": {
if index.primary == true {
"ident": "PRIMARY"
}
if index.primary == false {
"ident": index.ident
"unique": index.unique
}
"type": index.type
if index.predicate != _|_ {
"predicate": index.predicate
}
"fields": [
for field in index.fields {
"attribute": res.model.attributes[field.attribute].expIdent
if field.modifiers != _|_ {
"modifiers": field.modifiers
}
if field.sort != _|_ {
"sort": field.sort
}
if field.nulls != _|_ {
"nulls": field.nulls
}
},
]
}
}
}
}
}
},
[...schema.#codegen] &
[
{
template: "gocode/dal/$component_model.go.tpl"
output: "system/model/corteza.gen.go"
payload: {
package: "model"
imports: [
for res in app.resources if (res.model.attributes != _|_) {
"\(res.package.ident)type \"\(res.package.import)\"",
}
]
// Operation/resource validators, grouped by resource
models: {
for res in app.resources if (res.model.attributes != _|_) {
"\(res.ident)": {
// overriding resoure-type (import package alias) for app resources
"resType": "\(res.package.ident)type.\(res.expIdent)ResourceType"
(_dalModelFn & { "res": res }).output
},
},
}
}
},
for cmp in app.corteza.components {
template: "gocode/dal/$component_model.go.tpl"
output: "\(cmp.ident)/model/models.gen.go"
payload: {
package: "model"
imports: [
"\"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\"",
]
// Operation/resource validators, grouped by resource
models: {
for res in cmp.resources if (res.model.attributes != _|_) {
"\(res.ident)": {
(_dalModelFn & { "res": res }).output
}
},
}
}
},
for cmp in app.corteza.components {
template: "gocode/dal/$component_init.go.tpl"
output: "\(cmp.ident)/model/init.gen.go"
payload: {
package: "model"
}
},
]

View File

@@ -0,0 +1,94 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components if !cmp.envoy.omit {
template: "gocode/envoy/yaml_decode.go.tpl"
output: "\(cmp.ident)/envoy/yaml_decode.gen.go"
payload: {
package: "envoy"
imports: [
"github.com/cortezaproject/corteza/server/\(cmp.ident)/types"
]
componentIdent: cmp.ident
resources: [ for res in cmp.resources { res }]
}
},
for cmp in app.corteza.components if !cmp.envoy.omit {
template: "gocode/envoy/store_decode.go.tpl"
output: "\(cmp.ident)/envoy/store_decode.gen.go"
payload: {
package: "envoy"
imports: [
"github.com/cortezaproject/corteza/server/\(cmp.ident)/types"
]
componentIdent: cmp.ident
resources: [ for res in cmp.resources { res }]
}
},
for cmp in app.corteza.components if !cmp.envoy.omit {
template: "gocode/envoy/store_encode.go.tpl"
output: "\(cmp.ident)/envoy/store_encode.gen.go"
payload: {
package: "envoy"
imports: [
"github.com/cortezaproject/corteza/server/\(cmp.ident)/types"
]
componentIdent: cmp.ident
resources: [ for res in cmp.resources { res }]
}
},
for cmp in app.corteza.components if !cmp.envoy.omit {
template: "gocode/envoy/yaml_encode.go.tpl"
output: "\(cmp.ident)/envoy/yaml_encode.gen.go"
payload: {
package: "envoy"
imports: [
"github.com/cortezaproject/corteza/server/\(cmp.ident)/types"
]
componentIdent: cmp.ident
resources: [ for res in cmp.resources { res }]
}
},
{
template: "gocode/envoy/util_rbac.go.tpl"
output: "pkg/envoyx/util_rbac.gen.go"
payload: {
package: "envoyx"
components: [for cmp in app.corteza.components if !cmp.envoy.omit {
ident: cmp.ident,
resources: cmp.resources
}]
}
},
{
template: "gocode/envoy/utils.go.tpl"
output: "pkg/envoyx/utils.gen.go"
payload: {
package: "envoyx"
components: [for cmp in app.corteza.components if !cmp.envoy.omit {
ident: cmp.ident,
resources: cmp.resources
}]
}
},
]

View File

@@ -0,0 +1,133 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
// "strings"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "gocode/envoy/rbac_references_$component.go.tpl"
output: "pkg/envoyx/rbac_references_\(cmp.ident).gen.go"
payload: {
package: "envoyx"
imports: [
"\"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\"",
]
resources: [
for res in cmp.resources if res.rbac != _|_ {
rbacRefFunc: "\(cmp.expIdent)\(res.expIdent)RbacReferences"
references: [
for p in res.parents {p},
{param: res.ident, refField: "ID", expIdent: res.expIdent},
]
},
]
}
},
]+
[
{
template: "gocode/envoy/rbac_rules_parse.go.tpl"
output: "pkg/envoyx/rbac_rules_parse.gen.go"
payload: {
package: "envoyx"
imports: [
for cmp in app.corteza.components {
"\(cmp.ident)Types \"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\""
},
]
resources: [
for cmp in app.corteza.components for res in cmp.resources if res.rbac != _|_ {
importAlias: "\(cmp.ident)Types"
expIdent: res.expIdent
typeConst: "\(importAlias).\(expIdent)ResourceType"
rbacRefFunc: "\(cmp.expIdent)\(res.expIdent)RbacReferences"
references: [
for p in res.parents {p},
{param: res.ident, refField: "ID", expIdent: res.expIdent},
]
},
for cmp in app.corteza.components {
importAlias: "\(cmp.ident)Types"
expIdent: cmp.expIdent
typeConst: "\(importAlias).ComponentResourceType"
references: []
},
]
}
},
]+
[
{
template: "gocode/envoy/resource_translation.go.tpl"
output: "pkg/envoy/resource/resource_translation.gen.go"
payload: {
package: "resource"
resources: [
for cmp in app.corteza.components for res in cmp.resources if res.locale != _|_ {
expIdent: "\(cmp.expIdent)\(res.expIdent)"
extended: res.locale.extended
},
]
}
},
]+
[
{
template: "gocode/envoy/resource_translation_parse.go.tpl"
output: "pkg/envoy/resource/resource_translation_parse.gen.go"
payload: {
package: "resource"
imports: [
for cmp in app.corteza.components for res in cmp.resources if res.locale != _|_ {
"\(cmp.ident)Types \"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\""
},
]
resources: [
for cmp in app.corteza.components for res in cmp.resources if res.locale != _|_ {
importAlias: "\(cmp.ident)Types"
typeConst: "\(importAlias).\(res.expIdent)ResourceTranslationType"
resTrRefFunc: "\(cmp.expIdent)\(res.expIdent)ResourceTranslationReferences"
references: [
for p in res.parents {p},
{param: res.ident, refField: "ID", expIdent: res.expIdent},
]
},
]
}
},
]+
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/envoy/resource_translation_references_$component.go.tpl"
output: "pkg/envoy/resource/resource_translation_references_\(cmp.ident).gen.go"
payload: {
package: "resource"
imports: [
"\"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\"",
]
resources: [
for res in cmp.resources if res.locale != _|_ {
resTrRefFunc: "\(cmp.expIdent)\(res.expIdent)ResourceTranslationReferences"
expIdent: res.expIdent
references: [
for p in res.parents {p},
]
},
]
}
},
] if len(tpl.payload.resources) > 0 {tpl}
]

View File

@@ -0,0 +1,43 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
[...schema.#codegen] &
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/locale/$component_service.go.tpl"
output: "\(cmp.ident)/service/locale.gen.go"
payload: {
package: "service"
imports: [
"\"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\"",
]
resources: [
for res in cmp.resources if (res.locale != _|_) if (!res.locale.skipSvc) {
expIdent: res.expIdent
ident: res.ident
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
extended: res.locale.extended
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
struct: key.struct
"extended": extended
customHandler: key.customHandler
if key.serviceFunc != _|_ {serviceFunc: key.serviceFunc}
}]
},
]
}
},
// skip empty type lists
] if len(tpl.payload.resources) > 0 {tpl}
]

View File

@@ -0,0 +1,55 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
"strings"
)
[...schema.#codegen] &
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/locale/$component_types.go.tpl"
output: "\(cmp.ident)/types/locale.gen.go"
payload: {
package: "types"
resources: [
for res in cmp.resources if res.locale != _|_ {
expIdent: res.expIdent
const: res.locale.resource.const
type: res.locale.resource.type
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
extended: res.locale.extended
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
struct: key.struct
field: strings.ToTitle(key.name)
path: strings.Join([ for p in key.expandedPath {
if p.var {"{{\(p.part)}}"}
if !p.var {p.part}
}], ".")
if !key.customHandler {
fieldPath: strings.Join([ for p in key.expandedPath {
strings.ToTitle(p.part)
}], ".")
}
"extended": extended
if key.decodeFunc != _|_ {decodeFunc: key.decodeFunc}
if key.encodeFunc != _|_ {encodeFunc: key.encodeFunc}
}]
},
]
}
},
// skip empty type lists
] if len(tpl.payload.resources) > 0 {tpl}
]

View File

@@ -0,0 +1,82 @@
package codegen
import (
"strings"
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
[...schema.#codegen] &
[
{
template: "gocode/options/options.go.tpl"
output: "pkg/options/options.gen.go"
payload: {
package: "options"
// make unique list of packages we'll import
imports: [ for i in {for g in app.corteza.options for i in g.imports {"\(i)": i}} {i}]
groups: [
for g in app.corteza.options {
func: g.expIdent
struct: g.expIdent + "Opt"
options: [
for o in g.options {
o
default?: string
if (o.defaultGoExpr != _|_) {
default: o.defaultGoExpr
}
if (o.defaultGoExpr == _|_ && o.defaultValue != _|_) {
default: "\"" + o.defaultValue + "\""
}
},
]
},
]
}
},
]+
[
{
template: "docs/.env.example.tpl"
output: ".env.example"
syntax: ".env"
payload: {
groups: [
for g in app.corteza.options {
title: "# " + strings.Join(strings.Split(g.title, "\n"), "\n# ")
if (g.intro != _|_) {
intro: "# " + strings.Join(strings.Split(g.intro, "\n"), "\n# ")
}
options: [
for o in g.options {
handle: o.handle
env: o.env
type: o.type
defaultGoExpr?: string
if (o.defaultGoExpr != _|_) {
defaultGoExpr: o.defaultGoExpr
}
defaultValue?: string
if (o.defaultValue != _|_) {
defaultValue: o.defaultValue
}
if (o.description != _|_) {
description: "# " + strings.Join(strings.Split(o.description, "\n"), "\n# ")
}
},
]
},
]
}
},
]

View File

@@ -0,0 +1,83 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/codegen/schema"
"github.com/cortezaproject/corteza/server/app"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "gocode/rbac/$component_access_control.go.tpl"
output: "\(cmp.ident)/service/access_control.gen.go"
payload: {
package: "service"
imports: [
"\"github.com/cortezaproject/corteza/server/\(cmp.ident)/types\"",
]
// All known RBAC resources
resources: [
for res in cmp.resources if res.rbac != _|_ {
resFunc: "types.\(res.expIdent)RbacResource"
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
},
{
resFunc: "types.ComponentRbacResource"
component: true
},
]
// All possible RBAC operations on component and resources
// flattened
operations: [
for res in cmp.resources if res.rbac != _|_ for op in res.rbac.operations {
"op": op.handle
const: "types.\(res.expIdent)ResourceType"
resFunc: "types.\(res.expIdent)RbacResource"
goType: "types.\(res.expIdent)"
description: op.description
checkFuncName: op.checkFuncName
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
},
for op in cmp.rbac.operations {
"op": op.handle
const: "types.ComponentResourceType"
resFunc: "types.ComponentRbacResource"
goType: "types.Component"
description: op.description
checkFuncName: op.checkFuncName
component: true
},
]
// Operation/resource validators, grouped by resource
loaders: [
for res in cmp.resources if res.rbac != _|_ {
const: "types.\(res.expIdent)ResourceType"
resFunc: "types.\(res.expIdent)RbacResource"
funcName: "load\(res.expIdent)"
refIndex: [ { 0 }, for i, p in res.parents {i + 1} ]
},
]
// Operation/resource validators, grouped by resource
validation: [
for res in cmp.resources if res.rbac != _|_ {
label: res.ident
const: "types.\(res.expIdent)ResourceType"
funcName: "rbac\(res.expIdent)ResourceValidator"
references: [ for p in res.parents {p.refField}, "ID"]
operations: [ for op in res.rbac.operations {op.handle}]
},
{
label: "\(cmp.ident) component"
const: "types.ComponentResourceType"
funcName: "rbacComponentResourceValidator"
operations: [ for op in cmp.rbac.operations {op.handle}]
},
]
}
},
]

View File

@@ -0,0 +1,41 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "gocode/rbac/$component_types_rbac.go.tpl"
output: "\(cmp.ident)/types/rbac.gen.go"
payload: {
package: "types"
cmpIdent: cmp.ident
// Operation/resource validators, grouped by resource
types: [
for res in cmp.resources if res.rbac != _|_ {
const: "\(res.expIdent)ResourceType"
type: res.fqrt
resFunc: "\(res.expIdent)RbacResource"
tplFunc: "\(res.expIdent)RbacResourceTpl"
attFunc: "\(res.expIdent)RbacAttributes"
goType: res.expIdent
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
},
{
const: "ComponentResourceType"
type: cmp.fqrt
resFunc: "ComponentRbacResource"
tplFunc: "ComponentRbacResourceTpl"
attFunc: "ComponentRbacAttributes"
goType: "Component"
component: true
},
]
}
},
]

View File

@@ -0,0 +1,251 @@
package codegen
import (
"strings"
"list"
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
_StoreResource: {
res = "res": schema.#Resource
typePkg = "typePkg": string
pkAttrNames: [string, ...]
hasPrimaryKey: res.model.indexes.primary != _|_
if hasPrimaryKey {
// primary key flag is no longer explicitly set on
// the attribute but via model.indexes.primary
pkAttrNames: [
for f in res.model.indexes.primary.fields { f.attribute }
]
}
result: {
ident: res.store.ident
identPlural: res.store.identPlural
expIdent: res.store.expIdent
expIdentPlural: res.store.expIdentPlural
modelIdent: res.model.ident
goType: "\(typePkg).\(res.expIdent)"
goSetType: "\(typePkg).\(res.expIdent)Set"
goFilterType: "\(typePkg).\(res.filter.expIdent)"
struct: [ for attr in res.model.attributes if attr.store {
"ident": attr.ident
"expIdent": attr.expIdent
"storeIdent": attr.storeIdent
"name": attr.name
"primaryKey": list.Contains(pkAttrNames, attr.name)
"ignoreCase": attr.ignoreCase
"goType": strings.Replace(attr.goType, "types.", "\(typePkg).", 1)
}]
filter: {
// query fields as defined in struct
"query": [ for name in res.filter.query {res.model.attributes[name]}],
// filter by nil state as defined in filter
"byNilState": [ for name in res.filter.byNilState {res.filter.struct[name]}]
// filter by false as defined in filter
"byFalseState": [ for name in res.filter.byFalseState {res.filter.struct[name]}]
// filter by value as defined in filter
// @todo this should be pulled from the struct
"byValue": [ for name in res.filter.byValue {res.filter.struct[name]}]
"byLabel": res.features.labels
"byFlag": res.features.flags
}
auxIdent: "aux\(expIdent)"
auxStruct: struct
features: {
paging: res.features.paging
sorting: res.features.sorting
checkFn: res.features.checkFn
}
api: {
if res.store.api != _|_ {
_base: {
"ident": res.store.ident
"expStoreIdent": res.store.expIdentPlural
"goType": goType
"goFilterType": goFilterType
"auxIdent": auxIdent
}
if hasPrimaryKey {
deleteByPK: {
"pkAttrNames": pkAttrNames,
attributes: [ for attr in pkAttrNames { res.model.attributes[attr] } ]
_expIdents: strings.Join([ for attr in pkAttrNames { res.model.attributes[attr].expIdent } ], "")
"expFnIdent": "Delete\(res.store.expIdent)By\(_expIdents)"
}
}
lookups: [
for l in res.store.api.lookups {
_base
"expFnIdent": l.expIdent
if (l.description != _|_) {
description: "// \(l.expIdent) " + strings.Join(strings.Split(l.description, "\n"), "\n// ")
}
// Copy all relevant fields from the struct
"args": [
for name in l.fields {
let attr = res.model.attributes[name]
"ident": attr.ident
"storeIdent": attr.storeIdent
"goType": attr.goType
"ignoreCase": attr.ignoreCase
},
]
"nullConstraint": l.nullConstraint
"returnType": "\(goType)"
"collectionFnIdent": "\(res.store.ident)Collection"
},
]
// all additional store functions we need for this resource
functions: [
for f in res.store.api.functions {
_base
"expFnIdent": f.expIdent
if (f.description != _|_) {
description: "// \(f.expIdent) " + strings.Join(strings.Split(f.description, "\n"), "\n// ")
}
"args": [ for a in f.args {
"ident": a.ident
"goType": strings.Replace(a.goType, "types.", "\(typePkg).", 1)
"spread": a.spread
}]
"return": [ for r in f.return {strings.Replace(r, "types.", "\(typePkg).", 1)}]
},
]
sortableFields: {
_base
"fnIdent": "sortable\(expIdent)Fields"
fields: {
for attr in res.model.attributes if attr.sortable || attr.unique || list.Contains(pkAttrNames, attr.name) {
{
"\(strings.ToLower(attr.name))": attr.name
"\(strings.ToLower(attr.ident))": attr.name
}
}
}
}
collectCursorValues: {
_base
"fnIdent": "collect\(expIdent)CursorValues"
fields: [ for attr in res.model.attributes if attr.sortable || attr.unique || list.Contains(pkAttrNames, attr.name) {
attr
"primaryKey": list.Contains(pkAttrNames, attr.name)
} ]
primaryKeys: [ for attr in res.model.attributes if list.Contains(pkAttrNames, attr.name) {attr} ]
}
checkConstraints: {
_base
"fnIdent": "check\(expIdent)Constraints"
checks: [
for lookup in res.store.api.lookups if lookup.constraintCheck {
lookupFnIdent: lookup.expIdent
fields: [ for name in lookup.fields {res.model.attributes[name]}]
nullConstraint: [
for f in res.model.attributes if list.Contains(lookup.nullConstraint, f.name) {
"expIdent": f.expIdent
},
]
},
]
}
}
}
}
}
// Codegen template payload, reused for multiple outputs
_payload: {
package: string | *"store"
imports: {
// per-component type imports
for cmp in app.corteza.components for res in cmp.resources if res.store != _|_ {
"github.com/cortezaproject/corteza/server/\(cmp.ident)/types": "\(cmp.ident)Type"
}
for res in app.corteza.resources if res.store != _|_ {
"\(res.package.import)": "\(res.package.ident)Type"
}
for cmp in app.corteza.components for res in cmp.resources for i in res.imports {
"\(i.import)": ""
}
}
types: {
// for each resource in every store with store and actions defined
for cmp in app.corteza.components for res in cmp.resources if res.store != _|_ {
// use _Store resource as a function (https://cuetorials.com/patterns/functions/)
// and pass res(ource) and type-package string in as "arguments"
"\(res.store.ident)": { _StoreResource & { "res": res, "typePkg": "\(cmp.ident)Type" } }.result
},
for res in app.corteza.resources if res.store != _|_ {
"\(res.store.ident)": { _StoreResource & { "res": res, "typePkg": "\(res.package.ident)Type" } }.result
}
}
}
[...schema.#codegen] &
[
{
"bulk": [
{
"template": "gocode/store/interfaces.go.tpl"
"output": "store/interfaces.gen.go"
}, {
"template": "gocode/store/rdbms/rdbms.go.tpl"
"output": "store/adapters/rdbms/rdbms.gen.go"
}, {
"template": "gocode/store/rdbms/aux_types.go.tpl"
"output": "store/adapters/rdbms/aux_types.gen.go"
}, {
"template": "gocode/store/rdbms/queries.go.tpl"
"output": "store/adapters/rdbms/queries.gen.go"
}, {
"template": "gocode/store/rdbms/filters.go.tpl"
"output": "store/adapters/rdbms/filters.gen.go"
}, {
"template": "gocode/store/tests/all.go.tpl"
"output": "store/tests/all_test.go"
},
]
"payload": { _payload }
},
]

View File

@@ -0,0 +1,40 @@
package codegen
import (
"github.com/cortezaproject/corteza/server/app"
"github.com/cortezaproject/corteza/server/codegen/schema"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "gocode/types/$component_resources.go.tpl"
output: "\(cmp.ident)/types/resources.gen.go"
payload: {
package: "types"
cmpIdent: cmp.ident
// Operation/resource validators, grouped by resource
types: [
for res in cmp.resources {
const: "\(res.expIdent)ResourceType"
type: res.fqrt
},
{
const: "ComponentResourceType"
type: cmp.fqrt
},
]
}
},
for cmp in app.corteza.components {
template: "gocode/types/$component_getters_setters.go.tpl"
output: "\(cmp.ident)/types/getters_setters.gen.go"
payload: {
package: "types"
cmpIdent: cmp.ident
resources: [ for res in cmp.resources { res }]
}
},
]

101
server/codegen/tool/main.go Normal file
View File

@@ -0,0 +1,101 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path"
"text/template"
"time"
"github.com/cortezaproject/corteza/server/pkg/cli"
)
type (
inOut struct {
Template string `json:"template"`
Output string `json:"output"`
Syntax string `json:"syntax"`
}
task struct {
inOut
Bulk []inOut `json:"bulk"`
Payload interface{} `json:"payload"`
}
)
var (
verbose bool
showHelp bool
tplRootPath string
outputBase string
)
func init() {
flag.BoolVar(&showHelp, "h", false, "show help")
flag.BoolVar(&verbose, "v", false, "be verbose")
flag.StringVar(&tplRootPath, "p", "codegen/assets/templates", "location of the template files")
flag.StringVar(&outputBase, "b", ".", "base dir for output")
flag.Parse()
}
// Takes JSON input with codegen tasks and definitions and generates files
func main() {
if showHelp {
flag.PrintDefaults()
os.Exit(0)
}
var (
input = json.NewDecoder(os.Stdin)
tasks = make([]*task, 0)
tpl *template.Template
err error
)
started := time.Now()
print("Waiting for stdin ...")
if err = input.Decode(&tasks); err != nil {
cli.HandleError(fmt.Errorf("failed to decode input from standard input: %v", err))
}
println(time.Now().Sub(started).Round(time.Second)/time.Second, "sec")
if tpl, err = loadTemplates(baseTemplate(), tplRootPath); err != nil {
cli.HandleError(fmt.Errorf("failed to load templates: %v", err))
}
for _, j := range tasks {
if len(j.Bulk) == 0 {
j.Bulk = append(j.Bulk, j.inOut)
}
for _, o := range j.Bulk {
output := path.Join(outputBase, o.Output)
print(fmt.Sprintf("generating %s (from %s) ...", output, o.Template))
switch o.Syntax {
case "go":
err = writeFormattedGo(output, tpl.Lookup(o.Template), j.Payload)
default:
err = write(output, tpl.Lookup(o.Template), j.Payload)
}
if err != nil {
cli.HandleError(fmt.Errorf("failed to write template: %v", err))
} else {
print("done\n")
}
}
}
}
func print(msg string) {
if verbose {
_, _ = fmt.Fprint(os.Stderr, msg)
}
}

View File

@@ -0,0 +1,105 @@
package main
import (
"bytes"
"fmt"
"go/format"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/sprig"
)
func baseTemplate() *template.Template {
return template.New("").
Funcs(sprig.TxtFuncMap())
}
func loadTemplates(rTpl *template.Template, rootDir string) (*template.Template, error) {
cleanRoot := filepath.Clean(rootDir)
pfx := len(cleanRoot) + 1
return rTpl, filepath.Walk(cleanRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || !strings.HasSuffix(path, ".tpl") || err != nil {
return err
}
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
name := path[pfx:]
rTpl, err = rTpl.New(name).Parse(string(b))
return err
})
}
func writeFormattedGo(dst string, tpl *template.Template, payload interface{}) error {
return write(dst, tpl, payload, func(in io.ReadWriter) (out io.ReadWriter, err error) {
var (
org, bb []byte
)
if org, err = ioutil.ReadAll(in); err != nil {
return
}
cp := bytes.NewBuffer(org)
if bb, err = format.Source(cp.Bytes()); err != nil {
// output error and return un-formatted source
_, _ = fmt.Fprintf(os.Stderr, "%s fmt warn: %v\n", dst, err)
return cp, nil
}
return bytes.NewBuffer(bb), nil
})
}
func write(dst string, tpl *template.Template, payload interface{}, pp ...func(io.ReadWriter) (io.ReadWriter, error)) (err error) {
var (
output io.WriteCloser
buf io.ReadWriter
)
if tpl == nil {
return fmt.Errorf("could not find template for %s", dst)
}
buf = &bytes.Buffer{}
if err := tpl.Execute(buf, payload); err != nil {
return err
}
for _, proc := range pp {
if buf, err = proc(buf); err != nil {
return
}
}
if dst == "" || dst == "-" {
output = os.Stdout
} else {
if output, err = os.Create(dst); err != nil {
return err
}
defer output.Close()
}
if _, err = io.Copy(output, buf); err != nil {
return err
}
return nil
}