diff --git a/dsl/model/cases_test.go b/dsl/model/cases_test.go new file mode 100644 index 00000000..f5c325d8 --- /dev/null +++ b/dsl/model/cases_test.go @@ -0,0 +1,229 @@ +package model + +import ( + "fmt" + "os" + "path/filepath" + "testing" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/yaoapp/gou/application" + "github.com/yaoapp/gou/model" + "github.com/yaoapp/kun/log" + "github.com/yaoapp/yao/config" + "github.com/yaoapp/yao/data" + "github.com/yaoapp/yao/dsl/types" + "github.com/yaoapp/yao/test" +) + +// systemModels system models +var systemModels = map[string]string{ + "__yao.dsl": "yao/models/dsl.mod.yao", +} + +func TestMain(m *testing.M) { + // Setup + test.Prepare(&testing.T{}, config.Conf) + defer test.Clean() + + // Load system models + model.WithCrypt([]byte(fmt.Sprintf(`{"key":"%s"}`, config.Conf.DB.AESKey)), "AES") + model.WithCrypt([]byte(`{}`), "PASSWORD") + err := loadSystemModels() + if err != nil { + log.Error("Load system models error: %s", err.Error()) + os.Exit(1) + } + + // Run tests + code := m.Run() + os.Exit(code) +} + +// loadSystemModels load system models +func loadSystemModels() error { + for id, path := range systemModels { + content, err := data.Read(path) + if err != nil { + return err + } + + // Parse model + var data map[string]interface{} + err = application.Parse(path, content, &data) + if err != nil { + return err + } + + // Set prefix + if table, ok := data["table"].(map[string]interface{}); ok { + if name, ok := table["name"].(string); ok { + table["name"] = "__yao_" + name + content, err = jsoniter.Marshal(data) + if err != nil { + log.Error("failed to marshal model data: %v", err) + return fmt.Errorf("failed to marshal model data: %v", err) + } + } + } + + // Load Model + mod, err := model.LoadSource(content, id, filepath.Join("__system", path)) + if err != nil { + log.Error("load system model %s error: %s", id, err.Error()) + return err + } + + // Drop table first + err = mod.DropTable() + if err != nil { + log.Error("drop table error: %s", err.Error()) + return err + } + + // Auto migrate + err = mod.Migrate(false, model.WithDonotInsertValues(true)) + if err != nil { + log.Error("migrate system model %s error: %s", id, err.Error()) + return err + } + } + + return nil +} + +// cleanTestData cleans test data from database +func cleanTestData() error { + m := model.Select("__yao.dsl") + err := m.DropTable() + if err != nil { + return err + } + err = m.Migrate(false, model.WithDonotInsertValues(true)) + if err != nil { + return err + } + return nil +} + +// getTestID generates a unique test ID +func getTestID() string { + return fmt.Sprintf("test_%d", time.Now().UnixNano()) +} + +// TestCase defines a single test case +type TestCase struct { + ID string + Source string + UpdatedSource string + Tags []string + Label string + Description string +} + +// NewTestCase creates a new test case +func NewTestCase() *TestCase { + id := getTestID() + return &TestCase{ + ID: id, + Source: fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Test User" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "name", "type": "string", "length": 80, "comment": "User Name", "index": true }, + { "name": "status", "type": "enum", "option": ["active", "disabled"], "default": "active", "comment": "Status", "index": true } + ], + "tags": ["test_%s"], + "label": "Test Label", + "description": "Test Description", + "option": { "timestamps": true, "soft_deletes": true } + }`, id, id, id), + UpdatedSource: fmt.Sprintf(`{ + "name": "%s", + "table": { "name": "%s", "comment": "Updated Test User" }, + "columns": [ + { "name": "id", "type": "ID" }, + { "name": "name", "type": "string", "length": 80, "comment": "User Name", "index": true }, + { "name": "status", "type": "enum", "option": ["active", "disabled", "pending"], "default": "active", "comment": "Status", "index": true } + ], + "tags": ["test_%s", "updated"], + "label": "Updated Label", + "description": "Updated Description", + "option": { "timestamps": true, "soft_deletes": true } + }`, id, id, id), + Tags: []string{fmt.Sprintf("test_%s", id)}, + Label: "Test Label", + Description: "Test Description", + } +} + +// CreateOptions returns creation options +func (tc *TestCase) CreateOptions() *types.CreateOptions { + return &types.CreateOptions{ + ID: tc.ID, + Source: tc.Source, + } +} + +// UpdateOptions returns update options +func (tc *TestCase) UpdateOptions() *types.UpdateOptions { + return &types.UpdateOptions{ + ID: tc.ID, + Source: tc.UpdatedSource, + } +} + +// UpdateInfoOptions returns update info options +func (tc *TestCase) UpdateInfoOptions() *types.UpdateOptions { + return &types.UpdateOptions{ + ID: tc.ID, + Info: &types.Info{ + Label: "Updated via Info", + Tags: []string{"tag1", "info"}, + Description: "Updated via info field", + }, + } +} + +// ListOptions returns list options +func (tc *TestCase) ListOptions(withSource bool) *types.ListOptions { + return &types.ListOptions{ + Source: withSource, + Tags: tc.Tags, + } +} + +// AssertInfo verifies if the information is correct +func (tc *TestCase) AssertInfo(info *types.Info) bool { + if info == nil { + return false + } + return info.ID == tc.ID && + info.Label == tc.Label && + len(info.Tags) == len(tc.Tags) && + info.Description == tc.Description +} + +// AssertUpdatedInfo verifies if the updated information is correct +func (tc *TestCase) AssertUpdatedInfo(info *types.Info) bool { + if info == nil { + return false + } + return info.ID == tc.ID && + info.Label == "Updated Label" && + len(info.Tags) == 2 && + info.Description == "Updated Description" +} + +// AssertUpdatedInfoViaInfo verifies if the information updated via Info is correct +func (tc *TestCase) AssertUpdatedInfoViaInfo(info *types.Info) bool { + if info == nil { + return false + } + return info.ID == tc.ID && + info.Label == "Updated via Info" && + len(info.Tags) == 2 && + info.Description == "Updated via info field" +} diff --git a/dsl/model/model.go b/dsl/model/model.go index 6b3d0144..73f6a28b 100644 --- a/dsl/model/model.go +++ b/dsl/model/model.go @@ -5,6 +5,7 @@ import ( "fmt" "github.com/yaoapp/gou/model" + "github.com/yaoapp/kun/exception" "github.com/yaoapp/yao/dsl/types" ) @@ -65,10 +66,44 @@ func (m *YaoModel) Load(ctx context.Context, options *types.LoadOptions) error { reset = v.(bool) } - path := types.ToPath(types.TypeModel, options.ID) - mod, err := model.LoadSync(path, options.ID) - if err != nil { - return err + var mod *model.Model + var err error + + // Case 1: If Source is provided, use LoadSource + if options.Source != "" { + mod, err = model.LoadSourceSync([]byte(options.Source), options.ID, "") + if err != nil { + return err + } + } else if options.Path != "" && options.Store == "fs" { + // Case 2: If Path is provided and Store is fs, use LoadSync with Path + mod, err = model.LoadSync(options.Path, options.ID) + if err != nil { + return err + } + } else if options.Store == "db" { + // Case 3: If Store is db, get Source from DB first + if m.db == nil { + return fmt.Errorf("db io is required for store type db") + } + source, exists, err := m.db.Source(options.ID) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("model %s not found in database", options.ID) + } + mod, err = model.LoadSourceSync([]byte(source), options.ID, "") + if err != nil { + return err + } + } else { + // Case 4: Default case, use LoadSync with ID + path := types.ToPath(types.TypeModel, options.ID) + mod, err = model.LoadSync(path, options.ID) + if err != nil { + return err + } } if migration || reset { @@ -99,7 +134,28 @@ func (m *YaoModel) Unload(ctx context.Context, options *types.UnloadOptions) err dropTable = v.(bool) } - mod := model.Select(options.ID) + // Try to get model, handle panic + var mod *model.Model + var err error + func() { + defer func() { + if r := recover(); r != nil { + if ex, ok := r.(exception.Exception); ok { + if ex.Message == fmt.Sprintf("Model:%s; not found", options.ID) { + err = fmt.Errorf("model %s not found", options.ID) + return + } + } + panic(r) + } + }() + mod = model.Select(options.ID) + }() + + if err != nil { + return err + } + if mod == nil { return fmt.Errorf("model %s not found", options.ID) } @@ -137,16 +193,50 @@ func (m *YaoModel) Reload(ctx context.Context, options *types.ReloadOptions) err reset = v.(bool) } - // Reload the model - path := types.ToPath(types.TypeModel, options.ID) - mod, err := model.LoadSync(path, options.ID) - if err != nil { - return err + var mod *model.Model + var err error + + // Case 1: If Source is provided, use LoadSource + if options.Source != "" { + mod, err = model.LoadSourceSync([]byte(options.Source), options.ID, "") + if err != nil { + return err + } + } else if options.Path != "" && options.Store == "fs" { + // Case 2: If Path is provided and Store is fs, use LoadSync with Path + mod, err = model.LoadSync(options.Path, options.ID) + if err != nil { + return err + } + } else if options.Store == "db" { + // Case 3: If Store is db, get Source from DB first + if m.db == nil { + return fmt.Errorf("db io is required for store type db") + } + source, exists, err := m.db.Source(options.ID) + if err != nil { + return err + } + if !exists { + return fmt.Errorf("model %s not found in database", options.ID) + } + mod, err = model.LoadSourceSync([]byte(source), options.ID, "") + if err != nil { + return err + } + } else { + // Case 4: Default case, use LoadSync with ID + path := types.ToPath(types.TypeModel, options.ID) + mod, err = model.LoadSync(path, options.ID) + if err != nil { + return err + } } if migrate || reset { return mod.Migrate(reset, model.WithDonotInsertValues(true)) } + return nil } diff --git a/dsl/model/model_test.go b/dsl/model/model_test.go new file mode 100644 index 00000000..3d6faa99 --- /dev/null +++ b/dsl/model/model_test.go @@ -0,0 +1,326 @@ +package model + +import ( + "context" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/yaoapp/yao/dsl/io" + "github.com/yaoapp/yao/dsl/types" +) + +func TestModelLoad(t *testing.T) { + testCase := NewTestCase() + fsio := io.NewFS(types.TypeModel) + dbio := io.NewDB(types.TypeModel) + manager := New("", fsio, dbio) + + // Test Load with nil options + err := manager.Load(context.Background(), nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "load options is required") + + // Test Load with empty ID + err = manager.Load(context.Background(), &types.LoadOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "load options id is required") + + // Test Load with Source + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID, + Source: testCase.Source, + }) + assert.NoError(t, err) + + // Test Load from filesystem + err = fsio.Create(&types.CreateOptions{ + ID: testCase.ID + "_fs", + Source: testCase.Source, + }) + assert.NoError(t, err) + + path := types.ToPath(types.TypeModel, testCase.ID+"_fs") + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + Path: path, + Store: "fs", + }) + assert.NoError(t, err) + + // Test Load from database + err = dbio.Create(&types.CreateOptions{ + ID: testCase.ID + "_db", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_db", + Store: "db", + }) + assert.NoError(t, err) + + // Test Load with default path (should use filesystem) + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + }) + assert.NoError(t, err) + + // Test Load with migration + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + Options: map[string]interface{}{"migration": true}, + }) + assert.NoError(t, err) + + // Test Load with reset + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + Options: map[string]interface{}{"reset": true}, + }) + assert.NoError(t, err) + + // Clean up + err = fsio.Delete(testCase.ID + "_fs") + assert.NoError(t, err) + err = dbio.Delete(testCase.ID + "_db") + assert.NoError(t, err) + err = cleanTestData() + assert.NoError(t, err) +} + +func TestModelLoadWithDB(t *testing.T) { + testCase := NewTestCase() + dbio := io.NewDB(types.TypeModel) + manager := New("", nil, dbio) + + // Create model in DB first + err := dbio.Create(&types.CreateOptions{ + ID: testCase.ID, + Source: testCase.Source, + }) + assert.NoError(t, err) + + // Test Load with Store=db + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID, + Store: "db", + }) + assert.NoError(t, err) + + // Test Load non-existent model from DB + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: "non-existent", + Store: "db", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "not found in database") + + // Clean up + err = dbio.Delete(testCase.ID) + assert.NoError(t, err) + err = cleanTestData() + assert.NoError(t, err) +} + +func TestModelUnload(t *testing.T) { + testCase := NewTestCase() + fsio := io.NewFS(types.TypeModel) + dbio := io.NewDB(types.TypeModel) + manager := New("", fsio, dbio) + + // Test Unload with nil options + err := manager.Unload(context.Background(), nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unload options is required") + + // Test Unload with empty ID + err = manager.Unload(context.Background(), &types.UnloadOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "unload options id is required") + + // Test Unload non-existent model + err = manager.Unload(context.Background(), &types.UnloadOptions{ + ID: "non-existent", + }) + assert.Error(t, err) + assert.Contains(t, err.Error(), "model non-existent not found") + + // Test Unload from filesystem + err = fsio.Create(&types.CreateOptions{ + ID: testCase.ID + "_fs", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + Store: "fs", + }) + assert.NoError(t, err) + + err = manager.Unload(context.Background(), &types.UnloadOptions{ + ID: testCase.ID + "_fs", + Options: map[string]interface{}{"dropTable": true}, + }) + assert.NoError(t, err) + + // Test Unload from database + err = dbio.Create(&types.CreateOptions{ + ID: testCase.ID + "_db", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_db", + Store: "db", + }) + assert.NoError(t, err) + + err = manager.Unload(context.Background(), &types.UnloadOptions{ + ID: testCase.ID + "_db", + Options: map[string]interface{}{"dropTable": true}, + }) + assert.NoError(t, err) + + // Clean up + err = fsio.Delete(testCase.ID + "_fs") + assert.NoError(t, err) + err = dbio.Delete(testCase.ID + "_db") + assert.NoError(t, err) + err = cleanTestData() + assert.NoError(t, err) +} + +func TestModelReload(t *testing.T) { + testCase := NewTestCase() + fsio := io.NewFS(types.TypeModel) + dbio := io.NewDB(types.TypeModel) + manager := New("", fsio, dbio) + + // Test Reload with nil options + err := manager.Reload(context.Background(), nil) + assert.Error(t, err) + assert.Contains(t, err.Error(), "reload options is required") + + // Test Reload with empty ID + err = manager.Reload(context.Background(), &types.ReloadOptions{}) + assert.Error(t, err) + assert.Contains(t, err.Error(), "reload options id is required") + + // Test Reload from filesystem + err = fsio.Create(&types.CreateOptions{ + ID: testCase.ID + "_fs", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + Store: "fs", + }) + assert.NoError(t, err) + + err = manager.Reload(context.Background(), &types.ReloadOptions{ + ID: testCase.ID + "_fs", + Store: "fs", + Options: map[string]interface{}{"migrate": true}, + }) + assert.NoError(t, err) + + // Test Reload from database + err = dbio.Create(&types.CreateOptions{ + ID: testCase.ID + "_db", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_db", + Store: "db", + }) + assert.NoError(t, err) + + err = manager.Reload(context.Background(), &types.ReloadOptions{ + ID: testCase.ID + "_db", + Store: "db", + Options: map[string]interface{}{"migrate": true}, + }) + assert.NoError(t, err) + + // Clean up + err = fsio.Delete(testCase.ID + "_fs") + assert.NoError(t, err) + err = dbio.Delete(testCase.ID + "_db") + assert.NoError(t, err) + err = cleanTestData() + assert.NoError(t, err) +} + +func TestModelLoaded(t *testing.T) { + testCase := NewTestCase() + fsio := io.NewFS(types.TypeModel) + dbio := io.NewDB(types.TypeModel) + manager := New("", fsio, dbio) + + // Test Load from filesystem + err := fsio.Create(&types.CreateOptions{ + ID: testCase.ID + "_fs", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_fs", + Store: "fs", + }) + assert.NoError(t, err) + + // Test Load from database + err = dbio.Create(&types.CreateOptions{ + ID: testCase.ID + "_db", + Source: testCase.Source, + }) + assert.NoError(t, err) + + err = manager.Load(context.Background(), &types.LoadOptions{ + ID: testCase.ID + "_db", + Store: "db", + }) + assert.NoError(t, err) + + // Test Loaded + infos, err := manager.Loaded(context.Background()) + assert.NoError(t, err) + assert.NotNil(t, infos) + assert.Contains(t, infos, testCase.ID+"_fs") + assert.Contains(t, infos, testCase.ID+"_db") + + // Clean up + err = fsio.Delete(testCase.ID + "_fs") + assert.NoError(t, err) + err = dbio.Delete(testCase.ID + "_db") + assert.NoError(t, err) + err = cleanTestData() + assert.NoError(t, err) +} + +func TestModelValidate(t *testing.T) { + manager := New("", nil, nil) + + // Test Validate + valid, messages := manager.Validate(context.Background(), "test source") + assert.True(t, valid) + assert.Empty(t, messages) +} + +func TestModelExecute(t *testing.T) { + manager := New("", nil, nil) + + // Test Execute + result, err := manager.Execute(context.Background(), "test_id", "test_method") + assert.Error(t, err) + assert.Contains(t, err.Error(), "Not implemented") + assert.Nil(t, result) +} diff --git a/dsl/types/types.go b/dsl/types/types.go index ef1b5fcf..27483763 100644 --- a/dsl/types/types.go +++ b/dsl/types/types.go @@ -145,7 +145,6 @@ type LoadOptions struct { type UnloadOptions struct { ID string Path string - Source string Store StoreType Options map[string]interface{} }