diff --git a/runner/embed.go b/runner/embed.go index 8cb47c7..44c509b 100644 --- a/runner/embed.go +++ b/runner/embed.go @@ -55,6 +55,9 @@ var renderLuaCode string //go:embed lua/session.lua var sessionLuaCode string +//go:embed lua/timestamp.lua +var timestampLuaCode string + // Module represents a Lua module to load type Module struct { name string @@ -78,6 +81,7 @@ var modules = []Module{ {"time", timeLuaCode, false}, {"math", mathLuaCode, false}, {"env", envLuaCode, true}, + {"timestamp", timestampLuaCode, false}, } // loadModule loads a single module into the Lua state diff --git a/runner/lua/cookie.lua b/runner/lua/cookie.lua index f9c9483..e3e4603 100644 --- a/runner/lua/cookie.lua +++ b/runner/lua/cookie.lua @@ -18,7 +18,7 @@ function cookie_set(name, value, options) end function cookie_get(name) - return __ctx._request_cookies and __ctx._request_cookies[name] + return __ctx.cookies and __ctx.cookies[name] end function cookie_delete(name, path, domain) diff --git a/runner/lua/timestamp.lua b/runner/lua/timestamp.lua new file mode 100644 index 0000000..f62b9ec --- /dev/null +++ b/runner/lua/timestamp.lua @@ -0,0 +1,93 @@ +-- timestamp.lua +local timestamp = {} + +-- Standard format presets using Lua format codes +local FORMATS = { + iso = "%Y-%m-%dT%H:%M:%SZ", + datetime = "%Y-%m-%d %H:%M:%S", + us_date = "%m/%d/%Y", + us_datetime = "%m/%d/%Y %I:%M:%S %p", + date = "%Y-%m-%d", + time = "%H:%M:%S", + time12 = "%I:%M:%S %p", + readable = "%B %d, %Y %I:%M:%S %p", + compact = "%Y%m%d_%H%M%S" +} + +-- Parse input to unix timestamp and microseconds +local function parse_input(input) + local unix_time, micros = 0, 0 + + if type(input) == "string" then + local frac, secs = input:match("^(0%.%d+)%s+(%d+)$") + if frac and secs then + unix_time = tonumber(secs) + micros = math.floor((tonumber(frac) * 1000000) + 0.5) + else + unix_time = tonumber(input) or 0 + end + elseif type(input) == "number" then + unix_time = math.floor(input) + micros = math.floor(((input - unix_time) * 1000000) + 0.5) + end + + return unix_time, micros +end + +-- Remove leading zeros from number string +local function no_leading_zero(s) + return s:gsub("^0+", "") or "0" +end + +-- Main format function +function timestamp.format(input, fmt) + fmt = fmt or "datetime" + local format_str = FORMATS[fmt] or fmt + local unix_time, micros = parse_input(input) + local result = os.date(format_str, unix_time) + + -- Handle microseconds if format contains dot + if format_str:find("%.") then + result = result .. string.format(".%06d", micros) + end + + return result +end + +-- US date/time with no leading zeros +function timestamp.us_datetime_no_zero(input) + local unix_time, micros = parse_input(input) + local month = no_leading_zero(os.date("%m", unix_time)) + local day = no_leading_zero(os.date("%d", unix_time)) + local year = os.date("%Y", unix_time) + local hour = no_leading_zero(os.date("%I", unix_time)) + local min = os.date("%M", unix_time) + local sec = os.date("%S", unix_time) + local ampm = os.date("%p", unix_time) + + return string.format("%s/%s/%s %s:%s:%s %s", month, day, year, hour, min, sec, ampm) +end + +-- Quick preset functions +function timestamp.iso(input) return timestamp.format(input, "iso") end +function timestamp.datetime(input) return timestamp.format(input, "datetime") end +function timestamp.us_date(input) return timestamp.format(input, "us_date") end +function timestamp.us_datetime(input) return timestamp.us_datetime_no_zero(input) end +function timestamp.date(input) return timestamp.format(input, "date") end +function timestamp.time(input) return timestamp.format(input, "time") end +function timestamp.time12(input) return timestamp.format(input, "time12") end +function timestamp.readable(input) return timestamp.format(input, "readable") end + +-- Microsecond precision variants +function timestamp.datetime_micro(input) + return timestamp.format(input, "%Y-%m-%d %H:%M:%S.") .. string.format("%06d", select(2, parse_input(input))) +end + +function timestamp.iso_micro(input) + return timestamp.format(input, "%Y-%m-%dT%H:%M:%S.") .. string.format("%06dZ", select(2, parse_input(input))) +end + +-- Register global convenience function +_G.format_time = timestamp.format + +return timestamp diff --git a/runner/lualibs/sqlite.go b/runner/lualibs/sqlite.go index 9de43d1..00c80f0 100644 --- a/runner/lualibs/sqlite.go +++ b/runner/lualibs/sqlite.go @@ -244,6 +244,28 @@ func setupParams(state *luajit.State, paramIndex int, execOpts *sqlitex.ExecOpti return fmt.Errorf("invalid parameters: %w", err) } + // Handle direct array types + if arrParams, ok := paramsAny.([]any); ok { + execOpts.Args = arrParams + return nil + } + if strArr, ok := paramsAny.([]string); ok { + args := make([]any, len(strArr)) + for i, v := range strArr { + args[i] = v + } + execOpts.Args = args + return nil + } + if floatArr, ok := paramsAny.([]float64); ok { + args := make([]any, len(floatArr)) + for i, v := range floatArr { + args[i] = v + } + execOpts.Args = args + return nil + } + params, ok := paramsAny.(map[string]any) if !ok { return fmt.Errorf("unsupported parameter type: %T", paramsAny) diff --git a/runner/runner.go b/runner/runner.go index 2f9f77d..87999c4 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -140,6 +140,13 @@ func (r *Runner) buildHTTPContext(ctx *fasthttp.RequestCtx, params *router.Param }) luaCtx.Set("headers", headers) + // Cookies + cookies := r.ctxPool.Get().(map[string]any) + ctx.Request.Header.VisitAllCookie(func(key, value []byte) { + cookies[string(key)] = string(value) + }) + luaCtx.Set("cookies", cookies) + // Route parameters if params != nil && len(params.Keys) > 0 { paramMap := r.paramsPool.Get().(map[string]any) @@ -187,8 +194,8 @@ func (r *Runner) buildHTTPContext(ctx *fasthttp.RequestCtx, params *router.Param return luaCtx } +// Releases the HTTP context's maps back to their pool func (r *Runner) releaseHTTPContext(luaCtx *Context) { - // Return pooled maps if headers, ok := luaCtx.Get("headers").(map[string]any); ok { for k := range headers { delete(headers, k) @@ -196,6 +203,13 @@ func (r *Runner) releaseHTTPContext(luaCtx *Context) { r.ctxPool.Put(headers) } + if cookies, ok := luaCtx.Get("cookies").(map[string]any); ok { + for k := range cookies { + delete(cookies, k) + } + r.ctxPool.Put(cookies) + } + if params, ok := luaCtx.Get("params").(map[string]any); ok && len(params) > 0 { for k := range params { delete(params, k)