RelationService looks up TranslationWriter like CRUDService so CreateChild can lift locale maps before fillChild, write Polish after the child PK, and return a hydrated title map. Co-authored-by: Cursor <cursoragent@cursor.com>
202 lines
5.9 KiB
Go
202 lines
5.9 KiB
Go
package cabana
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"reflect"
|
|
"testing"
|
|
"testing/fstest"
|
|
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
const mlParentFormConfig = `name: journals
|
|
form: ~/plugins/acme/demo/models/journal/fields.yaml
|
|
modelClass: Journal
|
|
`
|
|
|
|
const mlParentListConfig = `modelClass: Journal
|
|
list: ~/plugins/acme/demo/models/journal/columns.yaml
|
|
recordsPerPage: 20
|
|
toolbar:
|
|
buttons: [create]
|
|
`
|
|
|
|
const mlParentFields = `fields:
|
|
entries:
|
|
type: relation-manager
|
|
relation: entries
|
|
`
|
|
|
|
const mlParentColumns = `columns:
|
|
id:
|
|
label: ID
|
|
`
|
|
|
|
const mlParentRelation = `entries:
|
|
label: Entries
|
|
view:
|
|
list:
|
|
columns:
|
|
title:
|
|
label: Title
|
|
toolbarButtons: create|update
|
|
manage:
|
|
form: $/acme/demo/models/entry/fields.yaml
|
|
`
|
|
|
|
const mlChildFields = `fields:
|
|
title:
|
|
label: Title
|
|
type: mltext
|
|
required: true
|
|
`
|
|
|
|
type mlJournal struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
Entries []mlEntry `gorm:"-"`
|
|
}
|
|
|
|
func (mlJournal) TableName() string { return "cabana_ml_journals" }
|
|
func (mlJournal) Fillable() []string { return []string{} }
|
|
func (mlJournal) Rules() map[string]string { return map[string]string{} }
|
|
|
|
type mlEntry struct {
|
|
ID uint `gorm:"column:id;primaryKey"`
|
|
JournalID *uint `gorm:"column:journal_id"`
|
|
Title string `gorm:"column:title"`
|
|
}
|
|
|
|
func (mlEntry) TableName() string { return "cabana_ml_entries" }
|
|
func (mlEntry) Fillable() []string { return []string{"title"} }
|
|
func (mlEntry) Rules() map[string]string { return map[string]string{"title": "required"} }
|
|
|
|
type mlJournalController struct{}
|
|
|
|
func (mlJournalController) ID() string { return "acme.demo.journals" }
|
|
func (mlJournalController) ModelName() string { return "Journal" }
|
|
func (mlJournalController) ConfigDir() string { return "controllers/journals" }
|
|
func (mlJournalController) NewRecord() any { return &mlJournal{} }
|
|
|
|
func (mlJournalController) FormExtendQuery(_ context.Context, q *gorm.DB) *gorm.DB {
|
|
return q
|
|
}
|
|
|
|
func (mlJournalController) AdminRelationContracts() []RelationContract {
|
|
return []RelationContract{{
|
|
Name: "entries",
|
|
Kind: RelationHasMany,
|
|
NewRelated: func() any { return &mlEntry{} },
|
|
ForeignKey: "journal_id",
|
|
Columns: map[string]string{"title": "title"},
|
|
}}
|
|
}
|
|
|
|
var (
|
|
_ pact.AdminController = mlJournalController{}
|
|
_ pact.AdminRecordSource = mlJournalController{}
|
|
_ pact.FormExtendQuery = mlJournalController{}
|
|
_ AdminRelationContractProvider = mlJournalController{}
|
|
)
|
|
|
|
func mlRelationFS() fstest.MapFS {
|
|
return fstest.MapFS{
|
|
"controllers/journals/config_list.yaml": &fstest.MapFile{Data: []byte(mlParentListConfig)},
|
|
"controllers/journals/config_form.yaml": &fstest.MapFile{Data: []byte(mlParentFormConfig)},
|
|
"controllers/journals/config_relation.yaml": &fstest.MapFile{Data: []byte(mlParentRelation)},
|
|
"models/journal/columns.yaml": &fstest.MapFile{Data: []byte(mlParentColumns)},
|
|
"models/journal/fields.yaml": &fstest.MapFile{Data: []byte(mlParentFields)},
|
|
"models/entry/fields.yaml": &fstest.MapFile{Data: []byte(mlChildFields)},
|
|
}
|
|
}
|
|
|
|
func mlRelationCompiled(t *testing.T) *CompiledController {
|
|
t.Helper()
|
|
reg, err := compileRegistry([]controllerRef{{
|
|
plugin: formPlugin{fsys: mlRelationFS()},
|
|
ctl: mlJournalController{},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("registry: %v", err)
|
|
}
|
|
cc, ok := reg.Get("acme.demo.journals")
|
|
if !ok || cc.Form == nil || cc.Relations["entries"] == nil || cc.Relations["entries"].child == nil {
|
|
t.Fatalf("compiled journal missing entries form: %+v", cc)
|
|
}
|
|
return cc
|
|
}
|
|
|
|
func TestRelationChildMLNestedSave(t *testing.T) {
|
|
_, db := newListService(t)
|
|
if err := db.Migrator().DropTable(&mlEntry{}, &mlJournal{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := db.AutoMigrate(&mlJournal{}, &mlEntry{}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
parent := mlJournal{}
|
|
if err := db.Create(&parent).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
writer := &recordingWriter{defaultLocale: "en", enabled: []string{"en", "pl"}}
|
|
svc := RelationService{DB: db, writer: writer}
|
|
cc := mlRelationCompiled(t)
|
|
childForm := cc.Relations["entries"].child
|
|
|
|
projected := ProjectWritableFields(childForm, map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
"extra": map[string]any{"nested": true},
|
|
})
|
|
if len(projected) != 0 {
|
|
t.Fatalf("unlifted nested maps reached projection: %#v", projected)
|
|
}
|
|
|
|
created, err := svc.CreateChild(context.Background(), cc, "entries", parent.ID, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "pl": "Witaj"},
|
|
"extra": map[string]any{"nested": true},
|
|
}})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
got, ok := created.Data["title"].(map[string]string)
|
|
wantTitle := map[string]string{"en": "Hello", "pl": "Witaj"}
|
|
if !ok || !reflect.DeepEqual(got, wantTitle) {
|
|
t.Fatalf("create title = %#v", created.Data["title"])
|
|
}
|
|
|
|
var row mlEntry
|
|
if err := db.First(&row).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if row.Title != "Hello" {
|
|
t.Fatalf("host title = %q", row.Title)
|
|
}
|
|
if writer.attrs["en"] != nil {
|
|
t.Fatalf("default locale duplicated: %#v", writer.attrs["en"])
|
|
}
|
|
if writer.attrs["pl"]["title"] != "Witaj" {
|
|
t.Fatalf("Polish attributes = %#v", writer.attrs)
|
|
}
|
|
|
|
before := int64(0)
|
|
if err := db.Model(&mlEntry{}).Count(&before).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
_, err = svc.CreateChild(context.Background(), cc, "entries", parent.ID, RecordInput{Body: map[string]any{
|
|
"title": map[string]any{"en": "Hello", "de": "Hallo"},
|
|
}})
|
|
var ve *ValidationError
|
|
if err == nil || !errors.As(err, &ve) {
|
|
t.Fatalf("undeclared locale err = %v", err)
|
|
}
|
|
after := int64(0)
|
|
if err := db.Model(&mlEntry{}).Count(&after).Error; err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if after != before {
|
|
t.Fatalf("undeclared locale inserted a child: before=%d after=%d", before, after)
|
|
}
|
|
}
|