diff --git a/internal/app/app.go b/internal/app/app.go index 486889d..b83822e 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -72,8 +72,8 @@ func NewApp(sourceFS *util.SourceFs, workFS *util.WorkFs, logger *utils.Logger, AppEntry: appEntry, systemConfig: systemConfig, starlarkCache: map[string]*starlarkCacheEntry{}, - plugins: NewAppPlugins(plugins, appEntry.Metadata.Accounts), } + newApp.plugins = NewAppPlugins(newApp, plugins, appEntry.Metadata.Accounts) if appEntry.IsDev { newApp.appDev = dev.NewAppDev(logger, &util.WritableSourceFs{SourceFs: sourceFS}, workFS, systemConfig) diff --git a/internal/app/app_plugins.go b/internal/app/app_plugins.go index 3e66fac..5668f61 100644 --- a/internal/app/app_plugins.go +++ b/internal/app/app_plugins.go @@ -14,18 +14,21 @@ import ( type AppPlugins struct { sync.Mutex - plugins map[string]any + plugins map[string]any + + app *App pluginConfig map[string]utils.PluginSettings // pluginName -> accountName -> PluginSettings accountMap map[string]string // pluginName -> accountName } -func NewAppPlugins(pluginConfig map[string]utils.PluginSettings, appAccounts []utils.AccountLink) *AppPlugins { +func NewAppPlugins(app *App, pluginConfig map[string]utils.PluginSettings, appAccounts []utils.AccountLink) *AppPlugins { accountMap := make(map[string]string) for _, entry := range appAccounts { accountMap[entry.Plugin] = entry.AccountName } return &AppPlugins{ + app: app, plugins: make(map[string]any), pluginConfig: pluginConfig, accountMap: accountMap, @@ -63,7 +66,12 @@ func (p *AppPlugins) GetPlugin(pluginInfo *PluginInfo, accountName string) (any, appConfig = p.pluginConfig[pluginAccount] } - pluginContext := &PluginContext{Config: appConfig} + pluginContext := &PluginContext{ + Logger: p.app.Logger, + AppId: p.app.AppEntry.Id, + StoreInfo: p.app.storeInfo, + Config: appConfig, + } plugin, err := pluginInfo.builder(pluginContext) if err != nil { return nil, fmt.Errorf("error creating plugin %s: %w", pluginInfo.funcName, err) diff --git a/internal/app/app_plugins_test.go b/internal/app/app_plugins_test.go index 6aa8045..85ea292 100644 --- a/internal/app/app_plugins_test.go +++ b/internal/app/app_plugins_test.go @@ -25,7 +25,12 @@ func TestGetPlugin(t *testing.T) { {Plugin: "plugin2.in", AccountName: "account2"}, {Plugin: "plugin2.in#account2", AccountName: "plugin2.in#account3"}, } - appPlugins := NewAppPlugins(pluginConfig, appAccounts) + + app := &App{ + Logger: utils.NewLogger(&utils.LogConfig{}), + AppEntry: &utils.AppEntry{Id: "testApp", Path: "/test", Domain: "", SourceUrl: ".", IsDev: false}, + } + appPlugins := NewAppPlugins(app, pluginConfig, appAccounts) // Define the pluginInfo and accountName for testing pluginInfo := &PluginInfo{ diff --git a/internal/app/audit.go b/internal/app/audit.go index ce93e57..fd2c1e9 100644 --- a/internal/app/audit.go +++ b/internal/app/audit.go @@ -60,6 +60,11 @@ func (a *App) Audit() (*utils.ApproveResult, error) { Load: auditLoader, } + err = a.loadSchemaInfo(a.sourceFS) + if err != nil { + return nil, err + } + builtin, err := a.createBuiltin() if err != nil { return nil, err diff --git a/internal/app/plugin.go b/internal/app/plugin.go index 8ede3a4..50411f6 100644 --- a/internal/app/plugin.go +++ b/internal/app/plugin.go @@ -20,7 +20,10 @@ import ( ) type PluginContext struct { - Config utils.PluginSettings + Logger *utils.Logger + AppId utils.AppId + StoreInfo *utils.StoreInfo + Config utils.PluginSettings } type NewPluginFunc func(pluginContext *PluginContext) (any, error) diff --git a/internal/app/setup.go b/internal/app/setup.go index e79e619..830f48c 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -136,6 +136,7 @@ func (a *App) addSchemaTypes(builtin starlark.StringDict) (starlark.StringDict, newBuiltins[k] = v } + // Add type module for referencing type names typeDict := starlark.StringDict{} for _, t := range a.storeInfo.Types { tb := utils.TypeBuilder{Name: t.Name, Fields: t.Fields} @@ -146,8 +147,21 @@ func (a *App) addSchemaTypes(builtin starlark.StringDict) (starlark.StringDict, Name: util.TYPE_MODULE, Members: typeDict, } - newBuiltins[util.TYPE_MODULE] = &typeModule + + // Add table module for referencing table names + tableDict := starlark.StringDict{} + for _, t := range a.storeInfo.Types { + tableDict[t.Name] = starlark.String(t.Name) + } + + tableModule := starlarkstruct.Module{ + Name: util.TABLE_MODULE, + Members: tableDict, + } + newBuiltins[util.TABLE_MODULE] = &tableModule + a.Trace().Msgf("********Added %d types to builtins", len(a.storeInfo.Types)) + return newBuiltins, nil } diff --git a/internal/app/store/db_plugin.go b/internal/app/store/db_plugin.go deleted file mode 100644 index a009881..0000000 --- a/internal/app/store/db_plugin.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) ClaceIO, LLC -// SPDX-License-Identifier: Apache-2.0 - -package store - -import ( - "net/http" - - "github.com/claceio/clace/internal/app" - "go.starlark.net/starlark" -) - -func init() { - h := &dbPlugin{} - pluginFuncs := []app.PluginFunc{ - app.CreatePluginApi(h.Create, false), - } - app.RegisterPlugin("db", NewDBPlugin, pluginFuncs) -} - -type dbPlugin struct { - client *http.Client -} - -func NewDBPlugin(pluginContext *app.PluginContext) (any, error) { - return &dbPlugin{}, nil -} - -func (h *dbPlugin) Create(thread *starlark.Thread, builtin *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { - return nil, nil -} diff --git a/internal/app/store/sql_store.go b/internal/app/store/sql_store.go new file mode 100644 index 0000000..698c7e4 --- /dev/null +++ b/internal/app/store/sql_store.go @@ -0,0 +1,209 @@ +// Copyright (c) ClaceIO, LLC +// SPDX-License-Identifier: Apache-2.0 + +package store + +import ( + "database/sql" + "encoding/json" + "fmt" + "os" + "strings" + "sync" + + "github.com/claceio/clace/internal/app" + "github.com/claceio/clace/internal/utils" +) + +const ( + DB_CONNECTION_CONFIG = "db_connection" +) + +type SqlStore struct { + *utils.Logger + sync.Mutex + isInitialized bool + pluginContext *app.PluginContext + db *sql.DB + prefix string + isSqlite bool // false means postgres, no other options +} + +var _ Store = (*SqlStore)(nil) + +func NewSqlStore(pluginContext *app.PluginContext) (*SqlStore, error) { + return &SqlStore{ + Logger: pluginContext.Logger, + pluginContext: pluginContext, + }, nil +} + +func validateTableName(name string) error { + // TODO: validate table name + return nil +} + +func (s *SqlStore) genTableName(collection string) (string, error) { + err := validateTableName(collection) + if err != nil { + return "", err + } + return fmt.Sprintf("'%s_%s'", s.prefix, collection), nil +} + +func (s *SqlStore) initialize() error { + s.Lock() + defer s.Unlock() + + if s.isInitialized { + // Already initialized + return nil + } + + if err := s.initStore(); err != nil { + return err + } + s.isInitialized = true + return nil +} + +func checkConnectString(connStr string) (string, error) { + parts := strings.SplitN(connStr, ":", 2) + if len(parts) != 2 { + return "", fmt.Errorf("invalid connection string: %s", connStr) + } + if !strings.HasPrefix(parts[0], "sqlite") { // only sqlite for now + return "", fmt.Errorf("invalid connection string: %s, only sqlite supported", connStr) + } + return os.ExpandEnv(parts[1]), nil +} + +func (s *SqlStore) initStore() error { + if s.pluginContext.StoreInfo == nil { + return fmt.Errorf("store info not found") + } + + connectStringConfig, ok := s.pluginContext.Config[DB_CONNECTION_CONFIG] + if !ok { + return fmt.Errorf("db connection string not found in config") + } + connectString, ok := connectStringConfig.(string) + if !ok { + return fmt.Errorf("db connection string is not a string") + } + + var err error + connectString, err = checkConnectString(connectString) + if err != nil { + return err + } + + s.db, err = sql.Open("sqlite", connectString) + if err != nil { + return fmt.Errorf("error opening db %s: %w", connectString, err) + } + s.isSqlite = true + s.prefix = "db_" + string(s.pluginContext.AppId)[len(utils.ID_PREFIX_APP_PROD):] + + for _, storeType := range s.pluginContext.StoreInfo.Types { + collection, err := s.genTableName(storeType.Name) + if err != nil { + return err + } + + createStmt := "CREATE TABLE IF NOT EXISTS " + collection + " (id INTEGER PRIMARY KEY AUTOINCREMENT, version INTEGER, created_by TEXT, updated_by TEXT, created_at INTEGER, updated_at INTEGER, data JSON)" + _, err = s.db.Exec(createStmt) + if err != nil { + return fmt.Errorf("error creating table %s: %w", collection, err) + } + s.Info().Msgf("Created table %s", collection) + } + + return nil +} + +// Insert a new entry in the store +func (s *SqlStore) Insert(collection string, entry *Entry) (EntryId, error) { + if err := s.initialize(); err != nil { + return -1, err + } + + var err error + collection, err = s.genTableName(collection) + if err != nil { + return -1, err + } + + dataJson, err := json.Marshal(entry.Data) + if err != nil { + return -1, fmt.Errorf("error marshalling data for collection %s: %w", collection, err) + } + + createStmt := "INSERT INTO " + collection + " (version, created_by, updated_by, created_at, updated_at, data) VALUES (?, ?, ?, ?, ?, ?)" + result, err := s.db.Exec(createStmt, entry.Version, entry.CreatedBy, entry.UpdatedBy, entry.CreatedAt, entry.UpdatedAt, dataJson) + if err != nil { + return -1, nil + } + + insertId, err := result.LastInsertId() + if err != nil { + return -1, nil + } + return EntryId(insertId), nil +} + +// SelectById returns a single item from the store +func (s *SqlStore) SelectById(collection string, key EntryId) (*Entry, error) { + if err := s.initialize(); err != nil { + return nil, err + } + + var err error + collection, err = s.genTableName(collection) + if err != nil { + return nil, err + } + + query := "SELECT id, version, created_by, updated_by, created_at, updated_at, data FROM " + collection + " WHERE id = ?" + row := s.db.QueryRow(query, key) + + var dataStr string + entry := &Entry{} + + err = row.Scan(&entry.Id, &entry.Version, &entry.CreatedBy, &entry.UpdatedBy, &entry.CreatedAt, &entry.UpdatedAt, dataStr) + if err != nil { + if err == sql.ErrNoRows { + return nil, fmt.Errorf("entry %d not found in collection %s", key, collection) + } + return nil, err + } + + if dataStr != "" { + if err := json.Unmarshal([]byte(dataStr), &entry.Data); err != nil { + return nil, err + } + } + + return entry, nil +} + +// Select returns the entries matching the filter +func (s *SqlStore) Select(collection string, filter map[string]any, sort []string, offset, limit int64) (EntryIterator, error) { + return nil, nil + +} + +// Update an existing entry in the store +func (s *SqlStore) Update(collection string, Entry *Entry) error { + return nil +} + +// DeleteById an entry from the store by id +func (s *SqlStore) DeleteById(collection string, key EntryId) error { + return nil +} + +// Delete entries from the store matching the filter +func (s *SqlStore) Delete(collection string, filter map[string]any) error { + return nil +} diff --git a/internal/app/store/sqlite_store.go b/internal/app/store/sqlite_store.go deleted file mode 100644 index 2c1b53f..0000000 --- a/internal/app/store/sqlite_store.go +++ /dev/null @@ -1,119 +0,0 @@ -// Copyright (c) ClaceIO, LLC -// SPDX-License-Identifier: Apache-2.0 - -package store - -import ( - "database/sql" - "encoding/json" - "fmt" - - "github.com/claceio/clace/internal/utils" -) - -type SqliteStore struct { - *utils.Logger - config *utils.ServerConfig - db *sql.DB - prefix string -} - -func NewSqliteStore(logger *utils.Logger, config *utils.ServerConfig, db *sql.DB, prefix string) (*SqliteStore, error) { - return &SqliteStore{ - Logger: logger, - config: config, - db: db, - prefix: prefix, - }, nil -} - -func validateCollectionName(name string) error { - // TODO: validate collection name - return nil -} - -func (s *SqliteStore) genTableName(collection string) (string, error) { - err := validateCollectionName(collection) - if err != nil { - return "", err - } - return fmt.Sprintf("'%s_%s'", s.prefix, collection), nil -} - -// Create a new entry in the store -func (s *SqliteStore) Create(collection string, entry *Entry) (EntryId, error) { - var err error - collection, err = s.genTableName(collection) - if err != nil { - return -1, err - } - - dataJson, err := json.Marshal(entry.Data) - if err != nil { - return -1, fmt.Errorf("error marshalling data for collection %s: %w", collection, err) - } - - createStmt := "INSERT INTO " + collection + " (version, created_by, updated_by, created_at, updated_at, data) VALUES (?, ?, ?, ?, ?, ?)" - result, err := s.db.Exec(createStmt, entry.Version, entry.CreatedBy, entry.UpdatedBy, entry.CreatedAt, entry.UpdatedAt, dataJson) - if err != nil { - return -1, nil - } - - insertId, err := result.LastInsertId() - if err != nil { - return -1, nil - } - return EntryId(insertId), nil -} - -// GetByKey returns a single item from the store -func (s *SqliteStore) GetByKey(collection string, key EntryId) (*Entry, error) { - var err error - collection, err = s.genTableName(collection) - if err != nil { - return nil, err - } - - query := "SELECT id, version, created_by, updated_by, created_at, updated_at, data FROM " + collection + " WHERE id = ?" - row := s.db.QueryRow(query, key) - - var dataStr string - entry := &Entry{} - - err = row.Scan(&entry.Id, &entry.Version, &entry.CreatedBy, &entry.UpdatedBy, &entry.CreatedAt, &entry.UpdatedAt, dataStr) - if err != nil { - if err == sql.ErrNoRows { - return nil, fmt.Errorf("entry %d not found in collection %s", key, collection) - } - return nil, err - } - - if dataStr != "" { - if err := json.Unmarshal([]byte(dataStr), &entry.Data); err != nil { - return nil, err - } - } - - return entry, nil -} - -// Get returns the entries matching the filter -func (s *SqliteStore) Get(collection string, filter map[string]any, sort map[string]int) (EntryIterator, error) { - return nil, nil - -} - -// Update an existing entry in the store -func (s *SqliteStore) Update(collection string, Entry *Entry) error { - return nil -} - -// Delete an entry from the store by key -func (s *SqliteStore) DeleteByKey(collection string, key string) error { - return nil -} - -// Delete entries from the store matching the filter -func (s *SqliteStore) Delete(collection string, filter map[string]any) error { - return nil -} diff --git a/internal/app/store/store.go b/internal/app/store/store.go index 8215a9d..4924533 100644 --- a/internal/app/store/store.go +++ b/internal/app/store/store.go @@ -3,7 +3,14 @@ package store -import "time" +import ( + "fmt" + "time" + + "github.com/claceio/clace/internal/app/util" + "github.com/claceio/clace/internal/utils" + "go.starlark.net/starlark" +) type EntryId int64 type UserId string @@ -11,7 +18,7 @@ type Document map[string]any type Entry struct { Id EntryId - Version int + Version int64 CreatedBy UserId UpdatedBy UserId CreatedAt time.Time @@ -19,6 +26,70 @@ type Entry struct { Data Document } +var _ starlark.Unpacker = (*Entry)(nil) + +func (e *Entry) Unpack(value starlark.Value) error { + v, ok := value.(starlark.HasAttrs) + if !ok { + return fmt.Errorf("expected entry, got %s", value.Type()) + } + var err error + + entryData := make(map[string]any) + for _, attr := range v.AttrNames() { + switch attr { + case "_id": + id, err := util.GetIntAttr(v, attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + e.Id = EntryId(id) + case "_version": + e.Version, err = util.GetIntAttr(v, attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + case "_created_by": + createdBy, err := util.GetStringAttr(v, attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + e.CreatedBy = UserId(createdBy) + case "_updated_by": + updatedBy, err := util.GetStringAttr(v, attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + e.UpdatedBy = UserId(updatedBy) + case "_created_at": + createdAt, err := util.GetIntAttr(v, attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + e.CreatedAt = time.Unix(createdAt, 0) + case "_updated_at": + updatedAt, err := util.GetIntAttr(v, attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + e.UpdatedAt = time.Unix(updatedAt, 0) + default: + dataVal, err := v.Attr(attr) + if err != nil { + return fmt.Errorf("error reading %s: %w", attr, err) + } + data, err := utils.UnmarshalStarlark(dataVal) + if err != nil { + return fmt.Errorf("error unmarshalling %s : %w", attr, err) + } + entryData[attr] = data + } + } + + e.Data = entryData + return nil +} + type EntryIterator interface { HasMore() bool Fetch() *Entry @@ -26,21 +97,42 @@ type EntryIterator interface { // Store is the interface for a Clace document store. These API are exposed by the db plugin type Store interface { - // Create a new entry in the store - Create(collection string, Entry *Entry) (EntryId, error) + // Insert a new entry in the store + Insert(table string, Entry *Entry) (EntryId, error) - // GetByKey returns a single item from the store - GetByKey(collection string, key EntryId) (*Entry, error) + // SelectById returns a single item from the store + SelectById(table string, id EntryId) (*Entry, error) - // Get returns the entries matching the filter - Get(collection string, filter map[string]any, sort map[string]int, offset, limit int64) (EntryIterator, error) + // Select returns the entries matching the filter + Select(table string, filter map[string]any, sort []string, offset, limit int64) (EntryIterator, error) // Update an existing entry in the store - Update(collection string, Entry *Entry) error + Update(table string, Entry *Entry) error - // Delete an entry from the store by key - DeleteByKey(collection string, key EntryId) error + // DeleteById an entry from the store by id + DeleteById(table string, id EntryId) error // Delete entries from the store matching the filter - Delete(collection string, filter map[string]any) error + Delete(table string, filter map[string]any) error +} + +func CreateType(name string, entry *Entry) (*utils.StarlarkType, error) { + data := make(map[string]starlark.Value) + + data["_id"] = starlark.MakeInt(int(entry.Id)) + data["_version"] = starlark.MakeInt(int(entry.Version)) + data["_created_by"] = starlark.String(string(entry.CreatedBy)) + data["_updated_by"] = starlark.String(string(entry.UpdatedBy)) + data["_created_at"] = starlark.MakeInt(int(entry.CreatedAt.Unix())) + data["_updated_at"] = starlark.MakeInt(int(entry.UpdatedAt.Unix())) + + var err error + for k, v := range entry.Data { + data[k], err = utils.MarshalStarlark(v) + if err != nil { + return nil, err + } + } + + return utils.NewStarlarkType(name, data), nil } diff --git a/internal/app/store/store_plugin.go b/internal/app/store/store_plugin.go new file mode 100644 index 0000000..24808f5 --- /dev/null +++ b/internal/app/store/store_plugin.go @@ -0,0 +1,74 @@ +// Copyright (c) ClaceIO, LLC +// SPDX-License-Identifier: Apache-2.0 + +package store + +import ( + "fmt" + + "github.com/claceio/clace/internal/app" + "github.com/claceio/clace/internal/utils" + "go.starlark.net/starlark" +) + +func init() { + h := &storePlugin{} + pluginFuncs := []app.PluginFunc{ + app.CreatePluginApi(h.Insert, false), + app.CreatePluginApiName(h.SelectById, false, "select_by_id"), + } + app.RegisterPlugin("store", NewStorePlugin, pluginFuncs) +} + +type storePlugin struct { + sqlStore *SqlStore +} + +func NewStorePlugin(pluginContext *app.PluginContext) (any, error) { + sqlStore, err := NewSqlStore(pluginContext) + + return &storePlugin{ + sqlStore: sqlStore, + }, err +} + +func (s *storePlugin) Insert(thread *starlark.Thread, builtin *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var table string + var entry Entry + + if err := starlark.UnpackArgs("insert", args, kwargs, "table", &table, "entry", &entry); err != nil { + return nil, err + } + + id, err := s.sqlStore.Insert(table, &entry) + if err != nil { + return utils.NewErrorResponse(err), nil + } + return utils.NewResponse(id), nil +} + +func (s *storePlugin) SelectById(thread *starlark.Thread, builtin *starlark.Builtin, args starlark.Tuple, kwargs []starlark.Tuple) (starlark.Value, error) { + var table string + var id starlark.Int + + if err := starlark.UnpackArgs("select_by_id", args, kwargs, "table", &table, "id", &id); err != nil { + return nil, err + } + + var idVal int64 + var ok bool + if idVal, ok = id.Int64(); !ok || idVal < 0 { + return utils.NewErrorResponse(fmt.Errorf("invalid id value")), nil + } + + entry, err := s.sqlStore.SelectById(table, EntryId(idVal)) + if err != nil { + return utils.NewErrorResponse(err), nil + } + + returnType, err := CreateType(table, entry) + if err != nil { + return utils.NewErrorResponse(err), nil + } + return utils.NewResponse(returnType), nil +} diff --git a/internal/app/tests/basic_test.go b/internal/app/tests/basic_test.go index 7259d2a..b8e145e 100644 --- a/internal/app/tests/basic_test.go +++ b/internal/app/tests/basic_test.go @@ -465,5 +465,11 @@ def handler(req): a.ServeHTTP(response, request) testutil.AssertEqualsInt(t, "code", 200, response.Code) - testutil.AssertStringContains(t, response.Body.String(), "Template. ABC map[abool:true adict:map[a:3 b:2] aint:2 alist:[1 4 3] astring:abc2") + testutil.AssertStringContains(t, response.Body.String(), "Template. ABC map[") + testutil.AssertStringContains(t, response.Body.String(), "abool:true") + testutil.AssertStringContains(t, response.Body.String(), "adict:map[a:3 b:2]") + testutil.AssertStringContains(t, response.Body.String(), "aint:2") + testutil.AssertStringContains(t, response.Body.String(), "alist:[1 4 3]") + testutil.AssertStringContains(t, response.Body.String(), "astring:abc2") + testutil.AssertStringContains(t, response.Body.String(), "_created_at:0 _created_by: _id:0 _updated_at:") } diff --git a/internal/app/util/builtins.go b/internal/app/util/builtins.go index 10a3de4..81fe605 100644 --- a/internal/app/util/builtins.go +++ b/internal/app/util/builtins.go @@ -16,6 +16,7 @@ import ( const ( DEFAULT_MODULE = "ace" TYPE_MODULE = "type" + TABLE_MODULE = "table" APP = "app" PAGE = "page" FRAGMENT = "fragment" diff --git a/internal/app/util/disk_fs.go b/internal/app/util/disk_fs.go index 68ee83c..863aa52 100644 --- a/internal/app/util/disk_fs.go +++ b/internal/app/util/disk_fs.go @@ -42,6 +42,9 @@ func (d *DiskReadFS) Open(name string) (fs.File, error) { func (d *DiskReadFS) ReadFile(name string) ([]byte, error) { if dir, ok := d.fs.(fs.ReadFileFS); ok { + if name[0] == '/' { + name = name[1:] + } return dir.ReadFile(name) } diff --git a/internal/app/util/source_fs.go b/internal/app/util/source_fs.go index 31edb75..b778c99 100644 --- a/internal/app/util/source_fs.go +++ b/internal/app/util/source_fs.go @@ -144,7 +144,7 @@ func (f *SourceFs) HashName(name string) string { // Read file contents. Return original filename if we receive an error. buf, err := fs.ReadFile(f.ReadableFS, name) if err != nil { - fmt.Printf("HashName readfile error: %s\n", err) //TODO: log + fmt.Printf("HashName readfile error: %s %s\n", err, name) //TODO: log return name } diff --git a/internal/app/util/starlark.go b/internal/app/util/starlark_attr.go similarity index 72% rename from internal/app/util/starlark.go rename to internal/app/util/starlark_attr.go index 90157bf..52b3dd8 100644 --- a/internal/app/util/starlark.go +++ b/internal/app/util/starlark_attr.go @@ -7,10 +7,9 @@ import ( "fmt" "go.starlark.net/starlark" - "go.starlark.net/starlarkstruct" ) -func GetStringAttr(s *starlarkstruct.Struct, key string) (string, error) { +func GetStringAttr(s starlark.HasAttrs, key string) (string, error) { v, err := s.Attr(key) if err != nil { return "", fmt.Errorf("error getting %s: %s", key, err) @@ -23,7 +22,7 @@ func GetStringAttr(s *starlarkstruct.Struct, key string) (string, error) { return vs.GoString(), nil } -func GetIntAttr(s *starlarkstruct.Struct, key string) (int64, error) { +func GetIntAttr(s starlark.HasAttrs, key string) (int64, error) { v, err := s.Attr(key) if err != nil { return 0, fmt.Errorf("error getting %s: %s", key, err) @@ -40,7 +39,7 @@ func GetIntAttr(s *starlarkstruct.Struct, key string) (int64, error) { return intVal, nil } -func GetBoolAttr(s *starlarkstruct.Struct, key string) (bool, error) { +func GetBoolAttr(s starlark.HasAttrs, key string) (bool, error) { v, err := s.Attr(key) if err != nil { return false, fmt.Errorf("error getting %s: %s", key, err) @@ -53,7 +52,7 @@ func GetBoolAttr(s *starlarkstruct.Struct, key string) (bool, error) { return bool(vb), nil } -func GetListStringAttr(s *starlarkstruct.Struct, key string, optional bool) ([]string, error) { +func GetListStringAttr(s starlark.HasAttrs, key string, optional bool) ([]string, error) { v, err := s.Attr(key) if err != nil { if optional { @@ -83,3 +82,17 @@ func GetListStringAttr(s *starlarkstruct.Struct, key string, optional bool) ([]s return ret, nil } + +func GetMapAttr(s starlark.HasAttrs, key string) (bool, error) { + v, err := s.Attr(key) + if err != nil { + return false, fmt.Errorf("error getting %s: %s", key, err) + } + + var vb starlark.Bool + var ok bool + if vb, ok = v.(starlark.Bool); !ok { + return false, fmt.Errorf("%s is not a bool", key) + } + return bool(vb), nil +} diff --git a/internal/app/util/store_info.go b/internal/app/util/store_info.go deleted file mode 100644 index ccdcbd9..0000000 --- a/internal/app/util/store_info.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright (c) ClaceIO, LLC -// SPDX-License-Identifier: Apache-2.0 - -package util - -import "github.com/claceio/clace/internal/utils" - -func ReadStoreInfo(fileName string, inp []byte) (*utils.StoreInfo, error) { - storeInfo, err := LoadStoreInfo(fileName, inp) - if err != nil { - return nil, err - } - - if err := validateStoreInfo(storeInfo); err != nil { - return nil, err - } - - return storeInfo, nil -} - -func validateStoreInfo(storeInfo *utils.StoreInfo) error { - if err := validateTypes(storeInfo.Types); err != nil { - return err - } - - // TODO: validate collections - return nil -} - -func validateTypes(types []utils.StoreType) error { - // TODO: validate types - return nil -} diff --git a/internal/app/util/loader.go b/internal/app/util/type_loader.go similarity index 92% rename from internal/app/util/loader.go rename to internal/app/util/type_loader.go index abd4588..6ecbc59 100644 --- a/internal/app/util/loader.go +++ b/internal/app/util/type_loader.go @@ -17,6 +17,33 @@ const ( INDEX = "index" ) +func ReadStoreInfo(fileName string, inp []byte) (*utils.StoreInfo, error) { + storeInfo, err := LoadStoreInfo(fileName, inp) + if err != nil { + return nil, err + } + + if err := validateStoreInfo(storeInfo); err != nil { + return nil, err + } + + return storeInfo, nil +} + +func validateStoreInfo(storeInfo *utils.StoreInfo) error { + if err := validateTypes(storeInfo.Types); err != nil { + return err + } + + // TODO: validate collections + return nil +} + +func validateTypes(types []utils.StoreType) error { + // TODO: validate types + return nil +} + func LoadStoreInfo(fileName string, data []byte) (*utils.StoreInfo, error) { definedTypes := make(map[string]starlark.Value) diff --git a/internal/server/server.go b/internal/server/server.go index bdc498e..c3c7b95 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -22,7 +22,8 @@ import ( "github.com/go-chi/chi/middleware" "golang.org/x/crypto/bcrypt" - _ "github.com/claceio/clace/plugins" // Register builtin plugins + _ "github.com/claceio/clace/internal/app/store" // Register db plugin + _ "github.com/claceio/clace/plugins" // Register builtin plugins ) const ( diff --git a/internal/utils/clace.default.toml b/internal/utils/clace.default.toml index ae87e9e..ec2e1f7 100644 --- a/internal/utils/clace.default.toml +++ b/internal/utils/clace.default.toml @@ -45,5 +45,5 @@ tailwindcss_command = "npx tailwindcss" file_watcher_debounce_millis = 300 node_path = "" # node module lookup paths https://esbuild.github.io/api/#node-paths -[plugin."db.in"] +[plugin."store.in"] db_connection = "sqlite:$CL_HOME/clace_app.db?_journal_mode=WAL" diff --git a/internal/utils/plugin_response.go b/internal/utils/plugin_response.go new file mode 100644 index 0000000..85ecb9d --- /dev/null +++ b/internal/utils/plugin_response.go @@ -0,0 +1,109 @@ +// Copyright (c) ClaceIO, LLC +// SPDX-License-Identifier: Apache-2.0 + +package utils + +import ( + "fmt" + + "go.starlark.net/starlark" +) + +// PluginResponse is a starlark.Value that represents the response to a plugin request +type PluginResponse struct { + errorCode int + err error + data any +} + +func NewErrorResponse(err error) *PluginResponse { + return &PluginResponse{ + errorCode: 1, + err: err, + data: nil, + } +} + +func NewErrorCodeResponse(errorCode int, err error, data any) *PluginResponse { + return &PluginResponse{ + errorCode: errorCode, + err: err, + data: data, + } +} + +func NewResponse(data any) *PluginResponse { + return &PluginResponse{ + data: data, + } +} + +func (r *PluginResponse) Attr(name string) (starlark.Value, error) { + switch name { + case "ErrorCode": + return starlark.MakeInt(r.errorCode), nil + case "Error": + if r.err == nil { + return starlark.None, nil + } + return starlark.String(r.err.Error()), nil + case "Data": + if r.data == nil { + return starlark.None, nil + } + + if _, ok := r.data.(starlark.Value); ok { + return r.data.(starlark.Value), nil + } + return MarshalStarlark(r.data) + + default: + return starlark.None, fmt.Errorf("response has no attribute '%s'", name) + } +} + +func (r *PluginResponse) AttrNames() []string { + return []string{"ErrorCode", "Error", "Data"} +} + +func (r *PluginResponse) String() string { + return fmt.Sprintf("%d:%s:%s", r.errorCode, r.err, r.data) +} + +func (r *PluginResponse) Type() string { + return "Response" +} + +func (r *PluginResponse) Freeze() { +} + +func (r *PluginResponse) Truth() starlark.Bool { + return r.errorCode != 0 +} + +func (r *PluginResponse) Hash() (uint32, error) { + var err error + var errValue starlark.Value + errValue, err = r.Attr("Error") + if err != nil { + return 0, err + } + + var dataValue starlark.Value + dataValue, err = r.Attr("Data") + if err != nil { + return 0, err + } + return starlark.Tuple{starlark.MakeInt(r.errorCode), errValue, dataValue}.Hash() +} + +func (r *PluginResponse) UnmarshalStarlarkType() (any, error) { + return map[string]any{ + "ErrorCode": r.errorCode, + "Error": r.err, + "Data": r.data, + }, nil +} + +var _ starlark.Value = (*PluginResponse)(nil) +var _ TypeUnmarshaler = (*PluginResponse)(nil) diff --git a/internal/utils/store_types.go b/internal/utils/store_type.go similarity index 74% rename from internal/utils/store_types.go rename to internal/utils/store_type.go index 19114a6..1f2473d 100644 --- a/internal/utils/store_types.go +++ b/internal/utils/store_type.go @@ -12,12 +12,13 @@ import ( type TypeName string const ( - INT TypeName = "INT" - STRING TypeName = "STRING" - BOOLEAN TypeName = "BOOLEAN" - LIST TypeName = "LIST" - DICT TypeName = "DICT" - //DATETIME TypeName = "datetime" + INT TypeName = "INT" + FLOAT TypeName = "FLOAT" + DATETIME TypeName = "DATETIME" + STRING TypeName = "STRING" + BOOLEAN TypeName = "BOOLEAN" + LIST TypeName = "LIST" + DICT TypeName = "DICT" ) type StoreInfo struct { @@ -58,13 +59,18 @@ func (s *TypeBuilder) CreateType(thread *starlark.Thread, _ *starlark.Builtin, a case INT: var v starlark.Int value = v + case FLOAT: + var v starlark.Float + value = v + case DATETIME: + var v starlark.Int + value = v case STRING: var v starlark.String value = v case BOOLEAN: var v starlark.Bool value = v - // TODO: add support for datetime case LIST: var v *starlark.List value = v @@ -95,5 +101,12 @@ func (s *TypeBuilder) CreateType(thread *starlark.Thread, _ *starlark.Builtin, a valueMap[argName] = *val } + valueMap["_id"] = starlark.MakeInt(0) + valueMap["_version"] = starlark.MakeInt(0) + valueMap["_created_by"] = starlark.String("") + valueMap["_updated_by"] = starlark.String("") + valueMap["_created_at"] = starlark.MakeInt(0) + valueMap["_updated_at"] = starlark.MakeInt(0) + return NewStarlarkType(s.Name, valueMap), nil }