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
15 changes: 15 additions & 0 deletions frontend/js/components/api-reference.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"),
},
],
},
Expand Down
15 changes: 15 additions & 0 deletions frontend/js/components/code-editor.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}")',
Expand Down
3 changes: 3 additions & 0 deletions frontend/js/i18n/locales/en.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
9 changes: 7 additions & 2 deletions frontend/llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
37 changes: 37 additions & 0 deletions internal/runner/lua_kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
26 changes: 25 additions & 1 deletion internal/runner/runner_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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) {
Expand Down
58 changes: 57 additions & 1 deletion internal/services/kv/kv.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
33 changes: 33 additions & 0 deletions internal/services/kv/kv_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading