3
0

Moving server files to ./server

This commit is contained in:
Corteza Monorepo Migrator
2022-11-14 09:26:39 +01:00
parent 204738f7c9
commit 683c7c56e2
5750 changed files with 0 additions and 26430 deletions
+2
View File
@@ -0,0 +1,2 @@
/build
tmp*.cue
+26
View File
@@ -0,0 +1,26 @@
.PHONY: *.cue
DEF_ALL = *.cue
DEF_SERVER = server.*.cue
DEF_DOCS = docs.*.cue
server: $(DEF_SERVER)
docs: $(DEF_DOCS)
all: $(DEF_SERVER) $(DEF_DOCS)
include ../Makefile.inc
$(DEF_SERVER): $(CUE) $(JSONTPLEXEC)
@ echo "$(COLOUR_GREEN)Generating server files from $@$(COLOUR_END)"
@ $(CUE) eval $@ --out json | $(JSONTPLEXEC) -v -p $(ROOT_DIR)/codegen/assets/templates -b $(ROOT_DIR)
ifeq ("${DOCS_DIR}", "")
$(DEF_DOCS):
@ echo "$(COLOUR_YELLOW)Skipping docs generation: DOCS_DIR is not set.$(COLOUR_END)"
@ echo "$Run task like this: make "$@" DOCS_DIR=/path/to/corteza-docs"
@ echo ""
else
$(DEF_DOCS): $(CUE) $(JSONTPLEXEC)
@ echo "$(COLOUR_GREEN)Generating doc files from $@$(COLOUR_END) $(COLOUR_BLUE)(dst: $(DOCS_DIR))$(COLOUR_END)"
@ $(CUE) eval $@ --out json | $(JSONTPLEXEC) -v -p $(ROOT_DIR)/codegen/assets/templates -b $(DOCS_DIR)
endif
@@ -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
}
@@ -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 }}
)
+24
View File
@@ -0,0 +1,24 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
[...schema.#codegen] &
[
{
template: "docs/options.adoc.tpl"
output: "src/modules/generated/partials/env-options.gen.adoc"
payload: {
groups: [
for g in app.corteza.options {
title: g.title
intro?: g.intro
options: g.options
},
]
}
},
]
+67
View File
@@ -0,0 +1,67 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/codegen/schema"
"github.com/cortezaproject/corteza-server/app"
)
#_indexPayload: {
label: string
resources: [...string]
}
#_operationsPayload: {
label: string
operations: [...{
slug: string
label: string
description: string
}]
}
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "docs/rbac.index.adoc.tpl"
output: "src/modules/generated/partials/access-control/\(cmp.handle)/index.gen.adoc"
payload: #_indexPayload & {
label: cmp.label
resources: [ for res in cmp.resources { res.handle } ]
}
}
] +
[
for cmp in app.corteza.components {
template: "docs/rbac.$component.adoc.tpl"
output: "src/modules/generated/partials/access-control/\(cmp.handle)/component.gen.adoc"
payload: #_operationsPayload & {
label: cmp.label
operations: [
for op in cmp.rbac.operations {
slug: "rbac-\(cmp.handle)-\(op.handle)"
label: op.handle
description: op.description
}
]
}
}
] +
[
for cmp in app.corteza.components for res in cmp.resources {
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
}
]
}
}
]
+19
View File
@@ -0,0 +1,19 @@
package schema
#_ioSpec: {
template: string
output: string
syntax: string | *"go"
if output =~ "\\.adoc$" {
syntax: "adoc"
}
}
#codegen: {
#_ioSpec
payload: _
} | {
bulk?: [...#_ioSpec]
payload: _
}
+28
View File
@@ -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"
}
}
}
}
+54
View File
@@ -0,0 +1,54 @@
package schema
import (
"strings"
"list"
)
#locale: {
resourceExpIdent: #expIdent
// @todo we need a better name here!
skipSvc: bool | *false
extended: bool | *false
resource: {
type: string
const: string | *("\(resourceExpIdent)ResourceTranslationType")
}
keys: {
[key=_]: #localeKey & {
name: key
_resourceExpIdent: resourceExpIdent
}
}
}
#localeKey: {
name: #handle
_resourceExpIdent: #expIdent
path: [...(#ident | {part: #ident, var: bool | *false})] | *([name])
expandedPath: [ for p in path {
if (p & {"p": #ident}) != _|_ {p, var: p.var}
if (p & string) != _|_ {"part": p, var: false}
}]
_suffix: strings.Join([ for p in expandedPath {strings.ToTitle(p.part)}], "")
struct: string | *("LocaleKey" + _resourceExpIdent + _suffix)
// As soon as we use vars in the path,
// custom handler must be present
_hasVars: list.Contains([ for p in path {p.var | false}], true)
customHandler: bool | *_hasVars
if customHandler {
decodeFunc: string | *("decodeTranslations" + _suffix)
encodeFunc: string | *("encodeTranslations" + _suffix)
serviceFunc: string | *("handle" + _resourceExpIdent + _suffix)
}
}
+265
View File
@@ -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"
})
+49
View File
@@ -0,0 +1,49 @@
package schema
import (
"strings"
)
#_ENV: =~"^[A-Z][A-Z0-9_]*[A-Z0-9]?$"
//#_optName: =~ "^[a-zA-Z][a-zA-Z0-9\\s]*[a-zA-Z0-9]+$"
#optionsGroup: #_base & {
imports: [...string] | *([])
handle: #handle
title: string | *handle
description?: string
env: #_ENV | *(strings.ToUpper(strings.Replace(handle, "-", "_", -1)))
_envPrefix: env
options: {
[_opt=_]: #option & {
handle: _opt
env: #_ENV | *(_envPrefix + "_" + strings.ToUpper(strings.Replace(handle, " ", "_", -1)))
}
}
}
#option: {
handle: #handle
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
// lowercased (unexported, golang) identifier
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// upercased (exported, golang) identifier
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
type: string | *"string"
description?: string
// Default expression to be used
defaultGoExpr?: string
env?: #_ENV
// Plain default value to use when generating .env.example
defaultValue?: string
}
+21
View File
@@ -0,0 +1,21 @@
package schema
#platform: {
ident: #baseHandle | *"corteza"
options: [...#optionsGroup]
components: [...{platform: ident} & #component]
resources: {
[key=#handle]: #Resource & {
"handle": key,
"platform": ident
}
}
// automation: {
// types: ....
// function ....
// }
}
+61
View File
@@ -0,0 +1,61 @@
package schema
import (
"strings"
)
#rbacComponent: {
resource: {
type: string
}
operations: {
[key=_]: #rbacOperation & {
handle: key
}
}
}
#rbacResource: {
resourceExpIdent: #expIdent
operations: {
[key=_]: #rbacOperation & {
handle: key
_resourceExpIdent: resourceExpIdent
}
}
}
#rbacOperation: {
handle: #handle
description: string | *handle
_resourceExpIdent?: string
// Some string manipulation that will result in
// more pronouncable access-control check function name
// When check function name is not explicitly defined we try
// to use resource and operation name and generate easy-to-read name
//
// <res> + <op> => Can<Op><Res>
// <res> + <op:foo.bar.verb> => Can<Verb><Foo><Bar>On<Res>
_operation: strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1)
_opSplit: strings.Split(_operation, ".")
_opFlip: [_opSplit[len(_opSplit)-1]] + _opSplit[0:len(_opSplit)-1]
_opFinal: strings.Replace(strings.ToTitle(strings.Join(_opFlip, " ")), " ", "", -1)
if _resourceExpIdent == _|_ {
checkFuncName: #expIdent | *"Can\(_opFinal)"
}
if len(_opSplit) > 1 && _resourceExpIdent != _|_ {
checkFuncName: #expIdent | *"Can\(_opFinal)On\(_resourceExpIdent)"
}
if len(_opSplit) <= 1 && _resourceExpIdent != _|_ {
checkFuncName: #expIdent | *"Can\(_opFinal)\(_resourceExpIdent)"
}
}
+137
View File
@@ -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
}
}
+39
View File
@@ -0,0 +1,39 @@
package schema
import (
"strings"
)
// Identifier
#ident: =~"^[a-z][a-zA-Z0-9_]*$"
// Exported identifier
#expIdent: =~"^[A-Z][a-zA-Z0-9_]*$"
// More liberal then identifier, allows underscores and dots
#handle: =~"^[A-Za-z][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9]+$"
// More liberal then identifier
#baseHandle: =~"^[a-z][a-z0-9-]*[a-z0-9]+$"
#_base: {
// lowercase dash-separated words
// used to build ident and exported identifiers
handle: #baseHandle
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
// lowercase (unexported, golang) identifier
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// plural
identPlural: #ident | *"\(ident)s"
// uppercase (exported, golang) identifier
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
// plural exported
expIdentPlural: #expIdent | *"\(expIdent)s"
...
}
+151
View File
@@ -0,0 +1,151 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
_dalModelFn: {
res="res": schema.#Resource
"output": {
"var": string | *"\(res.expIdent)"
"resType": string | *"types.\(res.expIdent)ResourceType"
"ident": res.model.ident
"attributes": [
for attr in res.model.attributes if attr.dal != _|_ {
attr
"dal": {
attr.dal
// Only these field types support "has-default" flag
if (attr.dal.type & ( "ID" | "Ref" | "Number" | "Boolean" | "Enum" | "JSON" )) != _|_ {
"hasDefault": attr.dal.default != _|_
}
if attr.dal.default != _|_ {
"quotedDefault": attr.dal.type == "String"
}
if attr.dal.meta != _|_ {
"meta": attr.dal.meta
}
}
}
]
"indexes": {[string]: close({
"ident": string
"type": string
"unique"?: bool
"predicate"?: string
"fields": [{
"attribute": string
"modifers"?: [string, ...]
"sort"?: string
"nulls"?: string
}, ...]
})} | *null
if res.model.indexes != _|_ {
"indexes": {
for index in res.model.indexes {
"\(index.ident)": {
if index.primary == true {
"ident": "PRIMARY"
}
if index.primary == false {
"ident": index.ident
"unique": index.unique
}
"type": index.type
if index.predicate != _|_ {
"predicate": index.predicate
}
"fields": [
for field in index.fields {
"attribute": res.model.attributes[field.attribute].expIdent
if field.modifiers != _|_ {
"modifiers": field.modifiers
}
if field.sort != _|_ {
"sort": field.sort
}
if field.nulls != _|_ {
"nulls": field.nulls
}
},
]
}
}
}
}
}
},
[...schema.#codegen] &
[
{
template: "gocode/dal/$component_model.go.tpl"
output: "system/model/corteza.gen.go"
payload: {
package: "model"
imports: [
for res in app.resources if (res.model.attributes != _|_) {
"\(res.package.ident)type \"\(res.package.import)\"",
}
]
// Operation/resource validators, grouped by resource
models: {
for res in app.resources if (res.model.attributes != _|_) {
"\(res.ident)": {
// overriding resoure-type (import package alias) for app resources
"resType": "\(res.package.ident)type.\(res.expIdent)ResourceType"
(_dalModelFn & { "res": res }).output
},
},
}
}
},
for cmp in app.corteza.components {
template: "gocode/dal/$component_model.go.tpl"
output: "\(cmp.ident)/model/models.gen.go"
payload: {
package: "model"
imports: [
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
]
// Operation/resource validators, grouped by resource
models: {
for res in cmp.resources if (res.model.attributes != _|_) {
"\(res.ident)": {
(_dalModelFn & { "res": res }).output
}
},
}
}
},
for cmp in app.corteza.components {
template: "gocode/dal/$component_init.go.tpl"
output: "\(cmp.ident)/model/init.gen.go"
payload: {
package: "model"
}
},
]
+133
View File
@@ -0,0 +1,133 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
// "strings"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "gocode/envoy/rbac_references_$component.go.tpl"
output: "pkg/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}
]
+43
View File
@@ -0,0 +1,43 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
[...schema.#codegen] &
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/locale/$component_service.go.tpl"
output: "\(cmp.ident)/service/locale.gen.go"
payload: {
package: "service"
imports: [
"\"github.com/cortezaproject/corteza-server/\(cmp.ident)/types\"",
]
resources: [
for res in cmp.resources if (res.locale != _|_) if (!res.locale.skipSvc) {
expIdent: res.expIdent
ident: res.ident
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
extended: res.locale.extended
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
struct: key.struct
"extended": extended
customHandler: key.customHandler
if key.serviceFunc != _|_ {serviceFunc: key.serviceFunc}
}]
},
]
}
},
// skip empty type lists
] if len(tpl.payload.resources) > 0 {tpl}
]
+55
View File
@@ -0,0 +1,55 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
"strings"
)
[...schema.#codegen] &
[
// wrapped with additional for loop to trim out templates with empty types list
for tpl in [
for cmp in app.corteza.components {
template: "gocode/locale/$component_types.go.tpl"
output: "\(cmp.ident)/types/locale.gen.go"
payload: {
package: "types"
resources: [
for res in cmp.resources if res.locale != _|_ {
expIdent: res.expIdent
const: res.locale.resource.const
type: res.locale.resource.type
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
extended: res.locale.extended
keys: [ for key in res.locale.keys if key.handlerFunc == _|_ {
struct: key.struct
field: strings.ToTitle(key.name)
path: strings.Join([ for p in key.expandedPath {
if p.var {"{{\(p.part)}}"}
if !p.var {p.part}
}], ".")
if !key.customHandler {
fieldPath: strings.Join([ for p in key.expandedPath {
strings.ToTitle(p.part)
}], ".")
}
"extended": extended
if key.decodeFunc != _|_ {decodeFunc: key.decodeFunc}
if key.encodeFunc != _|_ {encodeFunc: key.encodeFunc}
}]
},
]
}
},
// skip empty type lists
] if len(tpl.payload.resources) > 0 {tpl}
]
+82
View File
@@ -0,0 +1,82 @@
package codegen
import (
"strings"
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
[...schema.#codegen] &
[
{
template: "gocode/options/options.go.tpl"
output: "pkg/options/options.gen.go"
payload: {
package: "options"
// make unique list of packages we'll import
imports: [ for i in {for g in app.corteza.options for i in g.imports {"\(i)": i}} {i}]
groups: [
for g in app.corteza.options {
func: g.expIdent
struct: g.expIdent + "Opt"
options: [
for o in g.options {
o
default?: string
if (o.defaultGoExpr != _|_) {
default: o.defaultGoExpr
}
if (o.defaultGoExpr == _|_ && o.defaultValue != _|_) {
default: "\"" + o.defaultValue + "\""
}
},
]
},
]
}
},
]+
[
{
template: "docs/.env.example.tpl"
output: ".env.example"
syntax: ".env"
payload: {
groups: [
for g in app.corteza.options {
title: "# " + strings.Join(strings.Split(g.title, "\n"), "\n# ")
if (g.intro != _|_) {
intro: "# " + strings.Join(strings.Split(g.intro, "\n"), "\n# ")
}
options: [
for o in g.options {
handle: o.handle
env: o.env
type: o.type
defaultGoExpr?: string
if (o.defaultGoExpr != _|_) {
defaultGoExpr: o.defaultGoExpr
}
defaultValue?: string
if (o.defaultValue != _|_) {
defaultValue: o.defaultValue
}
if (o.description != _|_) {
description: "# " + strings.Join(strings.Split(o.description, "\n"), "\n# ")
}
},
]
},
]
}
},
]
@@ -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}]
},
]
}
},
]
+41
View File
@@ -0,0 +1,41 @@
package codegen
import (
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
[...schema.#codegen] &
[
for cmp in app.corteza.components {
template: "gocode/rbac/$component_types_rbac.go.tpl"
output: "\(cmp.ident)/types/rbac.gen.go"
payload: {
package: "types"
cmpIdent: cmp.ident
// Operation/resource validators, grouped by resource
types: [
for res in cmp.resources if res.rbac != _|_ {
const: "\(res.expIdent)ResourceType"
type: res.fqrt
resFunc: "\(res.expIdent)RbacResource"
tplFunc: "\(res.expIdent)RbacResourceTpl"
attFunc: "\(res.expIdent)RbacAttributes"
goType: res.expIdent
references: [ for p in res.parents {p}, {param: "id", refField: "ID"}]
},
{
const: "ComponentResourceType"
type: cmp.fqrt
resFunc: "ComponentRbacResource"
tplFunc: "ComponentRbacResourceTpl"
attFunc: "ComponentRbacAttributes"
goType: "Component"
component: true
},
]
}
},
]
+251
View File
@@ -0,0 +1,251 @@
package codegen
import (
"strings"
"list"
"github.com/cortezaproject/corteza-server/app"
"github.com/cortezaproject/corteza-server/codegen/schema"
)
_StoreResource: {
res = "res": schema.#Resource
typePkg = "typePkg": string
pkAttrNames: [string, ...]
hasPrimaryKey: res.model.indexes.primary != _|_
if hasPrimaryKey {
// primary key flag is no longer explicitly set on
// the attribute but via model.indexes.primary
pkAttrNames: [
for f in res.model.indexes.primary.fields { f.attribute }
]
}
result: {
ident: res.store.ident
identPlural: res.store.identPlural
expIdent: res.store.expIdent
expIdentPlural: res.store.expIdentPlural
modelIdent: res.model.ident
goType: "\(typePkg).\(res.expIdent)"
goSetType: "\(typePkg).\(res.expIdent)Set"
goFilterType: "\(typePkg).\(res.filter.expIdent)"
struct: [ for attr in res.model.attributes if attr.store {
"ident": attr.ident
"expIdent": attr.expIdent
"storeIdent": attr.storeIdent
"name": attr.name
"primaryKey": list.Contains(pkAttrNames, attr.name)
"ignoreCase": attr.ignoreCase
"goType": strings.Replace(attr.goType, "types.", "\(typePkg).", 1)
}]
filter: {
// query fields as defined in struct
"query": [ for name in res.filter.query {res.model.attributes[name]}],
// filter by nil state as defined in filter
"byNilState": [ for name in res.filter.byNilState {res.filter.struct[name]}]
// filter by false as defined in filter
"byFalseState": [ for name in res.filter.byFalseState {res.filter.struct[name]}]
// filter by value as defined in filter
// @todo this should be pulled from the struct
"byValue": [ for name in res.filter.byValue {res.filter.struct[name]}]
"byLabel": res.features.labels
"byFlag": res.features.flags
}
auxIdent: "aux\(expIdent)"
auxStruct: struct
features: {
paging: res.features.paging
sorting: res.features.sorting
checkFn: res.features.checkFn
}
api: {
if res.store.api != _|_ {
_base: {
"ident": res.store.ident
"expStoreIdent": res.store.expIdentPlural
"goType": goType
"goFilterType": goFilterType
"auxIdent": auxIdent
}
if hasPrimaryKey {
deleteByPK: {
"pkAttrNames": pkAttrNames,
attributes: [ for attr in pkAttrNames { res.model.attributes[attr] } ]
_expIdents: strings.Join([ for attr in pkAttrNames { res.model.attributes[attr].expIdent } ], "")
"expFnIdent": "Delete\(res.store.expIdent)By\(_expIdents)"
}
}
lookups: [
for l in res.store.api.lookups {
_base
"expFnIdent": l.expIdent
if (l.description != _|_) {
description: "// \(l.expIdent) " + strings.Join(strings.Split(l.description, "\n"), "\n// ")
}
// Copy all relevant fields from the struct
"args": [
for name in l.fields {
let attr = res.model.attributes[name]
"ident": attr.ident
"storeIdent": attr.storeIdent
"goType": attr.goType
"ignoreCase": attr.ignoreCase
},
]
"nullConstraint": l.nullConstraint
"returnType": "\(goType)"
"collectionFnIdent": "\(res.store.ident)Collection"
},
]
// all additional store functions we need for this resource
functions: [
for f in res.store.api.functions {
_base
"expFnIdent": f.expIdent
if (f.description != _|_) {
description: "// \(f.expIdent) " + strings.Join(strings.Split(f.description, "\n"), "\n// ")
}
"args": [ for a in f.args {
"ident": a.ident
"goType": strings.Replace(a.goType, "types.", "\(typePkg).", 1)
"spread": a.spread
}]
"return": [ for r in f.return {strings.Replace(r, "types.", "\(typePkg).", 1)}]
},
]
sortableFields: {
_base
"fnIdent": "sortable\(expIdent)Fields"
fields: {
for attr in res.model.attributes if attr.sortable || attr.unique || list.Contains(pkAttrNames, attr.name) {
{
"\(strings.ToLower(attr.name))": attr.name
"\(strings.ToLower(attr.ident))": attr.name
}
}
}
}
collectCursorValues: {
_base
"fnIdent": "collect\(expIdent)CursorValues"
fields: [ for attr in res.model.attributes if attr.sortable || attr.unique || list.Contains(pkAttrNames, attr.name) {
attr
"primaryKey": list.Contains(pkAttrNames, attr.name)
} ]
primaryKeys: [ for attr in res.model.attributes if list.Contains(pkAttrNames, attr.name) {attr} ]
}
checkConstraints: {
_base
"fnIdent": "check\(expIdent)Constraints"
checks: [
for lookup in res.store.api.lookups if lookup.constraintCheck {
lookupFnIdent: lookup.expIdent
fields: [ for name in lookup.fields {res.model.attributes[name]}]
nullConstraint: [
for f in res.model.attributes if list.Contains(lookup.nullConstraint, f.name) {
"expIdent": f.expIdent
},
]
},
]
}
}
}
}
}
// Codegen template payload, reused for multiple outputs
_payload: {
package: string | *"store"
imports: {
// per-component type imports
for cmp in app.corteza.components for res in cmp.resources if res.store != _|_ {
"github.com/cortezaproject/corteza-server/\(cmp.ident)/types": "\(cmp.ident)Type"
}
for res in app.corteza.resources if res.store != _|_ {
"\(res.package.import)": "\(res.package.ident)Type"
}
for cmp in app.corteza.components for res in cmp.resources for i in res.imports {
"\(i.import)": ""
}
}
types: {
// for each resource in every store with store and actions defined
for cmp in app.corteza.components for res in cmp.resources if res.store != _|_ {
// use _Store resource as a function (https://cuetorials.com/patterns/functions/)
// and pass res(ource) and type-package string in as "arguments"
"\(res.store.ident)": { _StoreResource & { "res": res, "typePkg": "\(cmp.ident)Type" } }.result
},
for res in app.corteza.resources if res.store != _|_ {
"\(res.store.ident)": { _StoreResource & { "res": res, "typePkg": "\(res.package.ident)Type" } }.result
}
}
}
[...schema.#codegen] &
[
{
"bulk": [
{
"template": "gocode/store/interfaces.go.tpl"
"output": "store/interfaces.gen.go"
}, {
"template": "gocode/store/rdbms/rdbms.go.tpl"
"output": "store/adapters/rdbms/rdbms.gen.go"
}, {
"template": "gocode/store/rdbms/aux_types.go.tpl"
"output": "store/adapters/rdbms/aux_types.gen.go"
}, {
"template": "gocode/store/rdbms/queries.go.tpl"
"output": "store/adapters/rdbms/queries.gen.go"
}, {
"template": "gocode/store/rdbms/filters.go.tpl"
"output": "store/adapters/rdbms/filters.gen.go"
}, {
"template": "gocode/store/tests/all.go.tpl"
"output": "store/tests/all_test.go"
},
]
"payload": { _payload }
},
]
+30
View File
@@ -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
},
]
}
},
]
+101
View File
@@ -0,0 +1,101 @@
package main
import (
"encoding/json"
"flag"
"fmt"
"os"
"path"
"text/template"
"time"
"github.com/cortezaproject/corteza-server/pkg/cli"
)
type (
inOut struct {
Template string `json:"template"`
Output string `json:"output"`
Syntax string `json:"syntax"`
}
task struct {
inOut
Bulk []inOut `json:"bulk"`
Payload interface{} `json:"payload"`
}
)
var (
verbose bool
showHelp bool
tplRootPath string
outputBase string
)
func init() {
flag.BoolVar(&showHelp, "h", false, "show help")
flag.BoolVar(&verbose, "v", false, "be verbose")
flag.StringVar(&tplRootPath, "p", "codegen/assets/templates", "location of the template files")
flag.StringVar(&outputBase, "b", ".", "base dir for output")
flag.Parse()
}
// Takes JSON input with codegen tasks and definitions and generates files
func main() {
if showHelp {
flag.PrintDefaults()
os.Exit(0)
}
var (
input = json.NewDecoder(os.Stdin)
tasks = make([]*task, 0)
tpl *template.Template
err error
)
started := time.Now()
print("Waiting for stdin ...")
if err = input.Decode(&tasks); err != nil {
cli.HandleError(fmt.Errorf("failed to decode input from standard input: %v", err))
}
println(time.Now().Sub(started).Round(time.Second)/time.Second, "sec")
if tpl, err = loadTemplates(baseTemplate(), tplRootPath); err != nil {
cli.HandleError(fmt.Errorf("failed to load templates: %v", err))
}
for _, j := range tasks {
if len(j.Bulk) == 0 {
j.Bulk = append(j.Bulk, j.inOut)
}
for _, o := range j.Bulk {
output := path.Join(outputBase, o.Output)
print(fmt.Sprintf("generating %s (from %s) ...", output, o.Template))
switch o.Syntax {
case "go":
err = writeFormattedGo(output, tpl.Lookup(o.Template), j.Payload)
default:
err = write(output, tpl.Lookup(o.Template), j.Payload)
}
if err != nil {
cli.HandleError(fmt.Errorf("failed to write template: %v", err))
} else {
print("done\n")
}
}
}
}
func print(msg string) {
if verbose {
_, _ = fmt.Fprint(os.Stderr, msg)
}
}
+105
View File
@@ -0,0 +1,105 @@
package main
import (
"bytes"
"fmt"
"go/format"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"text/template"
"github.com/Masterminds/sprig"
)
func baseTemplate() *template.Template {
return template.New("").
Funcs(sprig.TxtFuncMap())
}
func loadTemplates(rTpl *template.Template, rootDir string) (*template.Template, error) {
cleanRoot := filepath.Clean(rootDir)
pfx := len(cleanRoot) + 1
return rTpl, filepath.Walk(cleanRoot, func(path string, info os.FileInfo, err error) error {
if err != nil {
return err
}
if info.IsDir() || !strings.HasSuffix(path, ".tpl") || err != nil {
return err
}
b, err := ioutil.ReadFile(path)
if err != nil {
return err
}
name := path[pfx:]
rTpl, err = rTpl.New(name).Parse(string(b))
return err
})
}
func writeFormattedGo(dst string, tpl *template.Template, payload interface{}) error {
return write(dst, tpl, payload, func(in io.ReadWriter) (out io.ReadWriter, err error) {
var (
org, bb []byte
)
if org, err = ioutil.ReadAll(in); err != nil {
return
}
cp := bytes.NewBuffer(org)
if bb, err = format.Source(cp.Bytes()); err != nil {
// output error and return un-formatted source
_, _ = fmt.Fprintf(os.Stderr, "%s fmt warn: %v\n", dst, err)
return cp, nil
}
return bytes.NewBuffer(bb), nil
})
}
func write(dst string, tpl *template.Template, payload interface{}, pp ...func(io.ReadWriter) (io.ReadWriter, error)) (err error) {
var (
output io.WriteCloser
buf io.ReadWriter
)
if tpl == nil {
return fmt.Errorf("could not find template for %s", dst)
}
buf = &bytes.Buffer{}
if err := tpl.Execute(buf, payload); err != nil {
return err
}
for _, proc := range pp {
if buf, err = proc(buf); err != nil {
return
}
}
if dst == "" || dst == "-" {
output = os.Stdout
} else {
if output, err = os.Create(dst); err != nil {
return err
}
defer output.Close()
}
if _, err = io.Copy(output, buf); err != nil {
return err
}
return nil
}