snapshot of https://github.com/cortezaproject/corteza.git (2024.9.x, 2026-09-18; blobs > 131072B omitted)
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
Some checks failed
SonarQube / SonarQube (push) Failing after 5m28s
E2e test on main branch push / server and client test (map[name:admin port:8080]) (push) Failing after 3m33s
Server and lib tests / Library test (js) (push) Failing after 1m36s
Server and lib tests / Library test (vue) (push) Successful in 2m24s
Server and lib tests / Web client test (admin) (push) Successful in 2m52s
Server and lib tests / Web client test (compose) (push) Successful in 3m23s
Server and lib tests / Web client test (discovery) (push) Successful in 2m49s
Server and lib tests / Web client test (one) (push) Successful in 2m52s
Server and lib tests / Web client test (privacy) (push) Successful in 2m44s
Server and lib tests / Web client test (reporter) (push) Successful in 2m56s
Server and lib tests / Web client test (workflow) (push) Successful in 3m0s
Server and lib tests / Server tests (push) Failing after 4m55s
E2e test on main branch push / notify-failure (push) Failing after 9s
Server and lib tests / Send matrix testing notification (push) Successful in 2s
Mark stale issues and pull requests / stale (push) Successful in 40s
This commit is contained in:
38
server/codegen/assets/templates/docs/.env.example.tpl
Normal file
38
server/codegen/assets/templates/docs/.env.example.tpl
Normal 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 }}
|
||||
39
server/codegen/assets/templates/docs/options.adoc.tpl
Normal file
39
server/codegen/assets/templates/docs/options.adoc.tpl
Normal 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 }}
|
||||
@@ -0,0 +1,13 @@
|
||||
= {{ .label }}
|
||||
|
||||
[cols="1s,5a,5a"]
|
||||
|===
|
||||
| Operation| Description | Default
|
||||
|
||||
{{ range .operations }}
|
||||
| [#{{ .slug }}]#<<{{ .slug }},{{ .label }}>>#
|
||||
| {{ .description }}
|
||||
| Deny
|
||||
|
||||
{{ end }}
|
||||
|===
|
||||
13
server/codegen/assets/templates/docs/rbac.$resource.adoc.tpl
Normal file
13
server/codegen/assets/templates/docs/rbac.$resource.adoc.tpl
Normal file
@@ -0,0 +1,13 @@
|
||||
= {{ .label }}
|
||||
|
||||
[cols="1s,5a,5a"]
|
||||
|===
|
||||
| Operation| Description | Default
|
||||
|
||||
{{ range .operations }}
|
||||
| [#{{ .slug }}]#<<{{ .slug }},{{ .label }}>>#
|
||||
| {{ .description }}
|
||||
| Deny
|
||||
|
||||
{{ end }}
|
||||
|===
|
||||
11
server/codegen/assets/templates/docs/rbac.index.adoc.tpl
Normal file
11
server/codegen/assets/templates/docs/rbac.index.adoc.tpl
Normal file
@@ -0,0 +1,11 @@
|
||||
= {{ .label }}
|
||||
|
||||
:leveloffset: +1
|
||||
|
||||
include::./component.gen.adoc[]
|
||||
{{ range .resources }}
|
||||
include::./resource.{{ . }}.gen.adoc[]
|
||||
{{ end }}
|
||||
|
||||
:leveloffset: -1
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }}
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }}
|
||||
401
server/codegen/assets/templates/gocode/envoy/store_decode.go.tpl
Normal file
401
server/codegen/assets/templates/gocode/envoy/store_decode.go.tpl
Normal 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
|
||||
}
|
||||
513
server/codegen/assets/templates/gocode/envoy/store_encode.go.tpl
Normal file
513
server/codegen/assets/templates/gocode/envoy/store_encode.go.tpl
Normal 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()
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
33
server/codegen/assets/templates/gocode/envoy/utils.go.tpl
Normal file
33
server/codegen/assets/templates/gocode/envoy/utils.go.tpl
Normal 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,
|
||||
}
|
||||
)
|
||||
872
server/codegen/assets/templates/gocode/envoy/yaml_decode.go.tpl
Normal file
872
server/codegen/assets/templates/gocode/envoy/yaml_decode.go.tpl
Normal 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
|
||||
}
|
||||
305
server/codegen/assets/templates/gocode/envoy/yaml_encode.go.tpl
Normal file
305
server/codegen/assets/templates/gocode/envoy/yaml_encode.go.tpl
Normal 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()
|
||||
}
|
||||
@@ -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.
|
||||
//
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
140
server/codegen/assets/templates/gocode/store/interfaces.go.tpl
Normal file
140
server/codegen/assets/templates/gocode/store/interfaces.go.tpl
Normal 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 }}
|
||||
@@ -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 }}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 }}
|
||||
)
|
||||
798
server/codegen/assets/templates/gocode/store/rdbms/rdbms.go.tpl
Normal file
798
server/codegen/assets/templates/gocode/store/rdbms/rdbms.go.tpl
Normal 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 }}
|
||||
@@ -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 }}
|
||||
}
|
||||
@@ -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 }}
|
||||
@@ -0,0 +1,9 @@
|
||||
package {{ .package }}
|
||||
|
||||
{{ template "gocode/header-gentext.tpl" }}
|
||||
|
||||
const (
|
||||
{{- range .types }}
|
||||
{{ .const }} = {{ printf "%q" .type }}
|
||||
{{- end }}
|
||||
)
|
||||
Reference in New Issue
Block a user