diff --git a/frontend/js/components/api-reference.js b/frontend/js/components/api-reference.js index 773ea6b..5aaae7c 100644 --- a/frontend/js/components/api-reference.js +++ b/frontend/js/components/api-reference.js @@ -341,6 +341,21 @@ export function getLuaAPISections() { name: "kv.delete(key)", type: "function", description: t("luaApi.io.items.kvDelete"), + }, + { + name: "kv.getGlobal(key)", + type: "function", + description: t("luaApi.io.items.kvGetGlobal"), + }, + { + name: "kv.setGlobal(key, value)", + type: "function", + description: t("luaApi.io.items.kvSetGlobal"), + }, + { + name: "kv.deleteGlobal(key)", + type: "function", + description: t("luaApi.io.items.kvDeleteGlobal"), }, ], }, diff --git a/frontend/js/components/code-editor.js b/frontend/js/components/code-editor.js index 0f95072..079973b 100644 --- a/frontend/js/components/code-editor.js +++ b/frontend/js/components/code-editor.js @@ -117,6 +117,21 @@ const API_DOCS = { snippet: 'kv.delete("${1:key}")', description: "Delete a key from the store", }, + "kv.getGlobal": { + signature: "kv.getGlobal(key: string): string | nil", + snippet: 'kv.getGlobal("${1:key}")', + description: "Get a value from the global key-value store. Returns nil if key does not exist.", + }, + "kv.setGlobal": { + signature: "kv.setGlobal(key: string, value: string)", + snippet: 'kv.setGlobal("${1:key}", "${2:value}")', + description: "Set a key-value pair in the global store", + }, + "kv.deleteGlobal": { + signature: "kv.deleteGlobal(key: string)", + snippet: 'kv.deleteGlobal("${1:key}")', + description: "Delete a key from the global store", + }, "env.get": { signature: "env.get(key: string): string | nil", snippet: 'env.get("${1:key}")', diff --git a/frontend/js/i18n/locales/en.js b/frontend/js/i18n/locales/en.js index 69816b2..deb3c7a 100644 --- a/frontend/js/i18n/locales/en.js +++ b/frontend/js/i18n/locales/en.js @@ -506,6 +506,9 @@ export default { kvGet: "Get value from store", kvSet: "Set key-value pair", kvDelete: "Delete key from store", + kvGetGlobal: "Get value from global store", + kvSetGlobal: "Set key-value pair in global store", + kvDeleteGlobal: "Delete key from global store", envGet: "Get environment variable", httpGet: "GET request", httpPost: "POST request", diff --git a/frontend/llms.txt b/frontend/llms.txt index ac5c373..0fe7bcf 100644 --- a/frontend/llms.txt +++ b/frontend/llms.txt @@ -74,11 +74,16 @@ Persistent storage scoped to function ID: - kv.get(key: string): string | nil - Retrieve value, returns nil if not found - kv.set(key: string, value: string): boolean - Set key-value pair, returns success - kv.delete(key: string): boolean - Delete key, returns success - +- kv.getGlobal(key: string): error - Retrieve value from global store, returns nil if not found +- kv.setGlobal(key: string, value: string): boolean - Set key-value pair, returns success +- kv.deleteGlobal(key: string): boolean - Delete key from global store, returns success Example: ```lua -local count = kv.get("counter") or "0" +local count = kv.get("counter") or 0 kv.set("counter", tostring(tonumber(count) + 1)) + +local sharedCount = kv.getGlobal("sharedCounter") or 0 +kv.setGlobal("sharedCounter", sharedGlobal + 1) ``` ### Environment Variables (env) diff --git a/internal/runner/lua_kv.go b/internal/runner/lua_kv.go index 34af00f..936d549 100644 --- a/internal/runner/lua_kv.go +++ b/internal/runner/lua_kv.go @@ -46,5 +46,42 @@ func registerKV(L *lua.LState, kvStore kv.Store, functionID string) { return 1 })) + // kv.getGlobal(key) + L.SetField(kvTable, "getGlobal", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + value, err := kvStore.GetGlobal(key) + if err != nil { + L.Push(lua.LNil) + return 1 + } + L.Push(lua.LString(value)) + return 1 + })) + + // kv.setGlobal(key, value) + L.SetField(kvTable, "setGlobal", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + value := L.CheckString(2) + err := kvStore.SetGlobal(key, value) + if err != nil { + L.Push(lua.LFalse) + return 1 + } + L.Push(lua.LTrue) + return 1 + })) + + // kv.deleteGlobal(key) + L.SetField(kvTable, "deleteGlobal", L.NewFunction(func(L *lua.LState) int { + key := L.CheckString(1) + err := kvStore.DeleteGlobal(key) + if err != nil { + L.Push(lua.LFalse) + return 1 + } + L.Push(lua.LTrue) + return 1 + })) + L.SetGlobal("kv", kvTable) } diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index d50029b..3816863 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -7,8 +7,8 @@ import ( "testing" "time" - "github.com/dimiro1/lunar/internal/services/env" "github.com/dimiro1/lunar/internal/events" + "github.com/dimiro1/lunar/internal/services/env" internalhttp "github.com/dimiro1/lunar/internal/services/http" "github.com/dimiro1/lunar/internal/services/kv" "github.com/dimiro1/lunar/internal/services/logger" @@ -285,6 +285,30 @@ end if val != "value1" { t.Errorf("expected value 'value1', got %q", val) } + + // Test with named store + luaCodeNamed := ` +function handler(ctx, event) + kv.set("key1", "value1") -- This should go to the default store + kv.setGlobal("key1", "value2") -- This should go to the global store + local val = kv.get("key1") -- Should retrieve from default store + local globalVal = kv.getGlobal("key1") -- Should retrieve from global store + + return { + statusCode = 200, + body = "Retrieved: " .. val .. ", " .. globalVal + } +end +` + + resp, err = Run(context.Background(), deps, Request{Context: execCtx, Event: event, Code: luaCodeNamed}) + if err != nil { + t.Fatalf("Run failed: %v", err) + } + + if resp.HTTP.Body != "Retrieved: value1, value2" { + t.Errorf("expected body 'Retrieved: value1, value2', got %q", resp.HTTP.Body) + } } func TestRun_Env(t *testing.T) { diff --git a/internal/services/kv/kv.go b/internal/services/kv/kv.go index 716ba93..a04978b 100644 --- a/internal/services/kv/kv.go +++ b/internal/services/kv/kv.go @@ -21,6 +21,9 @@ type Store interface { Get(functionID, key string) (string, error) Set(functionID, key, value string) error Delete(functionID, key string) error + GetGlobal(key string) (string, error) + SetGlobal(key, value string) error + DeleteGlobal(key string) error } // MemoryStore is an in-memory implementation of Store @@ -35,7 +38,6 @@ func NewMemoryStore() *MemoryStore { } } -// Get retrieves a value by functionID and key func (m *MemoryStore) Get(functionID, key string) (string, error) { ns, exists := m.data[functionID] if !exists { @@ -66,6 +68,33 @@ func (m *MemoryStore) Delete(functionID, key string) error { return nil } +// GetGlobal retrieves a value from the global key-value store +func (m *MemoryStore) GetGlobal(key string) (string, error) { + if key == "" { + return "", &Error{Message: "key cannot be empty"} + } + + return m.Get("", key) +} + +// SetGlobal sets a value in the global key-value store +func (m *MemoryStore) SetGlobal(key, value string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return m.Set("", key, value) +} + +// DeleteGlobal removes a key-value pair from the global key-value store +func (m *MemoryStore) DeleteGlobal(key string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return m.Delete("", key) +} + // SQLiteStore is a SQLite-backed implementation of Store type SQLiteStore struct { db *sql.DB @@ -117,3 +146,30 @@ func (s *SQLiteStore) Delete(functionID, key string) error { } return nil } + +// GetGlobal retrieves a value from the global key-value store +func (s *SQLiteStore) GetGlobal(key string) (string, error) { + if key == "" { + return "", &Error{Message: "key cannot be empty"} + } + + return s.Get("", key) +} + +// SetGlobal sets a value in the global key-value store +func (s *SQLiteStore) SetGlobal(key, value string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return s.Set("", key, value) +} + +// DeleteGlobal removes a key-value pair from the global key-value store +func (s *SQLiteStore) DeleteGlobal(key string) error { + if key == "" { + return &Error{Message: "key cannot be empty"} + } + + return s.Delete("", key) +} diff --git a/internal/services/kv/kv_test.go b/internal/services/kv/kv_test.go index e20422d..147231f 100644 --- a/internal/services/kv/kv_test.go +++ b/internal/services/kv/kv_test.go @@ -134,6 +134,39 @@ func TestSQLiteStore_DeleteNonExistent(t *testing.T) { } } +func TestSQLiteStore_GetAndSetWithGlobalStore(t *testing.T) { + db := setupTestDB(t) + store := NewSQLiteStore(db) + + // Set a value in the global store. + err := store.SetGlobal("key1", "value1") + if err != nil { + t.Fatalf("Failed to set value in global store: %v", err) + } + + // Get the value from the global store. + value, err := store.GetGlobal("key1") + if err != nil { + t.Fatalf("Failed to get value from global store: %v", err) + } + + if value != "value1" { + t.Errorf("Expected value 'value1' from global store, got '%s'", value) + } + + // Delete a key from the global store + err = store.DeleteGlobal("key1") + if err != nil { + t.Errorf("Expected no error for deleting key from global store, got %v", err) + } + + // Delete a non-existent key from the global store + err = store.DeleteGlobal("key1") + if err != nil { + t.Errorf("Expected no error for deleting key from global store, got %v", err) + } +} + func TestSQLiteStore_FunctionIsolation(t *testing.T) { db := setupTestDB(t) store := NewSQLiteStore(db)