diff --git a/dsl/dsl.go b/dsl/dsl.go index 4377e070..68458209 100644 --- a/dsl/dsl.go +++ b/dsl/dsl.go @@ -77,13 +77,14 @@ func (dsl *DSL) Inspect(ctx context.Context, id string) (*types.Info, error) { } if !exists { - return nil, fmt.Errorf("dsl not found, %s", id) + return nil, fmt.Errorf("%s not found, %s", dsl.Type, id) } } // Merge the status from the manager loaded, err := dsl.manager.Loaded(ctx) if err != nil { + fmt.Printf("DEBUG: manager.Loaded failed: %v\n", err) return info, err } @@ -186,14 +187,22 @@ func (dsl *DSL) Create(ctx context.Context, options *types.CreateOptions) error return fmt.Errorf("create options is required") } + // Set default store type if not specified + if options.Store == "" { + options.Store = types.StoreTypeFile + } + + // Validate store type + if options.Store != types.StoreTypeDB && options.Store != types.StoreTypeFile { + return fmt.Errorf("invalid store type: %s", options.Store) + } + if options.Store == types.StoreTypeDB { err := dsl.db.Create(options) if err != nil { return err } - } - - if options.Store == types.StoreTypeFile { + } else if options.Store == types.StoreTypeFile { err := dsl.fs.Create(options) if err != nil { return err @@ -252,7 +261,16 @@ func (dsl *DSL) Update(ctx context.Context, options *types.UpdateOptions) error return err } if !exists { - return fmt.Errorf("%s DSL not found, %s", dsl.Type, options.ID) + return fmt.Errorf("%s not found, %s", dsl.Type, options.ID) + } + // Fix: If store is empty but found in fs, it should be File store + if info.Store == "" { + info.Store = types.StoreTypeFile + } + } else { + // Fix: If store is empty but found in db, it should be DB store + if info.Store == "" { + info.Store = types.StoreTypeDB } } @@ -309,7 +327,17 @@ func (dsl *DSL) Delete(ctx context.Context, options *types.DeleteOptions) error return err } if !exists { - return fmt.Errorf("%s DSL not found, %s", dsl.Type, options.ID) + return fmt.Errorf("%s not found, %s", dsl.Type, options.ID) + } else { + // Fix: If store is empty but found in fs, it should be File store + if info.Store == "" { + info.Store = types.StoreTypeFile + } + } + } else { + // Fix: If store is empty but found in db, it should be DB store + if info.Store == "" { + info.Store = types.StoreTypeDB } } diff --git a/dsl/dsl_test.go b/dsl/dsl_test.go new file mode 100644 index 00000000..d2021909 --- /dev/null +++ b/dsl/dsl_test.go @@ -0,0 +1,784 @@ +package dsl + +import ( + "context" + "fmt" + "os" + "testing" + "time" + + jsoniter "github.com/json-iterator/go" + "github.com/stretchr/testify/assert" + "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) + } + + // Load application + root := os.Getenv("YAO_TEST_APPLICATION") + if root == "" { + log.Error("YAO_TEST_APPLICATION environment variable is not set") + os.Exit(1) + } + app, err := application.OpenFromDisk(root) // Load app + if err != nil { + log.Error("Load application error: %s", err.Error()) + os.Exit(1) + } + application.Load(app) + + // 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, 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 unified test case for all DSL types +type TestCase struct { + ID string + Source string + UpdatedSource string + Tags []string + Label string + Description string + DSLType types.Type +} + +// NewModelTestCase creates a new model test case +func NewModelTestCase() *TestCase { + id := getTestID() + return &TestCase{ + ID: id, + DSLType: types.TypeModel, + 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 Model", + "description": "Test Model 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 Model", + "description": "Updated Model Description", + "option": { "timestamps": true, "soft_deletes": true } +}`, id, id, id), + Tags: []string{fmt.Sprintf("test_%s", id)}, + Label: "Test Model", + Description: "Test Model Description", + } +} + +// NewConnectorTestCase creates a new connector test case +func NewConnectorTestCase() *TestCase { + id := getTestID() + return &TestCase{ + ID: id, + DSLType: types.TypeConnector, + Source: fmt.Sprintf(`{ + "label": "Test Connector", + "description": "Test Connector Description", + "tags": ["test_%s"], + "type": "openai", + "options": { + "proxy": "https://api.openai.com/v1", + "model": "gpt-4o-mini", + "key": "sk-test-key" + } +}`, id), + UpdatedSource: fmt.Sprintf(`{ + "label": "Updated Connector", + "description": "Updated Connector Description", + "tags": ["test_%s", "updated"], + "type": "openai", + "options": { + "proxy": "https://api.openai.com/v1", + "model": "gpt-4o-mini", + "key": "sk-test-key" + } +}`, id), + Tags: []string{fmt.Sprintf("test_%s", id)}, + Label: "Test Connector", + Description: "Test Connector Description", + } +} + +// NewMCPTestCase creates a new MCP test case +func NewMCPTestCase() *TestCase { + id := getTestID() + return &TestCase{ + ID: id, + DSLType: types.TypeMCPClient, + Source: fmt.Sprintf(`{ + "name": "Test MCP Client %s", + "label": "Test MCP Client", + "description": "Test MCP Client Description", + "tags": ["test_%s"], + "transport": "stdio", + "command": "echo", + "arguments": ["hello", "world"], + "env": { + "MCP_TEST": "true" + }, + "enable_sampling": true, + "enable_roots": false, + "timeout": "30s" +}`, id, id), + UpdatedSource: fmt.Sprintf(`{ + "name": "Updated MCP Client %s", + "label": "Updated MCP Client", + "description": "Updated MCP Client Description", + "tags": ["test_%s", "updated"], + "transport": "stdio", + "command": "echo", + "arguments": ["hello", "updated"], + "env": { + "MCP_TEST": "true", + "MCP_UPDATED": "true" + }, + "enable_sampling": false, + "enable_roots": true, + "timeout": "60s" +}`, id, id), + Tags: []string{fmt.Sprintf("test_%s", id)}, + Label: "Test MCP Client", + Description: "Test MCP Client Description", + } +} + +// CreateOptions returns creation options +func (tc *TestCase) CreateOptions(store types.StoreType) *types.CreateOptions { + return &types.CreateOptions{ + ID: tc.ID, + Source: tc.Source, + Store: store, + } +} + +// UpdateOptions returns update options +func (tc *TestCase) UpdateOptions() *types.UpdateOptions { + return &types.UpdateOptions{ + ID: tc.ID, + Source: tc.UpdatedSource, + } +} + +// DeleteOptions returns delete options +func (tc *TestCase) DeleteOptions() *types.DeleteOptions { + return &types.DeleteOptions{ + ID: tc.ID, + } +} + +// LoadOptions returns load options +func (tc *TestCase) LoadOptions(store types.StoreType) *types.LoadOptions { + return &types.LoadOptions{ + ID: tc.ID, + Source: tc.Source, + Store: store, + } +} + +// UnloadOptions returns unload options +func (tc *TestCase) UnloadOptions(store types.StoreType) *types.UnloadOptions { + return &types.UnloadOptions{ + ID: tc.ID, + Store: store, + } +} + +// ReloadOptions returns reload options +func (tc *TestCase) ReloadOptions(store types.StoreType) *types.ReloadOptions { + return &types.ReloadOptions{ + ID: tc.ID, + Source: tc.UpdatedSource, + Store: store, + } +} + +// ListOptions returns list options +func (tc *TestCase) ListOptions(store types.StoreType) *types.ListOptions { + return &types.ListOptions{ + Tags: tc.Tags, + Store: store, + } +} + +// 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.Type == tc.DSLType && + info.Label == tc.Label && + len(info.Tags) == len(tc.Tags) && + info.Description == tc.Description && + !info.Readonly && + !info.Builtin && + !info.Mtime.IsZero() && + !info.Ctime.IsZero() +} + +// AssertUpdatedInfo verifies if the updated information is correct +func (tc *TestCase) AssertUpdatedInfo(info *types.Info) bool { + if info == nil { + return false + } + expectedLabel := "" + switch tc.DSLType { + case types.TypeModel: + expectedLabel = "Updated Model" + case types.TypeConnector: + expectedLabel = "Updated Connector" + case types.TypeMCPClient: + expectedLabel = "Updated MCP Client" + } + expectedDescription := "" + switch tc.DSLType { + case types.TypeModel: + expectedDescription = "Updated Model Description" + case types.TypeConnector: + expectedDescription = "Updated Connector Description" + case types.TypeMCPClient: + expectedDescription = "Updated MCP Client Description" + } + return info.ID == tc.ID && + info.Type == tc.DSLType && + info.Label == expectedLabel && + len(info.Tags) == 2 && + info.Description == expectedDescription && + !info.Readonly && + !info.Builtin && + !info.Mtime.IsZero() && + !info.Ctime.IsZero() +} + +// Test DSL creation with different types and stores +func TestDSLCreate(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc := tt.tcFunc() + + // Create + err = dsl.Create(ctx, tc.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // Verify exists + exists, err := dsl.Exists(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.True(t, exists) + + // Verify info + info, err := dsl.Inspect(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.True(t, tc.AssertInfo(info)) + + // Cleanup + err = dsl.Delete(ctx, tc.DeleteOptions()) + assert.Nil(t, err) + }) + } + } +} + +// Test DSL inspection +func TestDSLInspect(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc := tt.tcFunc() + + // Create + err = dsl.Create(ctx, tc.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // Inspect + info, err := dsl.Inspect(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.True(t, tc.AssertInfo(info)) + + // Cleanup + err = dsl.Delete(ctx, tc.DeleteOptions()) + assert.Nil(t, err) + }) + } + } +} + +// Test DSL source retrieval +func TestDSLSource(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc := tt.tcFunc() + + // Create + err = dsl.Create(ctx, tc.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // Get source + source, err := dsl.Source(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.Equal(t, tc.Source, source) + + // Cleanup + err = dsl.Delete(ctx, tc.DeleteOptions()) + assert.Nil(t, err) + }) + } + } +} + +// Test DSL listing +func TestDSLList(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc1 := tt.tcFunc() + tc2 := tt.tcFunc() + + // Create test cases + err = dsl.Create(ctx, tc1.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + err = dsl.Create(ctx, tc2.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // List all + list, err := dsl.List(ctx, &types.ListOptions{Store: store}) + if !assert.Nil(t, err) { + return + } + assert.GreaterOrEqual(t, len(list), 2) + + // List with tags + list, err = dsl.List(ctx, tc1.ListOptions(store)) + if !assert.Nil(t, err) { + return + } + assert.GreaterOrEqual(t, len(list), 1) + + // Cleanup + err = dsl.Delete(ctx, tc1.DeleteOptions()) + assert.Nil(t, err) + err = dsl.Delete(ctx, tc2.DeleteOptions()) + assert.Nil(t, err) + }) + } + } +} + +// Test DSL update +func TestDSLUpdate(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc := tt.tcFunc() + + // Create + err = dsl.Create(ctx, tc.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // Update + err = dsl.Update(ctx, tc.UpdateOptions()) + if !assert.Nil(t, err) { + return + } + + // Verify updated info + info, err := dsl.Inspect(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.True(t, tc.AssertUpdatedInfo(info)) + + // Cleanup + err = dsl.Delete(ctx, tc.DeleteOptions()) + assert.Nil(t, err) + }) + } + } +} + +// Test DSL delete +func TestDSLDelete(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc := tt.tcFunc() + + // Create + err = dsl.Create(ctx, tc.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // Delete + err = dsl.Delete(ctx, tc.DeleteOptions()) + if !assert.Nil(t, err) { + return + } + + // Verify deleted + exists, err := dsl.Exists(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.False(t, exists) + }) + } + } +} + +// Test DSL full flow (create, inspect, update, delete) +func TestDSLFlow(t *testing.T) { + ctx := context.Background() + + testCases := []struct { + name string + tcFunc func() *TestCase + dslType types.Type + stores []types.StoreType + }{ + {"Model", NewModelTestCase, types.TypeModel, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"Connector", NewConnectorTestCase, types.TypeConnector, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + {"MCP", NewMCPTestCase, types.TypeMCPClient, []types.StoreType{types.StoreTypeDB, types.StoreTypeFile}}, + } + + for _, tt := range testCases { + for _, store := range tt.stores { + t.Run(fmt.Sprintf("%s_%s", tt.name, store), func(t *testing.T) { + // Clean test data before each test + err := cleanTestData() + if err != nil { + t.Fatalf("Failed to clean test data: %v", err) + } + + dsl, err := New(tt.dslType) + if !assert.Nil(t, err) { + return + } + + tc := tt.tcFunc() + + // Create + err = dsl.Create(ctx, tc.CreateOptions(store)) + if !assert.Nil(t, err) { + return + } + + // Inspect + info, err := dsl.Inspect(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.True(t, tc.AssertInfo(info)) + + // Get source + source, err := dsl.Source(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.Equal(t, tc.Source, source) + + // Update + err = dsl.Update(ctx, tc.UpdateOptions()) + if !assert.Nil(t, err) { + return + } + + // Verify updated + info, err = dsl.Inspect(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.True(t, tc.AssertUpdatedInfo(info)) + + // Delete + err = dsl.Delete(ctx, tc.DeleteOptions()) + if !assert.Nil(t, err) { + return + } + + // Verify deleted + exists, err := dsl.Exists(ctx, tc.ID) + if !assert.Nil(t, err) { + return + } + assert.False(t, exists) + }) + } + } +} diff --git a/dsl/io/db.go b/dsl/io/db.go index 33144dc1..8104b79f 100644 --- a/dsl/io/db.go +++ b/dsl/io/db.go @@ -35,16 +35,11 @@ func fmtRow(row map[string]interface{}) map[string]interface{} { delete(row, "dsl_id") } if readonly, ok := row["readonly"]; ok { - row["readable"] = readonly + row["readonly"] = toBool(readonly) delete(row, "readonly") } - - // Convert boolean values - if row["readable"] != nil { - row["readable"] = toBool(row["readable"]) - } - if row["built_in"] != nil { - row["built_in"] = toBool(row["built_in"]) + if builtin, ok := row["built_in"]; ok { + row["built_in"] = toBool(builtin) } // Convert time values @@ -71,7 +66,10 @@ func (db *DB) Inspect(id string) (*types.Info, bool, error) { // Get the info var info types.Info rows, err := m.Get(model.QueryParam{ - Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}}, + Wheres: []model.QueryWhere{ + {Column: "dsl_id", Value: id}, + {Column: "type", Value: db.Type}, + }, Select: []interface{}{ "dsl_id", "type", @@ -80,7 +78,6 @@ func (db *DB) Inspect(id string) (*types.Info, bool, error) { "sort", "tags", "description", - "status", "store", "mtime", "ctime", @@ -111,6 +108,9 @@ func (db *DB) Inspect(id string) (*types.Info, bool, error) { return nil, false, err } + // Force set Store to DB since this record is from database + info.Store = types.StoreTypeDB + return &info, true, nil } @@ -122,7 +122,10 @@ func (db *DB) Source(id string) (string, bool, error) { // Get the source rows, err := m.Get(model.QueryParam{ - Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}}, + Wheres: []model.QueryWhere{ + {Column: "dsl_id", Value: id}, + {Column: "type", Value: db.Type}, + }, Select: []interface{}{"source"}, Limit: 1, }) @@ -178,7 +181,6 @@ func (db *DB) List(options *types.ListOptions) ([]*types.Info, error) { "sort", "tags", "description", - "status", "store", "mtime", "ctime", @@ -219,6 +221,11 @@ func (db *DB) List(options *types.ListOptions) ([]*types.Info, error) { return nil, err } + // Force set Store to DB since these records are from database + for _, info := range infos { + info.Store = types.StoreTypeDB + } + return infos, nil } @@ -229,26 +236,64 @@ func (db *DB) Create(options *types.CreateOptions) error { return fmt.Errorf("%s %s source is required", db.Type, options.ID) } - // Get info from source - var info types.Info - err := jsoniter.Unmarshal([]byte(options.Source), &info) + // Parse the source to extract metadata + var sourceData map[string]interface{} + err := jsoniter.Unmarshal([]byte(options.Source), &sourceData) if err != nil { return err } + // Extract common fields from source + var label, description string + var tags []string + var sort int + + if v, ok := sourceData["label"]; ok { + if s, ok := v.(string); ok { + label = s + } + } + + if v, ok := sourceData["description"]; ok { + if s, ok := v.(string); ok { + description = s + } + } + + if v, ok := sourceData["tags"]; ok { + if tagsList, ok := v.([]interface{}); ok { + for _, tag := range tagsList { + if s, ok := tag.(string); ok { + tags = append(tags, s) + } + } + } + } + + if v, ok := sourceData["sort"]; ok { + if s, ok := v.(float64); ok { + sort = int(s) + } + } + + // Set default store type if not specified + store := options.Store + if store == "" { + store = types.StoreTypeFile + } + // Get the info m := model.Select("__yao.dsl") data := map[string]interface{}{ "source": options.Source, "dsl_id": options.ID, "type": db.Type, - "label": info.Label, - "path": info.Path, - "sort": info.Sort, - "tags": info.Tags, - "description": info.Description, - "status": info.Status, - "store": info.Store, + "label": label, + "path": types.ToPath(db.Type, options.ID), + "sort": sort, + "tags": tags, + "description": description, + "store": store, "mtime": time.Now(), "ctime": time.Now(), "readonly": 0, @@ -257,14 +302,6 @@ func (db *DB) Create(options *types.CreateOptions) error { "updated_at": time.Now(), } - // Convert boolean values - if info.Readonly { - data["readonly"] = 1 - } - if info.Builtin { - data["built_in"] = 1 - } - _, err = m.Create(data) if err != nil { return err @@ -283,7 +320,10 @@ func (db *DB) Update(options *types.UpdateOptions) error { // Check if the dsl exists rows, err := m.Get(model.QueryParam{ - Wheres: []model.QueryWhere{{Column: "dsl_id", Value: options.ID}}, + Wheres: []model.QueryWhere{ + {Column: "dsl_id", Value: options.ID}, + {Column: "type", Value: db.Type}, + }, Select: []interface{}{"id"}, Limit: 1, }) @@ -300,24 +340,42 @@ func (db *DB) Update(options *types.UpdateOptions) error { "source": options.Source, } if options.Source != "" { - // Parse source to update other fields - var info types.Info - err = jsoniter.Unmarshal([]byte(options.Source), &info) + // Parse source to extract metadata + var sourceData map[string]interface{} + err = jsoniter.Unmarshal([]byte(options.Source), &sourceData) if err != nil { return err } - data["label"] = info.Label - data["description"] = info.Description - data["tags"] = info.Tags - data["sort"] = info.Sort - data["status"] = info.Status - data["store"] = info.Store - if info.Readonly { - data["readonly"] = 1 + // Extract common fields from source + if v, ok := sourceData["label"]; ok { + if s, ok := v.(string); ok { + data["label"] = s + } } - if info.Builtin { - data["built_in"] = 1 + + if v, ok := sourceData["description"]; ok { + if s, ok := v.(string); ok { + data["description"] = s + } + } + + if v, ok := sourceData["tags"]; ok { + if tagsList, ok := v.([]interface{}); ok { + var tags []string + for _, tag := range tagsList { + if s, ok := tag.(string); ok { + tags = append(tags, s) + } + } + data["tags"] = tags + } + } + + if v, ok := sourceData["sort"]; ok { + if s, ok := v.(float64); ok { + data["sort"] = int(s) + } } } else { // Update info @@ -366,7 +424,10 @@ func (db *DB) Delete(id string) error { // Check if the dsl exists rows, err := m.Get(model.QueryParam{ - Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}}, + Wheres: []model.QueryWhere{ + {Column: "dsl_id", Value: id}, + {Column: "type", Value: db.Type}, + }, Select: []interface{}{"id", "dsl_id"}, Limit: 1, }) @@ -391,7 +452,10 @@ func (db *DB) Exists(id string) (bool, error) { // Check if the dsl exists rows, err := m.Get(model.QueryParam{ - Wheres: []model.QueryWhere{{Column: "dsl_id", Value: id}}, + Wheres: []model.QueryWhere{ + {Column: "dsl_id", Value: id}, + {Column: "type", Value: db.Type}, + }, Select: []interface{}{"id", "dsl_id"}, Limit: 1, }) diff --git a/dsl/io/fs.go b/dsl/io/fs.go index 0b566465..76f9b423 100644 --- a/dsl/io/fs.go +++ b/dsl/io/fs.go @@ -21,7 +21,6 @@ func NewFS(typ types.Type) types.IO { // Inspect get the info from the file func (fs *FS) Inspect(id string) (*types.Info, bool, error) { file := types.ToPath(fs.Type, id) - var info types.Info = types.Info{ID: id, Path: file} exists, err := application.App.Exists(file) if err != nil { return nil, false, err @@ -36,21 +35,70 @@ func (fs *FS) Inspect(id string) (*types.Info, bool, error) { return nil, false, err } - // Unmarshal the data to the info - err = application.Parse(file, data, &info) + // Parse the source to extract metadata + var sourceData map[string]interface{} + err = application.Parse(file, data, &sourceData) if err != nil { return nil, true, err } - // Merge the mtime and ctime + // Extract common fields from source + var label, description string + var tags []string + var sort int + + if v, ok := sourceData["label"]; ok { + if s, ok := v.(string); ok { + label = s + } + } + + if v, ok := sourceData["description"]; ok { + if s, ok := v.(string); ok { + description = s + } + } + + if v, ok := sourceData["tags"]; ok { + if tagsList, ok := v.([]interface{}); ok { + for _, tag := range tagsList { + if s, ok := tag.(string); ok { + tags = append(tags, s) + } + } + } + } + + if v, ok := sourceData["sort"]; ok { + if s, ok := v.(float64); ok { + sort = int(s) + } + } + + // Get file info for timestamps fileInfo, err := application.App.Info(file) if err != nil { return nil, true, err } - info.Mtime = fileInfo.ModTime() - info.Ctime = fileInfo.ModTime() - return &info, true, nil + // Create Info structure with correct fields + info := &types.Info{ + ID: id, + Type: fs.Type, + Label: label, + Description: description, + Tags: tags, + Sort: sort, + Path: file, + Store: types.StoreTypeFile, + Readonly: false, + Builtin: false, + Status: types.StatusLoading, + Mtime: fileInfo.ModTime(), + Ctime: fileInfo.ModTime(), + } + + return info, true, nil } // Source get the source from the file