Moving server files to ./server
This commit is contained in:
@@ -0,0 +1,2 @@
|
||||
/build
|
||||
tmp*.cue
|
||||
@@ -0,0 +1,26 @@
|
||||
.PHONY: *.cue
|
||||
|
||||
DEF_ALL = *.cue
|
||||
DEF_SERVER = server.*.cue
|
||||
DEF_DOCS = docs.*.cue
|
||||
|
||||
server: $(DEF_SERVER)
|
||||
docs: $(DEF_DOCS)
|
||||
all: $(DEF_SERVER) $(DEF_DOCS)
|
||||
|
||||
include ../Makefile.inc
|
||||
|
||||
$(DEF_SERVER): $(CUE) $(JSONTPLEXEC)
|
||||
@ echo "$(COLOUR_GREEN)Generating server files from $@$(COLOUR_END)"
|
||||
@ $(CUE) eval $@ --out json | $(JSONTPLEXEC) -v -p $(ROOT_DIR)/codegen/assets/templates -b $(ROOT_DIR)
|
||||
|
||||
ifeq ("${DOCS_DIR}", "")
|
||||
$(DEF_DOCS):
|
||||
@ echo "$(COLOUR_YELLOW)Skipping docs generation: DOCS_DIR is not set.$(COLOUR_END)"
|
||||
@ echo "$Run task like this: make "$@" DOCS_DIR=/path/to/corteza-docs"
|
||||
@ echo ""
|
||||
else
|
||||
$(DEF_DOCS): $(CUE) $(JSONTPLEXEC)
|
||||
@ echo "$(COLOUR_GREEN)Generating doc files from $@$(COLOUR_END) $(COLOUR_BLUE)(dst: $(DOCS_DIR))$(COLOUR_END)"
|
||||
@ $(CUE) eval $@ --out json | $(JSONTPLEXEC) -v -p $(ROOT_DIR)/codegen/assets/templates -b $(DOCS_DIR)
|
||||
endif
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
|===
|
||||
@@ -0,0 +1,13 @@
|
||||
= {{ .label }}
|
||||
|
||||
[cols="1s,5a,5a"]
|
||||
|===
|
||||
| Operation| Description | Default
|
||||
|
||||
{{ range .operations }}
|
||||
| [#{{ .slug }}]#<<{{ .slug }},{{ .label }}>>#
|
||||
| {{ .description }}
|
||||
| Deny
|
||||
|
||||
{{ end }}
|
||||
|===
|
||||
@@ -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,69 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
+24
@@ -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 }}
|
||||
@@ -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,386 @@
|
||||
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{UserID: 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
|
||||
//
|
||||
// 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 }}0,{{ 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
|
||||
//
|
||||
// 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 }}
|
||||
|
||||
@@ -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,112 @@
|
||||
package rdbms
|
||||
|
||||
{{ template "gocode/header-gentext.tpl" }}
|
||||
|
||||
import (
|
||||
"strings"
|
||||
{{- range $path, $alias := .imports }}
|
||||
{{ $alias }} {{ printf "%q" $path }}
|
||||
{{- end }}
|
||||
"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(f {{ .goFilterType }})(ee []goqu.Expression, _ {{ .goFilterType }}, err error) {
|
||||
{{ range .filter.byNilState }}
|
||||
if expr := stateNilComparison({{ printf "%q" .storeIdent }}, f.{{ .expIdent }}); expr != nil {
|
||||
ee = append(ee, expr)
|
||||
}
|
||||
{{ end }}
|
||||
|
||||
{{ range .filter.byFalseState }}
|
||||
if expr := stateFalseComparison({{ 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 }}
|
||||
)
|
||||
@@ -0,0 +1,637 @@
|
||||
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/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
|
||||
}
|
||||
|
||||
if err = s.Exec(ctx, {{ .ident }}UpsertQuery(s.Dialect.GOQU(), rr[i])); 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
|
||||
}
|
||||
}
|
||||
|
||||
// Make sure results are always sorted at least by primary keys
|
||||
if f.Sort.Get("id") == nil {
|
||||
f.Sort = append(f.Sort, &filter.SortExpr{
|
||||
Column: "id",
|
||||
Descending: f.Sort.LastDescending(),
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
// Cloned sorting instructions for the actual sorting
|
||||
// Original are passed to the 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 {
|
||||
// there are fewer items fetched then requested limit
|
||||
limit := f.Limit
|
||||
f.Limit = 0
|
||||
var navSet {{ .goSetType }}
|
||||
if navSet, _, _, err = s.fetchFullPageOf{{ .expIdentPlural }}(ctx, f, sort); err != nil {
|
||||
return
|
||||
} else {
|
||||
f.Total = uint(len(navSet))
|
||||
f.Limit = limit
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
{{ 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 }}
|
||||
)
|
||||
|
||||
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
|
||||
|
||||
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, err = s.Query{{ .expIdentPlural }}(ctx, tryFilter); err != nil {
|
||||
return nil, nil, nil, err
|
||||
}
|
||||
|
||||
if len(aux) == 0 {
|
||||
// nothing fetched
|
||||
break
|
||||
}
|
||||
|
||||
// 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 {
|
||||
// not enough items fetched, try again with adjusted limit
|
||||
limit = reqItems - collected
|
||||
|
||||
if limit < MinEnsureFetchLimit {
|
||||
// In case limit is set very low and we've missed records in the first fetch,
|
||||
// make sure next fetch limit is a bit higher
|
||||
limit = MinEnsureFetchLimit
|
||||
}
|
||||
|
||||
// Update cursor so that it points to the last item fetched
|
||||
tryFilter.PageCursor = s.{{ .api.collectCursorValues.fnIdent }}(set[collected-1], filter.Sort...)
|
||||
|
||||
// Copy reverse flag from sorting
|
||||
tryFilter.PageCursor.LThen = filter.Sort.Reversed()
|
||||
continue
|
||||
}
|
||||
|
||||
if reqItems < collected {
|
||||
set = set[:reqItems]
|
||||
}
|
||||
|
||||
break
|
||||
}
|
||||
|
||||
collected := len(set)
|
||||
|
||||
if collected == 0 {
|
||||
return nil, nil, nil, nil
|
||||
}
|
||||
|
||||
if reversedOrder {
|
||||
// Fetched set needs to be reversed because we've forced a descending order to get the previous page
|
||||
for i, j := 0, collected-1; i < j; i, j = i+1, j-1 {
|
||||
set[i], set[j] = set[j], set[i]
|
||||
}
|
||||
|
||||
// when in reverse-order rules on what cursor to return change
|
||||
hasPrev, hasNext = hasNext, hasPrev
|
||||
}
|
||||
|
||||
if hasPrev {
|
||||
prev = s.{{ .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
|
||||
}
|
||||
{{ 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, 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(f)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = fmt.Errorf("could 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 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
|
||||
}
|
||||
|
||||
{{ 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, err
|
||||
{{ else }}
|
||||
return set, false, 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 }}
|
||||
goqu.I({{ printf "%q" . }}).IsNull(),
|
||||
{{- 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
|
||||
//
|
||||
// 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 issue:
|
||||
// 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) {
|
||||
for _, c := range cc {
|
||||
switch c.Column {
|
||||
{{- range .fields }}
|
||||
case {{ printf "%q" .ident }}:
|
||||
cur.Set(c.Column, res.{{ .expIdent }}, c.Descending)
|
||||
|
||||
{{- if .primaryKey }}
|
||||
pk{{ .expIdent }} = true
|
||||
{{- else if .unique }}
|
||||
hasUnique = true
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
_ = 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,9 @@
|
||||
package {{ .package }}
|
||||
|
||||
{{ template "gocode/header-gentext.tpl" }}
|
||||
|
||||
const (
|
||||
{{- range .types }}
|
||||
{{ .const }} = {{ printf "%q" .type }}
|
||||
{{- end }}
|
||||
)
|
||||
@@ -0,0 +1,24 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
{
|
||||
template: "docs/options.adoc.tpl"
|
||||
output: "src/modules/generated/partials/env-options.gen.adoc"
|
||||
payload: {
|
||||
groups: [
|
||||
for g in app.corteza.options {
|
||||
title: g.title
|
||||
intro?: g.intro
|
||||
|
||||
options: g.options
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,67 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
)
|
||||
|
||||
#_indexPayload: {
|
||||
label: string
|
||||
resources: [...string]
|
||||
}
|
||||
|
||||
#_operationsPayload: {
|
||||
label: string
|
||||
|
||||
operations: [...{
|
||||
slug: string
|
||||
label: string
|
||||
description: string
|
||||
}]
|
||||
}
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
for cmp in app.corteza.components {
|
||||
template: "docs/rbac.index.adoc.tpl"
|
||||
output: "src/modules/generated/partials/access-control/\(cmp.handle)/index.gen.adoc"
|
||||
payload: #_indexPayload & {
|
||||
label: cmp.label
|
||||
resources: [ for res in cmp.resources { res.handle } ]
|
||||
}
|
||||
}
|
||||
] +
|
||||
[
|
||||
for cmp in app.corteza.components {
|
||||
template: "docs/rbac.$component.adoc.tpl"
|
||||
output: "src/modules/generated/partials/access-control/\(cmp.handle)/component.gen.adoc"
|
||||
payload: #_operationsPayload & {
|
||||
label: cmp.label
|
||||
|
||||
operations: [
|
||||
for op in cmp.rbac.operations {
|
||||
slug: "rbac-\(cmp.handle)-\(op.handle)"
|
||||
label: op.handle
|
||||
description: op.description
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
] +
|
||||
[
|
||||
for cmp in app.corteza.components for res in cmp.resources {
|
||||
template: "docs/rbac.$resource.adoc.tpl"
|
||||
output: "src/modules/generated/partials/access-control/\(cmp.handle)/resource.\(res.handle).gen.adoc"
|
||||
payload: #_operationsPayload & {
|
||||
label: res.handle
|
||||
|
||||
operations: [
|
||||
for op in res.rbac.operations {
|
||||
slug: "rbac-\(res.handle)-\(op.handle)"
|
||||
label: op.handle
|
||||
description: op.description
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,19 @@
|
||||
package schema
|
||||
|
||||
#_ioSpec: {
|
||||
template: string
|
||||
output: string
|
||||
|
||||
syntax: string | *"go"
|
||||
if output =~ "\\.adoc$" {
|
||||
syntax: "adoc"
|
||||
}
|
||||
}
|
||||
|
||||
#codegen: {
|
||||
#_ioSpec
|
||||
payload: _
|
||||
} | {
|
||||
bulk?: [...#_ioSpec]
|
||||
payload: _
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
#component: #_base & {
|
||||
// copy field values from #_base
|
||||
handle: handle, ident: ident, expIdent: expIdent
|
||||
|
||||
label: strings.ToTitle(ident)
|
||||
platform: #baseHandle
|
||||
|
||||
resources: {
|
||||
[key=_]: {"handle": key, "component": handle, "platform": platform} & #Resource
|
||||
}
|
||||
|
||||
fqrt: platform + "::" + handle
|
||||
|
||||
// All known RBAC operations for this component
|
||||
rbac: #rbacComponent & {
|
||||
operations: {
|
||||
grant: {
|
||||
description: "Manage \(handle) permissions"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"list"
|
||||
)
|
||||
|
||||
#locale: {
|
||||
resourceExpIdent: #expIdent
|
||||
|
||||
// @todo we need a better name here!
|
||||
skipSvc: bool | *false
|
||||
|
||||
extended: bool | *false
|
||||
|
||||
resource: {
|
||||
type: string
|
||||
const: string | *("\(resourceExpIdent)ResourceTranslationType")
|
||||
}
|
||||
|
||||
keys: {
|
||||
[key=_]: #localeKey & {
|
||||
name: key
|
||||
_resourceExpIdent: resourceExpIdent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#localeKey: {
|
||||
name: #handle
|
||||
_resourceExpIdent: #expIdent
|
||||
|
||||
path: [...(#ident | {part: #ident, var: bool | *false})] | *([name])
|
||||
|
||||
expandedPath: [ for p in path {
|
||||
if (p & {"p": #ident}) != _|_ {p, var: p.var}
|
||||
if (p & string) != _|_ {"part": p, var: false}
|
||||
}]
|
||||
|
||||
_suffix: strings.Join([ for p in expandedPath {strings.ToTitle(p.part)}], "")
|
||||
|
||||
struct: string | *("LocaleKey" + _resourceExpIdent + _suffix)
|
||||
|
||||
// As soon as we use vars in the path,
|
||||
// custom handler must be present
|
||||
_hasVars: list.Contains([ for p in path {p.var | false}], true)
|
||||
customHandler: bool | *_hasVars
|
||||
|
||||
if customHandler {
|
||||
decodeFunc: string | *("decodeTranslations" + _suffix)
|
||||
encodeFunc: string | *("encodeTranslations" + _suffix)
|
||||
serviceFunc: string | *("handle" + _resourceExpIdent + _suffix)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
#Model: {
|
||||
ident: string
|
||||
attributes: {
|
||||
[name=_]: { "name": name }
|
||||
} & {
|
||||
[string]: #ModelAttribute
|
||||
}
|
||||
|
||||
indexes: ({
|
||||
[name=_]: { "name": name, "modelIdent": ident } & #ModelIndex
|
||||
} & {
|
||||
[string]: #ModelIndex
|
||||
}) | *({})
|
||||
}
|
||||
|
||||
|
||||
// logic in struct fields is a bit different
|
||||
#ModelAttribute: {
|
||||
name: #ident
|
||||
_words: strings.Replace(strings.Replace(name, "_", " ", -1), ".", " ", -1)
|
||||
|
||||
_ident: strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
|
||||
|
||||
// Golang type (built-in or other)
|
||||
goType: string | *"string"
|
||||
|
||||
// lowercase (unexported, golang) identifier
|
||||
ident: #ident | *_ident
|
||||
|
||||
// uppercase (exported, golang) identifier
|
||||
expIdent: #expIdent | *strings.ToTitle(ident)
|
||||
|
||||
// store identifier
|
||||
// @todo this should be moved to dal.ident
|
||||
storeIdent: #ident | *name
|
||||
|
||||
// enable or disable store for this attribute
|
||||
// @todo we should use dal prop for this, and extend it to support bool "false"
|
||||
// so that it can be disabled
|
||||
store: bool | *true
|
||||
|
||||
unique: bool | *false
|
||||
sortable: bool | *false
|
||||
descending: bool | *false
|
||||
ignoreCase: bool | *false
|
||||
|
||||
// currently disabled since not used by anything
|
||||
// it adds more than 4x overhead to the time it takes to generate the store code!
|
||||
// #ModelAttributeJsonTag
|
||||
|
||||
dal?: #ModelAttributeDal
|
||||
}
|
||||
|
||||
#ModelAttributeDal: {
|
||||
type: #ModelAttributeDalType | *"Text"
|
||||
|
||||
fqType: "dal.Type\(type)"
|
||||
|
||||
nullable: bool | *false
|
||||
|
||||
|
||||
if type == "ID" {
|
||||
generatedByStore: bool | *false
|
||||
default?: 0
|
||||
}
|
||||
|
||||
if type == "Ref" {
|
||||
refModelResType: #FQRT
|
||||
attribute: #handle | *"id"
|
||||
default?: 0
|
||||
}
|
||||
|
||||
if type == "Timestamp" {
|
||||
timezone: bool | *false
|
||||
precision: number | *(-1)
|
||||
defaultCurrentTimestamp?: true
|
||||
}
|
||||
|
||||
if type == "Time" {
|
||||
timezone: bool | *false
|
||||
precision: number | *(-1)
|
||||
defaultCurrentTimestamp?: true
|
||||
}
|
||||
|
||||
if type == "Date" {
|
||||
defaultCurrentTimestamp?: true
|
||||
}
|
||||
|
||||
if type == "Number" {
|
||||
precision: number | *(-1)
|
||||
scale: number | *(-1)
|
||||
default?: number
|
||||
meta?: { [string]: _ }
|
||||
}
|
||||
|
||||
if type == "Text" {
|
||||
length: number | *0
|
||||
default?: string
|
||||
}
|
||||
|
||||
if type == "Boolean" {
|
||||
default?: bool
|
||||
}
|
||||
|
||||
if type == "Enum" {
|
||||
values: []
|
||||
default?: string
|
||||
}
|
||||
|
||||
if type == "Geometry" {}
|
||||
|
||||
if type == "JSON" {
|
||||
default?: string | bytes
|
||||
defaultEmptyObject?: true
|
||||
}
|
||||
|
||||
if type == "Blob" {
|
||||
default?: bytes
|
||||
}
|
||||
|
||||
if type == "UUID" {}
|
||||
}
|
||||
|
||||
#ModelAttributeDalType:
|
||||
"ID" | "Ref" |
|
||||
"Timestamp" | "Time" | "Date" |
|
||||
"Number" |
|
||||
"Text" |
|
||||
"Boolean" |
|
||||
"Enum" |
|
||||
"Geometry" |
|
||||
"JSON" |
|
||||
"Blob" |
|
||||
"UUID"
|
||||
|
||||
|
||||
IdField: {
|
||||
// Expecting ID field to always have name ID
|
||||
name: "id"
|
||||
expIdent: "ID"
|
||||
unique: true
|
||||
|
||||
// @todo someday we'll replace this with the "ID" type
|
||||
goType: "uint64"
|
||||
dal: { type: "ID" }
|
||||
}
|
||||
HandleField: {
|
||||
// Expecting ID field to always have name handle
|
||||
name: "handle"
|
||||
unique: true
|
||||
ignoreCase: true
|
||||
|
||||
goType: "string"
|
||||
dal: { type: "Text", length: 64 }
|
||||
}
|
||||
|
||||
AttributeUserRef: {
|
||||
goType: "uint64"
|
||||
dal: { type: "Ref", refModelResType: "corteza::system:user", default: 0 }
|
||||
}
|
||||
|
||||
SortableTimestampField: {
|
||||
sortable: true
|
||||
goType: "time.Time"
|
||||
dal: { type: "Timestamp", timezone: true, nullable: false }
|
||||
}
|
||||
|
||||
SortableTimestampNowField: {
|
||||
sortable: true
|
||||
goType: "time.Time"
|
||||
dal: { type: "Timestamp", timezone: true, nullable: false, defaultCurrentTimestamp: true }
|
||||
}
|
||||
|
||||
SortableTimestampNilField: {
|
||||
sortable: true
|
||||
goType: "*time.Time"
|
||||
dal: { type: "Timestamp", timezone: true, nullable: true }
|
||||
}
|
||||
|
||||
#ModelAttributeJsonTag: {
|
||||
name: string
|
||||
|
||||
_specs: {field: string | *name, omitEmpty: bool | *false, "string": bool | *false}
|
||||
|
||||
json: string | _specs | bool | *false
|
||||
jsonTag?: string
|
||||
|
||||
// just wrap whatever we got in json
|
||||
if (json & string) != _|_ {
|
||||
jsonTag: "json:\"\(json)\""
|
||||
}
|
||||
|
||||
// json enable,d wrap with ident as a JSON prop name
|
||||
if (json & bool) != _|_ && json {
|
||||
// generic json tag
|
||||
jsonTag: "json:\"\(name)\""
|
||||
}
|
||||
|
||||
// full-specs
|
||||
if (json & bool) == _|_ && (json & _specs) != _|_ {
|
||||
_omitEmpty: string | *""
|
||||
if json.omitEmpty {
|
||||
_omitEmpty: ",omitempty"
|
||||
}
|
||||
_string: string | *""
|
||||
if json.string {
|
||||
_string: ",string"
|
||||
}
|
||||
|
||||
jsonTag: "json:\"\(json.field)\(_omitEmpty)\(_string)\""
|
||||
}
|
||||
}
|
||||
|
||||
#ModelIndex: close({
|
||||
name: #ident
|
||||
modelIdent: #ident
|
||||
_attributes: { [_]: #ModelAttribute }
|
||||
_words: strings.Replace(strings.Replace(name, "_", " ", -1), ".", " ", -1)
|
||||
|
||||
_ident: strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
|
||||
|
||||
// lowercase (unexported, golang) identifier
|
||||
ident: #ident | *"\(modelIdent)_\(_ident)"
|
||||
|
||||
primary: bool | *(strings.ToLower(name) == "primary")
|
||||
unique: bool | *(strings.Contains(name, "unique") || primary)
|
||||
|
||||
type: "BTREE" | *"BTREE"
|
||||
|
||||
// index predicate,
|
||||
// condition that must be met for the index to be used
|
||||
predicate?: string
|
||||
|
||||
attribute?: string
|
||||
attributes?: [string, ...]
|
||||
fields: [#ModelIndexField, ...]
|
||||
|
||||
if attribute != _|_ {
|
||||
attributes: [string, ...] & [attribute]
|
||||
}
|
||||
|
||||
if fields != _|_ && attributes != _|_ {
|
||||
fields: [
|
||||
for a in attributes {
|
||||
{"attribute": a} & #ModelIndexField
|
||||
}
|
||||
]
|
||||
}
|
||||
})
|
||||
|
||||
#IndexFieldModifier: "LOWERCASE"
|
||||
|
||||
#ModelIndexField: close({
|
||||
attribute: string
|
||||
modifiers?: [#IndexFieldModifier, ...]
|
||||
length?: number
|
||||
sort?: "DESC" | "ASC"
|
||||
nulls?: "FIRST" | "LAST"
|
||||
})
|
||||
@@ -0,0 +1,49 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
#_ENV: =~"^[A-Z][A-Z0-9_]*[A-Z0-9]?$"
|
||||
//#_optName: =~ "^[a-zA-Z][a-zA-Z0-9\\s]*[a-zA-Z0-9]+$"
|
||||
|
||||
#optionsGroup: #_base & {
|
||||
imports: [...string] | *([])
|
||||
|
||||
handle: #handle
|
||||
|
||||
title: string | *handle
|
||||
description?: string
|
||||
|
||||
env: #_ENV | *(strings.ToUpper(strings.Replace(handle, "-", "_", -1)))
|
||||
_envPrefix: env
|
||||
|
||||
options: {
|
||||
[_opt=_]: #option & {
|
||||
handle: _opt
|
||||
env: #_ENV | *(_envPrefix + "_" + strings.ToUpper(strings.Replace(handle, " ", "_", -1)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#option: {
|
||||
handle: #handle
|
||||
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
|
||||
|
||||
// lowercased (unexported, golang) identifier
|
||||
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
|
||||
|
||||
// upercased (exported, golang) identifier
|
||||
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
|
||||
|
||||
type: string | *"string"
|
||||
description?: string
|
||||
|
||||
// Default expression to be used
|
||||
defaultGoExpr?: string
|
||||
|
||||
env?: #_ENV
|
||||
|
||||
// Plain default value to use when generating .env.example
|
||||
defaultValue?: string
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package schema
|
||||
|
||||
#platform: {
|
||||
ident: #baseHandle | *"corteza"
|
||||
|
||||
options: [...#optionsGroup]
|
||||
|
||||
components: [...{platform: ident} & #component]
|
||||
|
||||
resources: {
|
||||
[key=#handle]: #Resource & {
|
||||
"handle": key,
|
||||
"platform": ident
|
||||
}
|
||||
}
|
||||
|
||||
// automation: {
|
||||
// types: ....
|
||||
// function ....
|
||||
// }
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
#rbacComponent: {
|
||||
resource: {
|
||||
type: string
|
||||
}
|
||||
|
||||
operations: {
|
||||
[key=_]: #rbacOperation & {
|
||||
handle: key
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#rbacResource: {
|
||||
resourceExpIdent: #expIdent
|
||||
|
||||
operations: {
|
||||
[key=_]: #rbacOperation & {
|
||||
handle: key
|
||||
_resourceExpIdent: resourceExpIdent
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#rbacOperation: {
|
||||
handle: #handle
|
||||
description: string | *handle
|
||||
_resourceExpIdent?: string
|
||||
|
||||
// Some string manipulation that will result in
|
||||
// more pronouncable access-control check function name
|
||||
|
||||
// When check function name is not explicitly defined we try
|
||||
// to use resource and operation name and generate easy-to-read name
|
||||
//
|
||||
// <res> + <op> => Can<Op><Res>
|
||||
// <res> + <op:foo.bar.verb> => Can<Verb><Foo><Bar>On<Res>
|
||||
|
||||
_operation: strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1)
|
||||
_opSplit: strings.Split(_operation, ".")
|
||||
|
||||
_opFlip: [_opSplit[len(_opSplit)-1]] + _opSplit[0:len(_opSplit)-1]
|
||||
_opFinal: strings.Replace(strings.ToTitle(strings.Join(_opFlip, " ")), " ", "", -1)
|
||||
|
||||
if _resourceExpIdent == _|_ {
|
||||
checkFuncName: #expIdent | *"Can\(_opFinal)"
|
||||
}
|
||||
|
||||
if len(_opSplit) > 1 && _resourceExpIdent != _|_ {
|
||||
checkFuncName: #expIdent | *"Can\(_opFinal)On\(_resourceExpIdent)"
|
||||
}
|
||||
|
||||
if len(_opSplit) <= 1 && _resourceExpIdent != _|_ {
|
||||
checkFuncName: #expIdent | *"Can\(_opFinal)\(_resourceExpIdent)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,137 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
// fully qualified resource type
|
||||
#FQRT: =~ "^corteza::(compose|system|federation|automation):[a-z][a-z0-9-]*$"
|
||||
|
||||
#Resource: {
|
||||
#_base
|
||||
|
||||
// type: #resourceType | *""
|
||||
|
||||
imports: [...{ import: string }]
|
||||
|
||||
// copy field values from #_base
|
||||
handle: handle, ident: ident, expIdent: expIdent
|
||||
|
||||
component: #baseHandle | *"component"
|
||||
platform: #baseHandle | *"corteza"
|
||||
|
||||
// Fully qualified resource name
|
||||
fqrt: #FQRT | *(platform + "::" + component + ":" + handle)
|
||||
|
||||
model: #Model & {
|
||||
// use resource handle (plural) as model ident as default
|
||||
// model ident represents a db table or a container name
|
||||
ident: string | *"\(strings.Replace(handle, "-", "_", -1))s"
|
||||
}
|
||||
|
||||
filter: {
|
||||
"expIdent": #expIdent | *"\(expIdent)Filter"
|
||||
|
||||
struct: {
|
||||
[name=_]: {"name": name} & #ModelAttribute
|
||||
}
|
||||
|
||||
// generate filtering by-nil-state for the specified fields
|
||||
"byNilState": [...string]
|
||||
|
||||
// generate filtering by-false-state for the specified fields
|
||||
"byFalseState": [...string]
|
||||
|
||||
// generate query filter for the specified fields
|
||||
"query": [...string]
|
||||
|
||||
// filter resources by fields (eq)
|
||||
"byValue": [...string]
|
||||
}
|
||||
// operations: #Operations
|
||||
|
||||
features: {
|
||||
// filtering by label
|
||||
labels: bool | *true
|
||||
|
||||
// filtering by flag
|
||||
flags: bool | *false
|
||||
|
||||
// support pagination
|
||||
paging: bool | *true
|
||||
|
||||
// support sorting
|
||||
sorting: bool | *true
|
||||
|
||||
// support resource check function
|
||||
checkFn: bool | *true
|
||||
}
|
||||
|
||||
// All parent resources
|
||||
parents: [... #_base & {
|
||||
// copy field values from #_base
|
||||
handle: handle, ident: ident, expIdent: expIdent
|
||||
|
||||
refField: #expIdent | *(expIdent + "ID")
|
||||
param: #ident | *(ident + "ID")
|
||||
}]
|
||||
|
||||
// All known RBAC operations for this resource
|
||||
rbac?: #rbacResource & {
|
||||
resourceExpIdent: expIdent
|
||||
}
|
||||
|
||||
locale?: #locale & {
|
||||
resourceExpIdent: expIdent
|
||||
resource: {
|
||||
// @todo can we merge this with RBAC type (FQRN?)
|
||||
type: component + ":" + handle
|
||||
}
|
||||
}
|
||||
|
||||
store?: {
|
||||
// how is this resource represented (prefixed/suffixed functions) in the store
|
||||
"ident": #ident | *ident
|
||||
"identPlural": #ident | *"\(store.ident)s"
|
||||
"expIdent": #expIdent | *strings.ToTitle(store.ident)
|
||||
"expIdentPlural": #expIdent | *"\(store.expIdent)s"
|
||||
|
||||
api?: {
|
||||
lookups: [...{
|
||||
_expFields: [ for f in fields {strings.ToTitle(model.attributes[f].expIdent)}]
|
||||
|
||||
"expIdent": "Lookup\(store.expIdent)By" + strings.Join(_expFields, "")
|
||||
description: string | *""
|
||||
|
||||
// fields used for the lookup (must exist in the struct)
|
||||
fields: [...string]
|
||||
|
||||
// Skip null constraints
|
||||
nullConstraint: [...string]
|
||||
constraintCheck: bool | *false
|
||||
}]
|
||||
|
||||
functions: [...{
|
||||
expIdent: string
|
||||
|
||||
description: string | *""
|
||||
|
||||
args: [...{ident: #ident, goType: string, spread: bool | *false}]
|
||||
return: [...string]
|
||||
}]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#storeFunction: {
|
||||
expIdent: #expIdent
|
||||
args: [...string]
|
||||
return: [...string]
|
||||
}
|
||||
|
||||
#PkgResource: #Resource & {
|
||||
package: {
|
||||
ident: #ident
|
||||
import: string
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
package schema
|
||||
|
||||
import (
|
||||
"strings"
|
||||
)
|
||||
|
||||
|
||||
// Identifier
|
||||
#ident: =~"^[a-z][a-zA-Z0-9_]*$"
|
||||
|
||||
// Exported identifier
|
||||
#expIdent: =~"^[A-Z][a-zA-Z0-9_]*$"
|
||||
|
||||
// More liberal then identifier, allows underscores and dots
|
||||
#handle: =~"^[A-Za-z][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9]+$"
|
||||
|
||||
// More liberal then identifier
|
||||
#baseHandle: =~"^[a-z][a-z0-9-]*[a-z0-9]+$"
|
||||
|
||||
#_base: {
|
||||
// lowercase dash-separated words
|
||||
// used to build ident and exported identifiers
|
||||
handle: #baseHandle
|
||||
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
|
||||
|
||||
// lowercase (unexported, golang) identifier
|
||||
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
|
||||
|
||||
// plural
|
||||
identPlural: #ident | *"\(ident)s"
|
||||
|
||||
// uppercase (exported, golang) identifier
|
||||
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
|
||||
|
||||
// plural exported
|
||||
expIdentPlural: #expIdent | *"\(expIdent)s"
|
||||
|
||||
...
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
|
||||
_dalModelFn: {
|
||||
res="res": schema.#Resource
|
||||
|
||||
"output": {
|
||||
"var": string | *"\(res.expIdent)"
|
||||
"resType": string | *"types.\(res.expIdent)ResourceType"
|
||||
|
||||
"ident": res.model.ident
|
||||
|
||||
"attributes": [
|
||||
for attr in res.model.attributes if attr.dal != _|_ {
|
||||
attr
|
||||
|
||||
"dal": {
|
||||
attr.dal
|
||||
|
||||
// Only these field types support "has-default" flag
|
||||
if (attr.dal.type & ( "ID" | "Ref" | "Number" | "Boolean" | "Enum" | "JSON" )) != _|_ {
|
||||
"hasDefault": attr.dal.default != _|_
|
||||
}
|
||||
|
||||
if attr.dal.default != _|_ {
|
||||
"quotedDefault": attr.dal.type == "String"
|
||||
}
|
||||
|
||||
if attr.dal.meta != _|_ {
|
||||
"meta": attr.dal.meta
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
"indexes": {[string]: close({
|
||||
"ident": string
|
||||
"type": string
|
||||
"unique"?: bool
|
||||
"predicate"?: string
|
||||
"fields": [{
|
||||
"attribute": string
|
||||
"modifers"?: [string, ...]
|
||||
"sort"?: string
|
||||
"nulls"?: string
|
||||
}, ...]
|
||||
})} | *null
|
||||
|
||||
if res.model.indexes != _|_ {
|
||||
"indexes": {
|
||||
for index in res.model.indexes {
|
||||
"\(index.ident)": {
|
||||
|
||||
if index.primary == true {
|
||||
"ident": "PRIMARY"
|
||||
}
|
||||
|
||||
if index.primary == false {
|
||||
"ident": index.ident
|
||||
"unique": index.unique
|
||||
}
|
||||
|
||||
"type": index.type
|
||||
|
||||
if index.predicate != _|_ {
|
||||
"predicate": index.predicate
|
||||
}
|
||||
|
||||
"fields": [
|
||||
for field in index.fields {
|
||||
"attribute": res.model.attributes[field.attribute].expIdent
|
||||
|
||||
if field.modifiers != _|_ {
|
||||
"modifiers": field.modifiers
|
||||
}
|
||||
if field.sort != _|_ {
|
||||
"sort": field.sort
|
||||
}
|
||||
if field.nulls != _|_ {
|
||||
"nulls": field.nulls
|
||||
}
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
{
|
||||
template: "gocode/dal/$component_model.go.tpl"
|
||||
output: "system/model/corteza.gen.go"
|
||||
payload: {
|
||||
package: "model"
|
||||
|
||||
imports: [
|
||||
for res in app.resources if (res.model.attributes != _|_) {
|
||||
"\(res.package.ident)type \"\(res.package.import)\"",
|
||||
}
|
||||
]
|
||||
|
||||
// Operation/resource validators, grouped by resource
|
||||
models: {
|
||||
for res in app.resources if (res.model.attributes != _|_) {
|
||||
"\(res.ident)": {
|
||||
// overriding resoure-type (import package alias) for app resources
|
||||
"resType": "\(res.package.ident)type.\(res.expIdent)ResourceType"
|
||||
|
||||
(_dalModelFn & { "res": res }).output
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/dal/$component_model.go.tpl"
|
||||
output: "\(cmp.ident)/model/models.gen.go"
|
||||
payload: {
|
||||
package: "model"
|
||||
|
||||
imports: [
|
||||
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
|
||||
]
|
||||
|
||||
// Operation/resource validators, grouped by resource
|
||||
models: {
|
||||
for res in cmp.resources if (res.model.attributes != _|_) {
|
||||
"\(res.ident)": {
|
||||
(_dalModelFn & { "res": res }).output
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/dal/$component_init.go.tpl"
|
||||
output: "\(cmp.ident)/model/init.gen.go"
|
||||
payload: {
|
||||
package: "model"
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,133 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
// "strings"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/envoy/rbac_references_$component.go.tpl"
|
||||
output: "pkg/envoy/resource/rbac_references_\(cmp.ident).gen.go"
|
||||
payload: {
|
||||
package: "resource"
|
||||
imports: [
|
||||
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
|
||||
]
|
||||
|
||||
resources: [
|
||||
for res in cmp.resources if res.rbac != _|_ {
|
||||
rbacRefFunc: "\(cmp.expIdent)\(res.expIdent)RbacReferences"
|
||||
references: [
|
||||
for p in res.parents {p},
|
||||
{param: res.ident, refField: "ID", expIdent: res.expIdent},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]+
|
||||
[
|
||||
{
|
||||
template: "gocode/envoy/rbac_rules_parse.go.tpl"
|
||||
output: "pkg/envoy/resource/rbac_rules_parse.gen.go"
|
||||
payload: {
|
||||
package: "resource"
|
||||
imports: [
|
||||
for cmp in app.corteza.components {
|
||||
"\(cmp.ident)Types \"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\""
|
||||
},
|
||||
]
|
||||
|
||||
resources: [
|
||||
for cmp in app.corteza.components for res in cmp.resources if res.rbac != _|_ {
|
||||
importAlias: "\(cmp.ident)Types"
|
||||
expIdent: res.expIdent
|
||||
|
||||
typeConst: "\(importAlias).\(expIdent)ResourceType"
|
||||
rbacRefFunc: "\(cmp.expIdent)\(res.expIdent)RbacReferences"
|
||||
references: [
|
||||
for p in res.parents {p},
|
||||
{param: res.ident, refField: "ID", expIdent: res.expIdent},
|
||||
]
|
||||
},
|
||||
|
||||
for cmp in app.corteza.components {
|
||||
importAlias: "\(cmp.ident)Types"
|
||||
expIdent: cmp.expIdent
|
||||
|
||||
typeConst: "\(importAlias).ComponentResourceType"
|
||||
references: []
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]+
|
||||
[
|
||||
{
|
||||
template: "gocode/envoy/resource_translation.go.tpl"
|
||||
output: "pkg/envoy/resource/resource_translation.gen.go"
|
||||
payload: {
|
||||
package: "resource"
|
||||
resources: [
|
||||
for cmp in app.corteza.components for res in cmp.resources if res.locale != _|_ {
|
||||
expIdent: "\(cmp.expIdent)\(res.expIdent)"
|
||||
extended: res.locale.extended
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]+
|
||||
[
|
||||
{
|
||||
template: "gocode/envoy/resource_translation_parse.go.tpl"
|
||||
output: "pkg/envoy/resource/resource_translation_parse.gen.go"
|
||||
payload: {
|
||||
package: "resource"
|
||||
imports: [
|
||||
for cmp in app.corteza.components for res in cmp.resources if res.locale != _|_ {
|
||||
"\(cmp.ident)Types \"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\""
|
||||
},
|
||||
]
|
||||
|
||||
resources: [
|
||||
for cmp in app.corteza.components for res in cmp.resources if res.locale != _|_ {
|
||||
importAlias: "\(cmp.ident)Types"
|
||||
typeConst: "\(importAlias).\(res.expIdent)ResourceTranslationType"
|
||||
resTrRefFunc: "\(cmp.expIdent)\(res.expIdent)ResourceTranslationReferences"
|
||||
references: [
|
||||
for p in res.parents {p},
|
||||
{param: res.ident, refField: "ID", expIdent: res.expIdent},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]+
|
||||
[
|
||||
// wrapped with additional for loop to trim out templates with empty types list
|
||||
for tpl in [
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/envoy/resource_translation_references_$component.go.tpl"
|
||||
output: "pkg/envoy/resource/resource_translation_references_\(cmp.ident).gen.go"
|
||||
payload: {
|
||||
package: "resource"
|
||||
imports: [
|
||||
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
|
||||
]
|
||||
|
||||
resources: [
|
||||
for res in cmp.resources if res.locale != _|_ {
|
||||
resTrRefFunc: "\(cmp.expIdent)\(res.expIdent)ResourceTranslationReferences"
|
||||
expIdent: res.expIdent
|
||||
references: [
|
||||
for p in res.parents {p},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
] if len(tpl.payload.resources) > 0 {tpl}
|
||||
]
|
||||
@@ -0,0 +1,43 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
// wrapped with additional for loop to trim out templates with empty types list
|
||||
for tpl in [
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/locale/$component_service.go.tpl"
|
||||
output: "\(cmp.ident)/service/locale.gen.go"
|
||||
payload: {
|
||||
package: "service"
|
||||
imports: [
|
||||
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
|
||||
]
|
||||
|
||||
resources: [
|
||||
for res in cmp.resources if (res.locale != _|_) if (!res.locale.skipSvc) {
|
||||
expIdent: res.expIdent
|
||||
ident: res.ident
|
||||
|
||||
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
|
||||
|
||||
extended: res.locale.extended
|
||||
|
||||
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
|
||||
struct: key.struct
|
||||
|
||||
"extended": extended
|
||||
customHandler: key.customHandler
|
||||
if key.serviceFunc != _|_ {serviceFunc: key.serviceFunc}
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
// skip empty type lists
|
||||
] if len(tpl.payload.resources) > 0 {tpl}
|
||||
]
|
||||
@@ -0,0 +1,55 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
"strings"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
// wrapped with additional for loop to trim out templates with empty types list
|
||||
for tpl in [
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/locale/$component_types.go.tpl"
|
||||
output: "\(cmp.ident)/types/locale.gen.go"
|
||||
payload: {
|
||||
package: "types"
|
||||
|
||||
resources: [
|
||||
for res in cmp.resources if res.locale != _|_ {
|
||||
expIdent: res.expIdent
|
||||
const: res.locale.resource.const
|
||||
type: res.locale.resource.type
|
||||
|
||||
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
|
||||
|
||||
extended: res.locale.extended
|
||||
|
||||
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
|
||||
struct: key.struct
|
||||
field: strings.ToTitle(key.name)
|
||||
|
||||
path: strings.Join([ for p in key.expandedPath {
|
||||
if p.var {"{{\(p.part)}}"}
|
||||
if !p.var {p.part}
|
||||
}], ".")
|
||||
|
||||
if !key.customHandler {
|
||||
fieldPath: strings.Join([ for p in key.expandedPath {
|
||||
strings.ToTitle(p.part)
|
||||
}], ".")
|
||||
}
|
||||
|
||||
"extended": extended
|
||||
if key.decodeFunc != _|_ {decodeFunc: key.decodeFunc}
|
||||
if key.encodeFunc != _|_ {encodeFunc: key.encodeFunc}
|
||||
|
||||
}]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
// skip empty type lists
|
||||
] if len(tpl.payload.resources) > 0 {tpl}
|
||||
]
|
||||
@@ -0,0 +1,82 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
{
|
||||
template: "gocode/options/options.go.tpl"
|
||||
output: "pkg/options/options.gen.go"
|
||||
payload: {
|
||||
package: "options"
|
||||
|
||||
// make unique list of packages we'll import
|
||||
imports: [ for i in {for g in app.corteza.options for i in g.imports {"\(i)": i}} {i}]
|
||||
|
||||
groups: [
|
||||
for g in app.corteza.options {
|
||||
func: g.expIdent
|
||||
struct: g.expIdent + "Opt"
|
||||
options: [
|
||||
for o in g.options {
|
||||
o
|
||||
|
||||
default?: string
|
||||
if (o.defaultGoExpr != _|_) {
|
||||
default: o.defaultGoExpr
|
||||
}
|
||||
|
||||
if (o.defaultGoExpr == _|_ && o.defaultValue != _|_) {
|
||||
default: "\"" + o.defaultValue + "\""
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]+
|
||||
[
|
||||
{
|
||||
template: "docs/.env.example.tpl"
|
||||
output: ".env.example"
|
||||
syntax: ".env"
|
||||
payload: {
|
||||
groups: [
|
||||
for g in app.corteza.options {
|
||||
title: "# " + strings.Join(strings.Split(g.title, "\n"), "\n# ")
|
||||
|
||||
if (g.intro != _|_) {
|
||||
intro: "# " + strings.Join(strings.Split(g.intro, "\n"), "\n# ")
|
||||
}
|
||||
|
||||
options: [
|
||||
for o in g.options {
|
||||
handle: o.handle
|
||||
env: o.env
|
||||
type: o.type
|
||||
|
||||
defaultGoExpr?: string
|
||||
if (o.defaultGoExpr != _|_) {
|
||||
defaultGoExpr: o.defaultGoExpr
|
||||
}
|
||||
|
||||
defaultValue?: string
|
||||
if (o.defaultValue != _|_) {
|
||||
defaultValue: o.defaultValue
|
||||
}
|
||||
|
||||
if (o.description != _|_) {
|
||||
description: "# " + strings.Join(strings.Split(o.description, "\n"), "\n# ")
|
||||
}
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,83 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/rbac/$component_access_control.go.tpl"
|
||||
output: "\(cmp.ident)/service/access_control.gen.go"
|
||||
payload: {
|
||||
package: "service"
|
||||
imports: [
|
||||
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
|
||||
]
|
||||
|
||||
// All known RBAC resources
|
||||
resources: [
|
||||
for res in cmp.resources if res.rbac != _|_ {
|
||||
resFunc: "types.\(res.expIdent)RbacResource"
|
||||
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
|
||||
},
|
||||
{
|
||||
resFunc: "types.ComponentRbacResource"
|
||||
component: true
|
||||
},
|
||||
]
|
||||
|
||||
// All possible RBAC operations on component and resources
|
||||
// flattened
|
||||
operations: [
|
||||
for res in cmp.resources if res.rbac != _|_ for op in res.rbac.operations {
|
||||
"op": op.handle
|
||||
const: "types.\(res.expIdent)ResourceType"
|
||||
resFunc: "types.\(res.expIdent)RbacResource"
|
||||
goType: "types.\(res.expIdent)"
|
||||
description: op.description
|
||||
checkFuncName: op.checkFuncName
|
||||
|
||||
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
|
||||
},
|
||||
for op in cmp.rbac.operations {
|
||||
"op": op.handle
|
||||
const: "types.ComponentResourceType"
|
||||
resFunc: "types.ComponentRbacResource"
|
||||
goType: "types.Component"
|
||||
description: op.description
|
||||
checkFuncName: op.checkFuncName
|
||||
component: true
|
||||
},
|
||||
]
|
||||
|
||||
// Operation/resource validators, grouped by resource
|
||||
loaders: [
|
||||
for res in cmp.resources if res.rbac != _|_ {
|
||||
const: "types.\(res.expIdent)ResourceType"
|
||||
resFunc: "types.\(res.expIdent)RbacResource"
|
||||
funcName: "load\(res.expIdent)"
|
||||
refIndex: [ { 0 }, for i, p in res.parents {i + 1} ]
|
||||
},
|
||||
]
|
||||
|
||||
// Operation/resource validators, grouped by resource
|
||||
validation: [
|
||||
for res in cmp.resources if res.rbac != _|_ {
|
||||
label: res.ident
|
||||
const: "types.\(res.expIdent)ResourceType"
|
||||
funcName: "rbac\(res.expIdent)ResourceValidator"
|
||||
references: [ for p in res.parents {p.refField}, "ID"]
|
||||
operations: [ for op in res.rbac.operations {op.handle}]
|
||||
},
|
||||
{
|
||||
label: "\(cmp.ident) component"
|
||||
const: "types.ComponentResourceType"
|
||||
funcName: "rbacComponentResourceValidator"
|
||||
operations: [ for op in cmp.rbac.operations {op.handle}]
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,41 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/rbac/$component_types_rbac.go.tpl"
|
||||
output: "\(cmp.ident)/types/rbac.gen.go"
|
||||
payload: {
|
||||
package: "types"
|
||||
|
||||
cmpIdent: cmp.ident
|
||||
// Operation/resource validators, grouped by resource
|
||||
types: [
|
||||
for res in cmp.resources if res.rbac != _|_ {
|
||||
const: "\(res.expIdent)ResourceType"
|
||||
type: res.fqrt
|
||||
resFunc: "\(res.expIdent)RbacResource"
|
||||
tplFunc: "\(res.expIdent)RbacResourceTpl"
|
||||
attFunc: "\(res.expIdent)RbacAttributes"
|
||||
goType: res.expIdent
|
||||
|
||||
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
|
||||
},
|
||||
{
|
||||
const: "ComponentResourceType"
|
||||
type: cmp.fqrt
|
||||
resFunc: "ComponentRbacResource"
|
||||
tplFunc: "ComponentRbacResourceTpl"
|
||||
attFunc: "ComponentRbacAttributes"
|
||||
goType: "Component"
|
||||
component: true
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,251 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"list"
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
|
||||
_StoreResource: {
|
||||
res = "res": schema.#Resource
|
||||
typePkg = "typePkg": string
|
||||
|
||||
|
||||
pkAttrNames: [string, ...]
|
||||
hasPrimaryKey: res.model.indexes.primary != _|_
|
||||
|
||||
if hasPrimaryKey {
|
||||
// primary key flag is no longer explicitly set on
|
||||
// the attribute but via model.indexes.primary
|
||||
pkAttrNames: [
|
||||
for f in res.model.indexes.primary.fields { f.attribute }
|
||||
]
|
||||
}
|
||||
|
||||
result: {
|
||||
ident: res.store.ident
|
||||
identPlural: res.store.identPlural
|
||||
expIdent: res.store.expIdent
|
||||
expIdentPlural: res.store.expIdentPlural
|
||||
modelIdent: res.model.ident
|
||||
goType: "\(typePkg).\(res.expIdent)"
|
||||
goSetType: "\(typePkg).\(res.expIdent)Set"
|
||||
goFilterType: "\(typePkg).\(res.filter.expIdent)"
|
||||
|
||||
|
||||
struct: [ for attr in res.model.attributes if attr.store {
|
||||
"ident": attr.ident
|
||||
"expIdent": attr.expIdent
|
||||
"storeIdent": attr.storeIdent
|
||||
"name": attr.name
|
||||
"primaryKey": list.Contains(pkAttrNames, attr.name)
|
||||
"ignoreCase": attr.ignoreCase
|
||||
"goType": strings.Replace(attr.goType, "types.", "\(typePkg).", 1)
|
||||
}]
|
||||
|
||||
filter: {
|
||||
// query fields as defined in struct
|
||||
"query": [ for name in res.filter.query {res.model.attributes[name]}],
|
||||
|
||||
// filter by nil state as defined in filter
|
||||
"byNilState": [ for name in res.filter.byNilState {res.filter.struct[name]}]
|
||||
|
||||
// filter by false as defined in filter
|
||||
"byFalseState": [ for name in res.filter.byFalseState {res.filter.struct[name]}]
|
||||
|
||||
// filter by value as defined in filter
|
||||
// @todo this should be pulled from the struct
|
||||
"byValue": [ for name in res.filter.byValue {res.filter.struct[name]}]
|
||||
"byLabel": res.features.labels
|
||||
"byFlag": res.features.flags
|
||||
}
|
||||
|
||||
auxIdent: "aux\(expIdent)"
|
||||
auxStruct: struct
|
||||
|
||||
features: {
|
||||
paging: res.features.paging
|
||||
sorting: res.features.sorting
|
||||
checkFn: res.features.checkFn
|
||||
}
|
||||
|
||||
api: {
|
||||
if res.store.api != _|_ {
|
||||
_base: {
|
||||
"ident": res.store.ident
|
||||
"expStoreIdent": res.store.expIdentPlural
|
||||
"goType": goType
|
||||
"goFilterType": goFilterType
|
||||
"auxIdent": auxIdent
|
||||
}
|
||||
|
||||
if hasPrimaryKey {
|
||||
deleteByPK: {
|
||||
"pkAttrNames": pkAttrNames,
|
||||
attributes: [ for attr in pkAttrNames { res.model.attributes[attr] } ]
|
||||
_expIdents: strings.Join([ for attr in pkAttrNames { res.model.attributes[attr].expIdent } ], "")
|
||||
"expFnIdent": "Delete\(res.store.expIdent)By\(_expIdents)"
|
||||
}
|
||||
}
|
||||
|
||||
lookups: [
|
||||
for l in res.store.api.lookups {
|
||||
_base
|
||||
|
||||
"expFnIdent": l.expIdent
|
||||
|
||||
if (l.description != _|_) {
|
||||
description: "// \(l.expIdent) " + strings.Join(strings.Split(l.description, "\n"), "\n// ")
|
||||
}
|
||||
|
||||
// Copy all relevant fields from the struct
|
||||
"args": [
|
||||
for name in l.fields {
|
||||
let attr = res.model.attributes[name]
|
||||
|
||||
"ident": attr.ident
|
||||
"storeIdent": attr.storeIdent
|
||||
"goType": attr.goType
|
||||
"ignoreCase": attr.ignoreCase
|
||||
},
|
||||
]
|
||||
|
||||
"nullConstraint": l.nullConstraint
|
||||
"returnType": "\(goType)"
|
||||
"collectionFnIdent": "\(res.store.ident)Collection"
|
||||
},
|
||||
]
|
||||
|
||||
// all additional store functions we need for this resource
|
||||
functions: [
|
||||
for f in res.store.api.functions {
|
||||
_base
|
||||
|
||||
"expFnIdent": f.expIdent
|
||||
|
||||
if (f.description != _|_) {
|
||||
description: "// \(f.expIdent) " + strings.Join(strings.Split(f.description, "\n"), "\n// ")
|
||||
}
|
||||
|
||||
"args": [ for a in f.args {
|
||||
"ident": a.ident
|
||||
"goType": strings.Replace(a.goType, "types.", "\(typePkg).", 1)
|
||||
"spread": a.spread
|
||||
}]
|
||||
"return": [ for r in f.return {strings.Replace(r, "types.", "\(typePkg).", 1)}]
|
||||
},
|
||||
]
|
||||
|
||||
sortableFields: {
|
||||
_base
|
||||
|
||||
"fnIdent": "sortable\(expIdent)Fields"
|
||||
|
||||
fields: {
|
||||
for attr in res.model.attributes if attr.sortable || attr.unique || list.Contains(pkAttrNames, attr.name) {
|
||||
{
|
||||
"\(strings.ToLower(attr.name))": attr.name
|
||||
"\(strings.ToLower(attr.ident))": attr.name
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
collectCursorValues: {
|
||||
_base
|
||||
|
||||
"fnIdent": "collect\(expIdent)CursorValues"
|
||||
|
||||
fields: [ for attr in res.model.attributes if attr.sortable || attr.unique || list.Contains(pkAttrNames, attr.name) {
|
||||
attr
|
||||
"primaryKey": list.Contains(pkAttrNames, attr.name)
|
||||
} ]
|
||||
|
||||
primaryKeys: [ for attr in res.model.attributes if list.Contains(pkAttrNames, attr.name) {attr} ]
|
||||
}
|
||||
|
||||
checkConstraints: {
|
||||
_base
|
||||
|
||||
"fnIdent": "check\(expIdent)Constraints"
|
||||
|
||||
checks: [
|
||||
for lookup in res.store.api.lookups if lookup.constraintCheck {
|
||||
lookupFnIdent: lookup.expIdent
|
||||
fields: [ for name in lookup.fields {res.model.attributes[name]}]
|
||||
nullConstraint: [
|
||||
for f in res.model.attributes if list.Contains(lookup.nullConstraint, f.name) {
|
||||
"expIdent": f.expIdent
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Codegen template payload, reused for multiple outputs
|
||||
_payload: {
|
||||
package: string | *"store"
|
||||
|
||||
imports: {
|
||||
// per-component type imports
|
||||
for cmp in app.corteza.components for res in cmp.resources if res.store != _|_ {
|
||||
"github.com/cortezaproject/corteza-server/\(cmp.ident)/types": "\(cmp.ident)Type"
|
||||
}
|
||||
|
||||
for res in app.corteza.resources if res.store != _|_ {
|
||||
"\(res.package.import)": "\(res.package.ident)Type"
|
||||
}
|
||||
|
||||
for cmp in app.corteza.components for res in cmp.resources for i in res.imports {
|
||||
"\(i.import)": ""
|
||||
}
|
||||
}
|
||||
|
||||
types: {
|
||||
// for each resource in every store with store and actions defined
|
||||
for cmp in app.corteza.components for res in cmp.resources if res.store != _|_ {
|
||||
// use _Store resource as a function (https://cuetorials.com/patterns/functions/)
|
||||
// and pass res(ource) and type-package string in as "arguments"
|
||||
"\(res.store.ident)": { _StoreResource & { "res": res, "typePkg": "\(cmp.ident)Type" } }.result
|
||||
},
|
||||
|
||||
for res in app.corteza.resources if res.store != _|_ {
|
||||
"\(res.store.ident)": { _StoreResource & { "res": res, "typePkg": "\(res.package.ident)Type" } }.result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
{
|
||||
"bulk": [
|
||||
{
|
||||
"template": "gocode/store/interfaces.go.tpl"
|
||||
"output": "store/interfaces.gen.go"
|
||||
}, {
|
||||
"template": "gocode/store/rdbms/rdbms.go.tpl"
|
||||
"output": "store/adapters/rdbms/rdbms.gen.go"
|
||||
}, {
|
||||
"template": "gocode/store/rdbms/aux_types.go.tpl"
|
||||
"output": "store/adapters/rdbms/aux_types.gen.go"
|
||||
}, {
|
||||
"template": "gocode/store/rdbms/queries.go.tpl"
|
||||
"output": "store/adapters/rdbms/queries.gen.go"
|
||||
}, {
|
||||
"template": "gocode/store/rdbms/filters.go.tpl"
|
||||
"output": "store/adapters/rdbms/filters.gen.go"
|
||||
}, {
|
||||
"template": "gocode/store/tests/all.go.tpl"
|
||||
"output": "store/tests/all_test.go"
|
||||
},
|
||||
]
|
||||
|
||||
"payload": { _payload }
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,30 @@
|
||||
package codegen
|
||||
|
||||
import (
|
||||
"github.com/cortezaproject/corteza-server/app"
|
||||
"github.com/cortezaproject/corteza-server/codegen/schema"
|
||||
)
|
||||
|
||||
[...schema.#codegen] &
|
||||
[
|
||||
for cmp in app.corteza.components {
|
||||
template: "gocode/types/$component_resources.go.tpl"
|
||||
output: "\(cmp.ident)/types/resources.gen.go"
|
||||
payload: {
|
||||
package: "types"
|
||||
|
||||
cmpIdent: cmp.ident
|
||||
// Operation/resource validators, grouped by resource
|
||||
types: [
|
||||
for res in cmp.resources {
|
||||
const: "\(res.expIdent)ResourceType"
|
||||
type: res.fqrt
|
||||
},
|
||||
{
|
||||
const: "ComponentResourceType"
|
||||
type: cmp.fqrt
|
||||
},
|
||||
]
|
||||
}
|
||||
},
|
||||
]
|
||||
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"os"
|
||||
"path"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"github.com/cortezaproject/corteza-server/pkg/cli"
|
||||
)
|
||||
|
||||
type (
|
||||
inOut struct {
|
||||
Template string `json:"template"`
|
||||
Output string `json:"output"`
|
||||
Syntax string `json:"syntax"`
|
||||
}
|
||||
|
||||
task struct {
|
||||
inOut
|
||||
|
||||
Bulk []inOut `json:"bulk"`
|
||||
|
||||
Payload interface{} `json:"payload"`
|
||||
}
|
||||
)
|
||||
|
||||
var (
|
||||
verbose bool
|
||||
showHelp bool
|
||||
tplRootPath string
|
||||
outputBase string
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.BoolVar(&showHelp, "h", false, "show help")
|
||||
flag.BoolVar(&verbose, "v", false, "be verbose")
|
||||
flag.StringVar(&tplRootPath, "p", "codegen/assets/templates", "location of the template files")
|
||||
flag.StringVar(&outputBase, "b", ".", "base dir for output")
|
||||
flag.Parse()
|
||||
}
|
||||
|
||||
// Takes JSON input with codegen tasks and definitions and generates files
|
||||
func main() {
|
||||
if showHelp {
|
||||
flag.PrintDefaults()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
var (
|
||||
input = json.NewDecoder(os.Stdin)
|
||||
tasks = make([]*task, 0)
|
||||
tpl *template.Template
|
||||
err error
|
||||
)
|
||||
|
||||
started := time.Now()
|
||||
print("Waiting for stdin ...")
|
||||
if err = input.Decode(&tasks); err != nil {
|
||||
cli.HandleError(fmt.Errorf("failed to decode input from standard input: %v", err))
|
||||
}
|
||||
|
||||
println(time.Now().Sub(started).Round(time.Second)/time.Second, "sec")
|
||||
|
||||
if tpl, err = loadTemplates(baseTemplate(), tplRootPath); err != nil {
|
||||
cli.HandleError(fmt.Errorf("failed to load templates: %v", err))
|
||||
}
|
||||
|
||||
for _, j := range tasks {
|
||||
if len(j.Bulk) == 0 {
|
||||
j.Bulk = append(j.Bulk, j.inOut)
|
||||
}
|
||||
|
||||
for _, o := range j.Bulk {
|
||||
output := path.Join(outputBase, o.Output)
|
||||
print(fmt.Sprintf("generating %s (from %s) ...", output, o.Template))
|
||||
|
||||
switch o.Syntax {
|
||||
case "go":
|
||||
err = writeFormattedGo(output, tpl.Lookup(o.Template), j.Payload)
|
||||
default:
|
||||
err = write(output, tpl.Lookup(o.Template), j.Payload)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
cli.HandleError(fmt.Errorf("failed to write template: %v", err))
|
||||
} else {
|
||||
print("done\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func print(msg string) {
|
||||
if verbose {
|
||||
_, _ = fmt.Fprint(os.Stderr, msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"go/format"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"text/template"
|
||||
|
||||
"github.com/Masterminds/sprig"
|
||||
)
|
||||
|
||||
func baseTemplate() *template.Template {
|
||||
return template.New("").
|
||||
Funcs(sprig.TxtFuncMap())
|
||||
}
|
||||
|
||||
func loadTemplates(rTpl *template.Template, rootDir string) (*template.Template, error) {
|
||||
cleanRoot := filepath.Clean(rootDir)
|
||||
pfx := len(cleanRoot) + 1
|
||||
|
||||
return rTpl, filepath.Walk(cleanRoot, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if info.IsDir() || !strings.HasSuffix(path, ".tpl") || err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
name := path[pfx:]
|
||||
rTpl, err = rTpl.New(name).Parse(string(b))
|
||||
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func writeFormattedGo(dst string, tpl *template.Template, payload interface{}) error {
|
||||
return write(dst, tpl, payload, func(in io.ReadWriter) (out io.ReadWriter, err error) {
|
||||
var (
|
||||
org, bb []byte
|
||||
)
|
||||
|
||||
if org, err = ioutil.ReadAll(in); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
cp := bytes.NewBuffer(org)
|
||||
|
||||
if bb, err = format.Source(cp.Bytes()); err != nil {
|
||||
// output error and return un-formatted source
|
||||
_, _ = fmt.Fprintf(os.Stderr, "%s fmt warn: %v\n", dst, err)
|
||||
return cp, nil
|
||||
}
|
||||
|
||||
return bytes.NewBuffer(bb), nil
|
||||
})
|
||||
}
|
||||
|
||||
func write(dst string, tpl *template.Template, payload interface{}, pp ...func(io.ReadWriter) (io.ReadWriter, error)) (err error) {
|
||||
var (
|
||||
output io.WriteCloser
|
||||
buf io.ReadWriter
|
||||
)
|
||||
|
||||
if tpl == nil {
|
||||
return fmt.Errorf("could not find template for %s", dst)
|
||||
}
|
||||
|
||||
buf = &bytes.Buffer{}
|
||||
if err := tpl.Execute(buf, payload); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, proc := range pp {
|
||||
if buf, err = proc(buf); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if dst == "" || dst == "-" {
|
||||
output = os.Stdout
|
||||
} else {
|
||||
if output, err = os.Create(dst); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
defer output.Close()
|
||||
}
|
||||
|
||||
if _, err = io.Copy(output, buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user