Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 48 additions & 0 deletions internal/module/artifact/generate/generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,12 @@ import (
"os"
"path/filepath"
"reflect"
"strconv"
"strings"
"testing"
"time"

"github.com/choysum-dev/choysum/internal/esbplugins"
"github.com/choysum-dev/choysum/internal/esbplugins/backendplugin"
module "github.com/choysum-dev/choysum/internal/module/artifact/result"
"github.com/choysum-dev/choysum/internal/parser/backendtsparser"
Expand Down Expand Up @@ -85,6 +88,50 @@ func seedGeneratorMetaTables(t *testing.T, runtimeScope *generatorTestScope) {
}
}

// seedAbstractBaseModel inserts an abstract BaseModel IrModel at BaseModelModuleSpec path
// with conventional services. Pass nil for the default name set; pass a non-nil empty
// slice to seed BaseModel with no services.
func seedAbstractBaseModel(t *testing.T, runtimeScope *generatorTestScope, names []string) {
t.Helper()
seedGeneratorMetaTables(t, runtimeScope)

path, _ := meta.BaseModelModuleSpec(runtimeScope)
path = esbplugins.NormalizePath(path)
if !strings.HasSuffix(path, ".ts") {
path = path + ".ts"
}
if names == nil {
names = []string{
"Browse", "BrowseMany", "Search", "NameSearch", "Copy", "Count",
"Create", "CreateMany", "Update", "UpdateById", "Delete", "DeleteById",
"DefaultGet", "FieldsGet", "GetFieldTranslations", "UpdateFieldTranslations",
"Onchange", "ReadGroup", "ReadGroupCount",
}
}

model := &meta.IrModel{
BaseModel: meta.BaseModel{Id: sql.NullString{String: "abstract-base-model", Valid: true}},
Name: "BaseModel",
Path: path,
Abstract: true,
}
if err := runtimeScope.db.Create(model).Error; err != nil {
t.Fatalf("create abstract BaseModel: %v", err)
}
for i, name := range names {
svc := &meta.IrService{
BaseModel: meta.BaseModel{Id: sql.NullString{String: "base-svc-" + strconv.Itoa(i), Valid: true}},
Name: name,
AccessibilityModifier: "public",
IsStatic: true,
ModelId: model.Id,
}
if err := runtimeScope.db.Create(svc).Error; err != nil {
t.Fatalf("create BaseModel service %s: %v", name, err)
}
}
}

func seedGeneratorAppFixture(t *testing.T, runtimeScope *generatorTestScope) (*meta.IrApplication, *meta.IrModule) {
t.Helper()
seedGeneratorMetaTables(t, runtimeScope)
Expand Down Expand Up @@ -467,6 +514,7 @@ func TestGeneratorEntryPoints(t *testing.T) {
t.Run("GenerateCtx runs full success pipeline", func(t *testing.T) {
runtimeScope := newGeneratorScope(t)
_, mod := seedGeneratorAppFixture(t, runtimeScope)
seedAbstractBaseModel(t, runtimeScope, nil)
protoDir := t.TempDir()
webDir := t.TempDir()
serviceDir := t.TempDir()
Expand Down
91 changes: 61 additions & 30 deletions internal/module/artifact/generate/webapistore.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,21 @@ import (
"context"
_ "embed"
"encoding/json"
"errors"
"os"
"path/filepath"
"strconv"
"strings"
"text/template"

"github.com/choysum-dev/choysum/internal/esbplugins"
module "github.com/choysum-dev/choysum/internal/module/artifact/result"
"github.com/choysum-dev/choysum/internal/module/artifact/staging"
"github.com/choysum-dev/choysum/pkg/meta"
"github.com/choysum-dev/choysum/pkg/scope"
"github.com/ettle/strcase"
xfmt "golang.org/x/exp/errors/fmt"
"gorm.io/gorm"
)

//go:embed webapistore.ts.tpl
Expand Down Expand Up @@ -324,6 +327,60 @@ func analyzeRelationFields(model *meta.IrModel) ([]RelationFieldInfo, []string)
return relationFields, importModels
}

// resolveBaseServiceNames loads conventional service names from the abstract BaseModel
// IrModel (by BaseModelModuleSpec path). These names are filtered out of generated
// XxxStore interfaces because they are already declared on the hand-written WebModelStore.
// Do not maintain a parallel hardcoded name list here.
func resolveBaseServiceNames(runtimeScope scope.Scope) (map[string]bool, error) {
path, _ := meta.BaseModelModuleSpec(runtimeScope)
return resolveBaseServiceNamesAtPath(runtimeScope, path)
}

func resolveBaseServiceNamesAtPath(runtimeScope scope.Scope, path string) (map[string]bool, error) {
path = strings.TrimSpace(path)
if path == "" {
return nil, xfmt.Errorf("base model module path is empty")
}
path = esbplugins.NormalizePath(path)
pathNoExt := strings.TrimSuffix(path, ".ts")
pathWithExt := pathNoExt + ".ts"

var model meta.IrModel
result := runtimeScope.Session().
Preload("Services", func(db *gorm.DB) *gorm.DB {
return db.Order("id ASC")
}).
Where("(path = ? OR path = ?) AND abstract = ?", pathNoExt, pathWithExt, true).
Order("id DESC").
Take(&model)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
if result.Error != nil {
if errors.Is(result.Error, gorm.ErrRecordNotFound) {
return nil, xfmt.Errorf("abstract BaseModel not found at path %s (or %s)", pathNoExt, pathWithExt)
}
return nil, xfmt.Errorf("load abstract BaseModel by path %s: %w", pathWithExt, result.Error)
}

names := conventionalBaseServiceNames(model.Services)
if len(names) == 0 {
return nil, xfmt.Errorf("abstract BaseModel at path %s has no conventional services", model.Path)
}
return names, nil
}

func conventionalBaseServiceNames(services []*meta.IrService) map[string]bool {
names := make(map[string]bool)
for _, svc := range services {
if svc == nil {
continue
}
if !meta.IsConventionalModelService(svc.AccessibilityModifier, svc.IsStatic, svc.Name) {
continue
}
names[svc.Name] = true
}
return names
}

func (g *webApiStoreGenerator) generate(ctx context.Context, app *meta.IrApplication) ([]*module.GeneratorResult, error) {
if ctx == nil {
ctx = context.Background()
Expand All @@ -334,35 +391,9 @@ func (g *webApiStoreGenerator) generate(ctx context.Context, app *meta.IrApplica
return nil, nil
}

// Preload BaseModel service names so the template can filter base CRUD services.
baseServiceNames := map[string]bool{
"Browse": true,
"BrowseMany": true,
"Search": true,
"NameSearch": true,
"Copy": true,
"Count": true,
"Create": true,
"CreateMany": true,
"Update": true,
"UpdateById": true,
"Delete": true,
"DeleteById": true,
"DefaultGet": true,
"FieldsGet": true,
"GetFieldTranslations": true,
"UpdateFieldTranslations": true,
"Onchange": true,
"ReadGroup": true,
"ReadGroupCount": true,
}
for _, m := range app.Models {
if m.Name == "BaseModel" {
for _, svc := range m.Services {
baseServiceNames[svc.Name] = true
}
break
}
baseServiceNames, err := resolveBaseServiceNames(g.runtimeScope)
if err != nil {
return nil, err
Comment thread
buke marked this conversation as resolved.
}

funcMap := template.FuncMap{
Expand All @@ -373,7 +404,7 @@ func (g *webApiStoreGenerator) generate(ctx context.Context, app *meta.IrApplica
p := strings.ReplaceAll(path, runtimeOpts.modulesPath, "@")
return strings.TrimSuffix(p, ".ts")
},
// Check whether the service name comes from BaseModel.
// True when name is a BaseModel conventional service (already on WebModelStore).
"IsBaseService": func(name string) bool {
return baseServiceNames[name]
},
Expand Down
Loading