Refactored store codegen, replace squirrel with goqu

This commit is contained in:
Denis Arh
2022-04-23 16:34:04 +02:00
parent e75a57b0e9
commit 773d64636f
544 changed files with 47139 additions and 39359 deletions
+1
View File
@@ -1 +1,2 @@
/build
tmp*.cue
-3
View File
@@ -18,6 +18,3 @@ $(DEF_DOCS): $(CUE) $(JSONTPLEXEC)
@[ "${DOCS_DIR}" ] || ( echo "DOCS_DIR is not set, run make like this: make docs DOCS_DIR=/path/to/corteza-docs"; exit 1 )
@ 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)
gen:
@ rm $(JSONTPLEXEC)
@@ -1,206 +0,0 @@
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/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 }}{{ . }} 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
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, svc.store, systemTypes.ResourceTranslationFilter{
Resource: res,
})
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...)
aux = current.Old(rr)
_ = aux.Walk(func(cc *systemTypes.ResourceTranslation) error {
cc.UpdatedAt = now()
cc.UpdatedBy = me.Identity()
return nil
})
sysLocale = append(sysLocale, aux...)
}
err = store.UpsertResourceTranslation(ctx, svc.store, sysLocale...)
if err != nil {
return err
}
// Reload ALL resource translations
// @todo we could probably do this more selectively and refresh only updated resources?
_ = locale.Global().ReloadResourceTranslations(ctx)
return nil
}
func (svc resourceTranslationsManager) Locale() locale.Resource {
return svc.locale
}
{{- range .resources }}
func (svc resourceTranslationsManager) {{ .expIdent }}(ctx context.Context, {{ range .references }}{{ . }} uint64, {{ end }}) (locale.ResourceTranslationSet, error) {
var (
err error
out locale.ResourceTranslationSet
res *types.{{ .expIdent }}
k types.LocaleKey
)
res, err = svc.load{{ .expIdent }}(ctx, svc.store, {{ range .references }}{{ . }}, {{ end }})
if err != nil {
return nil, err
}
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}}
}
{{ 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
}
@@ -1,120 +0,0 @@
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({{ if .references }}{{ range .references }}r.{{ . }},{{ end }}{{ 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({{ if .references }}{{ range .references }}{{ . }} uint64,{{ end }}{{ end }}) string {
{{- if .references }}
cpts := []interface{{"{}"}}{
{{ .expIdent }}ResourceTranslationType,
{{- range .references }}
strconv.FormatUint({{ . }}, 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) {
var aux *locale.ResourceTranslation
{{- 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 }}
if r.{{ .fieldPath }} != "" {
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,133 @@
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/locale"
"golang.org/x/text/language"
"github.com/cortezaproject/corteza-server/pkg/report"
)
{{ 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)
// 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, *zap.Logger) 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
{{ .api.deleteByPK.expFnIdent }}(ctx context.Context, {{ template "extraArgs" .ident }} {{ range .api.deleteByPK.primaryKeys }}{{ .ident }} {{ .goType }},{{ end }}) error
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...)
}
// 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.primaryKeys }}{{ .ident }} {{ .goType }},{{ end }}) error {
return s.{{ .api.deleteByPK.expFnIdent }}(ctx, {{ template "extraParams" .ident }}{{ range .api.deleteByPK.primaryKeys }}{{ .ident }},{{ 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 scalable) (error) {
return row.Scan(
{{- range .auxStruct }}
&aux.{{ .expIdent }},
{{- end }}
)
}
{{ end }}
@@ -0,0 +1,91 @@
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({{ .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 "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.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 }}
@@ -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" .settings.rdbms.table }})
// {{ .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,606 @@
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.config.Dialect, 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.config.Dialect, 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.config.Dialect, 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.config.Dialect, {{ .ident }}PrimaryKeys(rr[i]))); err != nil {
return
}
}
return nil
}
// Delete{{ .expIdent }}ByID 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.primaryKeys }}{{ .ident }} {{ .goType }},{{ end }}) error {
return s.Exec(ctx, {{ .ident }}DeleteQuery(s.config.Dialect, goqu.Ex{
{{- range .api.deleteByPK.primaryKeys }}
{{ printf "%q" .storeIdent }}: {{ .ident }},
{{- 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.config.Dialect))
}
// 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 {
// 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 fetchFullPageOf{{ .expIdentPlural }} fn used for cursor creation so it MUST keep the initial
// direction information
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,
)
f.PageCursor = nil
if err != nil {
return nil, f, err
}
{{ else }}
set, _, err = s.Query{{ .expIdentPlural }}(ctx, f)
if err != nil {
return nil, f, err
}
{{ end }}
return set, f, nil
}
{{ if .features.paging }}
// fetchFullPageOf{{ .expIdentPlural }} collects all requested results.
//
// Function applies:
// - cursor conditions (where ...)
// - limit
//
// Main responsibility of this function is to perform additional sequential queries in case when not enough results
// are collected due to failed check on a specific row (by check fn).
//
// Function then moves cursor to the last item fetched
//
// This function is auto-generated
func (s Store) fetchFullPageOf{{ .expIdentPlural }}(
ctx context.Context,
filter {{ .goFilterType }},
) (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
tryFilter = filter
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.config.Filters.{{ .expIdent }} != nil {
// extended filter set
tExpr, f, err = s.config.Filters.{{ .expIdent }}(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 tExpr, err = cursor(f.PageCursor); err != nil {
return
} else {
expr = append(expr, tExpr...)
}
}
{{ end }}
query := {{ .ident }}SelectQuery(s.config.Dialect).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
}
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.config.Dialect).Where(
{{- range .args }}
{{- if .ignoreCase }}
s.config.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 }}
}
}
}
)
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 }}
}
+8 -1
View File
@@ -1,6 +1,6 @@
package schema
#codegen: {
#_ioSpec: {
template: string
output: string
@@ -8,6 +8,13 @@ package schema
if output =~ "\\.adoc$" {
syntax: "adoc"
}
}
#codegen: {
bulk?: [...#_ioSpec]
if bulk == _|_ {
#_ioSpec
}
payload: _
}
+1 -1
View File
@@ -12,7 +12,7 @@ import (
platform: #baseHandle
resources: {
[key=_]: {"handle": key, "component": handle, "platform": platform} & #resource
[key=_]: {"handle": key, "component": handle, "platform": platform} & #Resource
}
fqrn: platform + "::" + handle
+8 -4
View File
@@ -4,11 +4,15 @@ package schema
ident: #baseHandle | *"corteza"
options: [...#optionsGroup]
components: [...{platform: ident} & #component]
// env-var definitions
// options: {}
//
components: [...{platform: ident} & #component]
resources: {
[key=#handle]: #Resource & {
"handle": key,
"platform": ident
}
}
// automation: {
// types: ....
+93 -60
View File
@@ -1,6 +1,14 @@
package schema
#resource: #_base & {
import (
"strings"
)
#Resource: {
#_base
imports: [...{ import: string }]
// copy field values from #_base
handle: handle, ident: ident, expIdent: expIdent
@@ -10,9 +18,40 @@ package schema
// Fully qualified resource name
fqrn: string | *(platform + "::" + component + ":" + handle)
// fields: #Fields
struct: #Struct
filter: {
"expIdent": #expIdent | *"\(expIdent)Filter"
struct: #Struct
// 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
// 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
@@ -35,64 +74,58 @@ package schema
}
}
// List of known keys for resource translation
// locale?: {
// [Name=_]: {
// name: Name & #Handle
// path: string
// custom: bool | *false
// }
// }
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(struct[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]
}]
}
settings: {
defaultOrder: [...{ field: string, descending: bool | *false }]
rdbms: {
// use resource handle (plural) as RDBMS table name as default
table: string | *"\(strings.Replace(handle, "-", "_", -1))s"
}
}
}
}
//#fields: {
// // Each field can be
// [key=_]: #fields | *({name: key} & #field)
//}
//
//#field: {
// name: #expIdent
// unique: bool | *false
//
// // Golang type (built-in or other)
// type: string | *"string"
//
// // System fields,
// system: bool | *false
//
// if name =~ "At$" {
// type: string | *"*time.Time"
// }
//}
#storeFunction: {
expIdent: #expIdent
args: [...string]
return: [...string]
}
//#Operations: {
// [Operation=_]: {operation: Operation} & #Operation
//}
//#Operation: {
// name: #ExpIdent
// description: string
// can: string | false | *"\(name)"
//}
//idField: {
// // Expecting ID field to allways have name ID
// name: "ID"
// unique: true
//
// // Service fields,
// // @todo We might want to have a better name for this
// // service: true
//
// // @todo someday we'll replace this with the "ID" type
// type: "uint64"
//}
//
//handleField: {
// // Expecting ID field to allways have name ID
// name: "handle"
// unique: true
//
// // @todo someday we'll replace this with the "ID" type
// type: "string" & #handle
//}
#PkgResource: #Resource & {
package: {
ident: #ident
import: string
}
}
+9 -3
View File
@@ -13,7 +13,7 @@ import (
// More liberal then identifier, allows underscores and dots
#handle: =~"^[A-Za-z][a-zA-Z0-9_\\-\\.]*[a-zA-Z0-9]+$"
// More liberal then identifier, allows underscores and dots
// More liberal then identifier
#baseHandle: =~"^[a-z][a-z0-9-]*[a-z0-9]+$"
#_base: {
@@ -22,11 +22,17 @@ import (
handle: #baseHandle | *"base"
_words: strings.Replace(strings.Replace(strings.Replace(handle, "-", " ", -1), "_", " ", -1), ".", " ", -1)
// lowercased (unexported, golang) identifier
// lowercase (unexported, golang) identifier
ident: #ident | *strings.ToCamel(strings.Replace(strings.ToTitle(_words), " ", "", -1))
// upercased (exported, golang) identifier
// plural
identPlural: #ident | *"\(ident)s"
// uppercase (exported, golang) identifier
expIdent: #expIdent | *strings.Replace(strings.ToTitle(_words), " ", "", -1)
// plural exported
expIdentPlural: #expIdent | *"\(expIdent)s"
...
}
+103
View File
@@ -0,0 +1,103 @@
package schema
import (
"strings"
)
#Struct: {
// Each field can be
[name=_]: {"name": name} & #StructField
}
// logic in struct fields is a bit different
#StructField: {
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
storeIdent: #ident | *name
store: bool | *true
unique: bool | *false
sortable: bool | *false
descending: bool | *false
primaryKey: bool | *false
ignoreCase: bool | *false
#StructJsonTag
}
IdField: #StructField & {
// Expecting ID field to always have name ID
name: "id"
expIdent: "ID"
primaryKey: true
unique: true
// @todo someday we'll replace this with the "ID" type
goType: "uint64"
}
HandleField: #StructField & {
// Expecting ID field to always have name handle
name: "handle"
unique: true
ignoreCase: true
goType: "string"
}
SortableTimestampField: #StructField & {
sortable: true
goType: "time.Time"
}
SortableTimestampNilField: #StructField & {
sortable: true
goType: "*time.Time"
}
#StructJsonTag: {
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)\""
}
}
+230
View File
@@ -0,0 +1,230 @@
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
result: {
ident: res.store.ident
identPlural: res.store.identPlural
expIdent: res.store.expIdent
expIdentPlural: res.store.expIdentPlural
goType: "\(typePkg).\(res.expIdent)"
goSetType: "\(typePkg).\(res.expIdent)Set"
goFilterType: "\(typePkg).\(res.filter.expIdent)"
struct: [ for f in res.struct if f.store {
"ident": f.ident
"expIdent": f.expIdent
"storeIdent": f.storeIdent
"name": f.name
"primaryKey": f.primaryKey
"ignoreCase": f.ignoreCase
"goType": strings.Replace(f.goType, "types.", "\(typePkg).", 1)
}]
filter: {
// query fields as defined in struct
"query": [ for name in res.filter.query {res.struct[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
}
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
}
deleteByPK: {
primaryKeys: [ for f in res.struct if f.primaryKey {f} ]
_pkExpNames: strings.Join([ for f in primaryKeys { f.expIdent } ], "")
"expFnIdent": "Delete\(res.store.expIdent)By\(_pkExpNames)"
}
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 f = res.struct[name]
"ident": f.ident
"storeIdent": f.storeIdent
"goType": f.goType
"ignoreCase": f.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 f in res.struct if f.sortable || f.unique || f.primaryKey {
{
"\(strings.ToLower(f.name))": f.name
"\(strings.ToLower(f.ident))": f.name
}
}
}
}
collectCursorValues: {
_base
"fnIdent": "collect\(expIdent)CursorValues"
fields: [ for f in res.struct if f.sortable || f.unique || f.primaryKey {f} ]
primaryKeys: [ for f in res.struct if f.primaryKey {f} ]
}
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.struct[name]}]
nullConstraint: [
for f in res.struct if list.Contains(lookup.nullConstraint, f.name) {
"expIdent": f.expIdent
},
]
},
]
}
}
}
settings: res.store.settings
}
}
// 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 }
},
]
+34 -18
View File
@@ -7,16 +7,24 @@ import (
"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 {
Template string `json:"template"`
Output string `json:"output"`
Syntax string `json:"syntax"`
Payload interface{} `json:"payload"`
inOut
Bulk []inOut `json:"bulk"`
Payload interface{} `json:"payload"`
}
)
@@ -49,32 +57,40 @@ func main() {
err error
)
print("Waiting for stdin ...\n")
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 {
output := path.Join(outputBase, j.Output)
print(fmt.Sprintf("generating %s (from %s) ...", output, j.Template))
switch j.Syntax {
case "go":
err = writeFormattedGo(output, tpl.Lookup(j.Template), j.Payload)
default:
err = write(output, tpl.Lookup(j.Template), j.Payload)
if len(j.Bulk) == 0 {
j.Bulk = append(j.Bulk, j.inOut)
}
if err != nil {
cli.HandleError(fmt.Errorf("failed to write template: %v", err))
} else {
print("done\n")
}
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")
}
}
}
}
+7 -5
View File
@@ -47,17 +47,19 @@ func loadTemplates(rTpl *template.Template, rootDir string) (*template.Template,
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 (
bb []byte
org, bb []byte
)
if bb, err = ioutil.ReadAll(in); err != nil {
if org, err = ioutil.ReadAll(in); err != nil {
return
}
if bb, err = format.Source(bb); err != nil {
// output error and return unformatted source
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 in, err
return cp, nil
}
return bytes.NewBuffer(bb), nil