pkulchenko / fullmoon Goto Github PK
View Code? Open in Web Editor NEWFast and minimalistic Redbean-based Lua web framework in one file.
License: MIT License
Fast and minimalistic Redbean-based Lua web framework in one file.
License: MIT License
Hello,
Would it be possible to put in fullmoon a way to disable root listing (redbean default) without the need to create an index file?
e.g.
When the listing is deactivated
A blank or 404, default route to /
is created if the user has not defined
I was wondering if it would be possible to add custom template engines to Fullmoon?
Trojan:Script/Wacatac.B!ml was detected by windows defender
I see the section for layouts in the readme but can't figure out how it works from the code. Is it not implemented or did I just miss it in there or misunderstand what the placeholder in the readme is about?
In the meantime I found render
and rolled my own but I think a built-in would be handy to lots of apps.
❯ .\redbean-build.com -vvmbag
D2022-06-07T17:39:50.627197:tool/net/redbean.c:5341:redbean-build:8132] (lua) LuaRun("/.init.lua")
E2022-06-07T17:39:50+000145:tool/net/redbean.c:1034:redbean-build:8132] (lua) failed to run lua code: /.init.lua:1: module 'techbench' not found:
no field package.preload['techbench']
no file 'E:\scoop\global\apps\lua\current'
no file 'E:\scoop\global\apps\lua\current'
zip -sf redbean-build.com
Archive contains:
usr/share/ssl/root/
usr/share/ssl/root/amazon.pem
usr/share/ssl/root/certum.pem
usr/share/ssl/root/comodo.pem
usr/share/ssl/root/digicert.pem
usr/share/ssl/root/geotrust.pem
usr/share/ssl/root/globalsign.pem
usr/share/ssl/root/godaddy.pem
usr/share/ssl/root/google.pem
usr/share/ssl/root/isrg.pem
usr/share/ssl/root/quovadis.pem
usr/share/ssl/root/redbean.pem
usr/share/ssl/root/starfield.pem
usr/share/ssl/root/verisign.pem
usr/share/zoneinfo/
usr/share/zoneinfo/Beijing
usr/share/zoneinfo/Berlin
usr/share/zoneinfo/Boulder
usr/share/zoneinfo/Chicago
usr/share/zoneinfo/GST
usr/share/zoneinfo/Honolulu
usr/share/zoneinfo/Israel
usr/share/zoneinfo/Japan
usr/share/zoneinfo/London
usr/share/zoneinfo/Melbourne
usr/share/zoneinfo/New_York
usr/share/zoneinfo/Singapore
.ape
help.txt
.init.lua // ------------------------------------- added
favicon.ico
redbean.png
.lua/
.lua/fullmoon.lua // -------------------------- added
.lua/techbench.lua // ------------------------ added
Total 35 entries (255284 bytes)
unzip -lc .\redbean-build.com .init.lua
Archive: ./redbean-build.com
extracting: .init.lua
require "techbench" // -------------------- .init.lua content
Hi, thank you very much for writing this awesome library!
Would you think it's a good idea to replace
Line 881 in f47bbbb
with
return Route()
This would hand over route back to redbean core if no matching fullmoon route found. I feel redbean file path based routes (e.g. /foo/bar.lua) sometimes is very convenient to be used together with fullmoon.lua.
I've been using a local modified copy without issue. Wondering if it's a good idea to upstream a PR.
Thanks!
The redbean parent site no longer has a redbean-latest.com page.
It just has a download page with a list of releases. Downloading one of those works as expected.
Vince
Hi,
is there any way to save to SQLite database, so that content would not disappear?
This did not work:
-- TechEmpower Benchmark implementation for Fullmoon web framework
-- (https://github.com/TechEmpower/FrameworkBenchmarks/)
-- Copyright 2021-22 Paul Kulchenko
-- data setup
local lsqlite3 = require"lsqlite3"
-- local dbm = lsqlite3.open_memory()
local dbm = lsqlite3.open('redbean.sqlite3')
-- db:busy_timeout(1000)
-- db:exec[[PRAGMA journal_mode=WAL]]
-- db:exec[[PRAGMA synchronous=NORMAL]]
if dbm:exec[[
CREATE TABLE IF NOT EXISTS World (id INTEGER PRIMARY KEY, randomNumber INTEGER NOT NULL default 0);
CREATE TABLE IF NOT EXISTS Fortune (id INTEGER PRIMARY KEY, message TEXT);
CREATE TABLE IF NOT EXISTS CachedWorld (id INTEGER PRIMARY KEY, randomNumber INTEGER NOT NULL default 0);
INSERT INTO Fortune (id, message) VALUES (1, 'fortune: No such file or directory');
INSERT INTO Fortune (id, message) VALUES (2, 'A computer scientist is someone who fixes things that aren''t broken.');
INSERT INTO Fortune (id, message) VALUES (3, 'After enough decimal places, nobody gives a damn.');
INSERT INTO Fortune (id, message) VALUES (4, 'A bad random number generator: 1, 1, 1, 1, 1, 4.33e+67, 1, 1, 1');
INSERT INTO Fortune (id, message) VALUES (5, 'A computer program does what you tell it to do, not what you want it to do.');
INSERT INTO Fortune (id, message) VALUES (6, 'Emacs is a nice operating system, but I prefer UNIX. — Tom Christaensen');
INSERT INTO Fortune (id, message) VALUES (7, 'Any program that runs right is obsolete.');
INSERT INTO Fortune (id, message) VALUES (8, 'A list is only as strong as its weakest link. — Donald Knuth');
INSERT INTO Fortune (id, message) VALUES (9, 'Feature: A bug with seniority.');
INSERT INTO Fortune (id, message) VALUES (10, 'Computers make very fast, very accurate mistakes.');
INSERT INTO Fortune (id, message) VALUES (11, '<script>alert("This should not be displayed in a browser alert box.");</script>');
INSERT INTO Fortune (id, message) VALUES (12, 'フレームワークのベンチマーク');
]] > 0 then error("can't create tables: "..dbm:errmsg()) end
local function fetchRow(stmt, ...)
if stmt:bind_values(...) > 0 then error("can't bind values: "..dbm:errmsg()) end
local f, s = stmt:nrows()
local row = f(s)
stmt:reset()
return row
end
local function exec(stmt, ...)
if stmt:bind_values(...) > 0 then error("can't bind values: "..dbm:errmsg()) end
if stmt:step() ~= lsqlite3.DONE then error("can't execute prepared statement: "..dbm:errmsg()) end
stmt:reset()
end
local randomInsertDStmt = dbm:prepare("INSERT INTO World (id, randomNumber) VALUES (?, ?)")
local randomInsertMStmt = dbm:prepare("INSERT INTO CachedWorld (id, randomNumber) VALUES (?, ?)")
dbm:exec("begin") -- open transaction for bulk insert
for i = 1, 10000 do
exec(randomInsertDStmt, i, math.random(10000))
exec(randomInsertMStmt, i, math.random(10000))
end
dbm:exec("end")
-- framework setup
local fm = require "fullmoon"
fm.setTemplate("fortune", [[<!DOCTYPE html><html><head><title>Fortunes</title></head><body>
<table><tr><th>id</th><th>message</th></tr>
{% for i = 1,#fortunes do %}
<tr><td>{%= fortunes[i][1] %}</td><td>{%& fortunes[i][2] %}</td></tr>
{% end %}
</table></body></html>]])
local function inRange(value, min, max)
value = tonumber(value) or 1
return value < min and min or value > max and max or value
end
local randomSelectDStmt = dbm:prepare"SELECT id, randomNumber FROM World WHERE id = ?"
local randomSelectMStmt = dbm:prepare"SELECT id, randomNumber FROM CachedWorld WHERE id = ?"
local randomUpdateStmt = dbm:prepare"UPDATE World SET randomnumber = ? WHERE id = ?"
fm.setRoute(fm.GET"/plaintext", fm.serveResponse(200, "Hello, World!"))
fm.setRoute(fm.GET"/json", fm.serveContent("json", {Message = "Hello, World!"}))
fm.setRoute(fm.GET"/db", function(r)
return fm.serveContent("json", fetchRow(randomSelectDStmt, math.random(10000)))
end)
fm.setRoute(fm.GET{"/queries/?", "/cached-worlds/?"}, function(r)
local stmt = r.path:find("^/queries") and randomSelectDStmt or randomSelectMStmt
local results = {}
for i = 1, inRange(r.params.queries, 1, 500) do
results[i] = fetchRow(stmt, math.random(10000))
end
return fm.serveContent("json", results)
end)
fm.setRoute(fm.GET"/updates/?", function(r)
local results = {}
for i = 1, inRange(r.params.queries, 1, 500) do
results[i] = fetchRow(randomSelectDStmt, math.random(10000))
results[i].randomNumber = math.random(10000) -- assign new random number
exec(randomUpdateStmt, results[i].randomNumber, results[i].id)
end
return fm.serveContent("json", results)
end)
fm.setRoute(fm.GET"/fortunes", function(r)
local num, fortunes = 2, {{0, "Additional fortune added at request time."}}
for id, message in dbm:urows("SELECT id, message FROM Fortune") do
fortunes[num] = {id, message}
num = num + 1
end
table.sort(fortunes, function(a, b) return a[2] < b[2] end)
return fm.serveContent("fortune", {fortunes = fortunes})
end)
fm.run({port = 8080})
Form input validation helper functions and examples would be very useful to have.
As I've been working on several state synchronization projects -- SyncX, which is an implementation of Sync9 in Lua and Cabinet, which is an implementation of shelf in Lua -- I came to realize that it may be worth extending the web framework I've been developing with SQLite-based data synchronization, as it already includes SQLite, has the network layer to send and accept HTTP/HTTPS requests and has a DB management layer that takes care of query management and schema upgrades.
A quick word on terminology first. I'm using nodeid, peerid, and occasionally userid, which all mean slightly different things. nodeid is a unique identifier for each node/instance running the module and participating in synchronization. Each node can have multiple databases it's used to synchronize, but for the purpose of this document we can assume that it's one DB per node (without changing anything in the design). peer is one of the nodes that a particular node is directly interacting with with its identity and some version information associated with it being tracked.
The original goal was to build the simplest synchronization mechanism that would take DB changes and send them to other peers to apply. Since SQLite already provides the session extension that allows tracking of changes to one database and packaging and applying those changes to another database, I decided to build the synchronization on top of that mechanism.
The session extension API that is needed for the synchronization to work is quite simple:
create_session()
and changeset()
functions)apply_changeset()
function)When changes are captured, the session extension generates the smallest number of INSERT/UPDATE/DELETE statements that create the same result when applied.
The session extension also detects conflicts of various categories when changes are applied (that need to be resolved in different ways) and provides functions to inspect changesets if needed.
The session extension needs to be configured to track changes, so the framework provides one function (sync()
) to set up necessary tables and hooks to the current connections. When the sync() function is called, it adds methods that capture/apply changes and "promotes" the connection to provide the sync by installing the hook that captures the changes. It also creates two tables that are used to track the changes: one to track peers to exchange changes with and the other one to track generated and received changesets.
local dbm1 = fm.makeStorage("sync1.db", sql) -- open new connection to sync1.db
assert(dbm1:upgrade({delete = false})) -- upgrade the schema if needed
local cdbm1 = dbm1:sync({userid = "u1", remotes = {"http://localhost"}})
assert(1 == dbm1:execute("insert into foo(k, a, b, c, cnt1) values (1, 'v1', 'v1', 'v1', 0)"))
assert(1 == dbm1:execute("update foo set a = 'v2', b = 'v2', c = 'v1', cnt1 = cnt1 + 5 where k = 1"))
assert(1 == dbm1:execute("update foo set a = 'v3', b = 'v1', cnt1 = cnt1 + 2 where k = 1"))
All changes executed through the "original" connection are going to be versioned. If some operations need to be excluded, they can be executed with the connection returned by the sync()
function. If some tables need to be excluded, they can be excluded using the filter
option to the sync()
function.
As the result of this setup, any combination of versioned or non-version tables is supported.
The schema changes are not versioned, but schema upgrades are provided by the upgrade() function and are done transparently for the user (as long as the schema follows some rules, as described below).
Only those nodes that have been configured with one or more "remotes" are going to send messages to those remotes. Only those nodes that have been configured with "userid" (node identifier) are going to capture local changes. A node can capture local changes, but don't send then anywhere, as it may accept requests from other nodes and then send the recorded changes in response to the request from other nodes.
This allows for a flexible and dynamic configuration with nodes accepting requests as desired and sending generated and received messages to other nodes (based on configured "remotes" values).
Versioning is based on hybrid logical clocks with the clock updated with each local operation and reset if the remote clock value is larger.
Only local changes are versioned. As existing rows or values are not versioned individually, deletes don't need to be tracked in any special ways (no tombstones).
Changesets can be purged when their acceptance is acknowledged by all peers. It's easy to check for the earliest version seen by all peers and delete all older versions (if needed).
Most of the time the received changesets are going to have a newer/later version, so they can be directly applied to the current state of the DB. In some cases, some local changesets may already record newer changes, in which case to apply received changesets, the local changesets need to be "reverted" and re-applied after remote changesets are applied.
In other words, if versions 1, 3, and 4 are applied locally and version 2 is received, then 4 and then 3 need to be reverted with the state restored as of version 1 with 2 then applied and 3 and 4 re-applied. All these changes happen within a transaction and the conflicts are resolved as described below.
With the session extension sending old/new values for all UPDATE statements, it's possible to detect various conflicts that may happen with concurrent updates and need to be resolved for the changes to be consistently applied despite arriving in (potentially) different order.
Here is the conflict resolver function that is provided as part of the apply_changeset()
call:
local function conflictResolver(udata, econf, iter)
-- local and remote update/delete for the same key; allow
if econf == sdbm.CHANGESET_DATA then return sdbm.CHANGESET_REPLACE end
-- no primary key for update/delete; ignore
if econf == sdbm.CHANGESET_NOTFOUND then return sdbm.CHANGESET_OMIT end
-- local and remote insert with the same key; the applied insert replaces the existing one
if econf == sdbm.CHANGESET_CONFLICT then return sdbm.CHANGESET_REPLACE end
-- FK violations from the changeset; abort the changes and retry after fixing.
-- this may happen when the changeset is deleting a record still being referenced locally
if econf == sdbm.CHANGESET_FOREIGN_KEY then return sdbm.CHANGESET_ABORT end
-- ignore changes that violate constraints, as they can't be applied
-- TODO: mark as PENDING as re-apply with other PENDING changes
-- (as these may be related to schema changes that haven't been applied yet)
if econf == sdbm.CHANGESET_CONSTRAINT then return sdbm.CHANGESET_OMIT end
error("Conflict handler: Unexpected conflict value "..tostring(econf))
end
Some of the received changesets may have conflicts that can't be resolved locally. For example, if the remote schema has been updated and added fields to a table, then an insert to that table can't be applied locally until the schema is updated. These changesets are marked as PENDING
and are re-applied as part of each sync()
call with the expectation that the local conditions have improved enough to correct apply changesets.
Sending messages in the opposite direction (from peers with older schema to a newer one) is allowed, as sending changes for tables with fewer fields is accepted (assuming schema changes are adding fields, but not remove them).
I've considered using JSON, multipart/mixed, SQLite DB and a couple of other options, but for now settled on zipping the changesets into one archive with their versions being filenames, as SQLite can both generate and read/decompress zip archive, so it plays well with the goals of the project.
Sync-related constraints
Upgade-related constraints
The biggest selling point of caddy web server is the automatic letsencrypt DNS-01 challenge,
the configuration of Caddyfile is very simple:
{
servers {
protocols h3
}
acme_dns cloudflare <api token>
}
:443, mysite.com:443 {
respond / "Hello, world!"
}
So I'm expecting this in fullmoon.
I'm using the oneof
validator to check that a set of radio buttons is 0,1,2 or unselected with a rule like:
{ "status", optional = true, oneof = { false, "0", "1", "2" }
but the boolean false trips up on the table.concat
here:
https://github.com/pkulchenko/fullmoon/blob/master/fullmoon.lua#L998
Should it be mentioned in docs that oneof
values need to be table.concat
safe or could the error message be turned into a string some other way so that booleans can work? I'd prefer them to work so am using the attached patch for now but am willing to use test
for these cases too if you think thats better.
Probably overlooking something on my end, but I'm unable to get the response body for some reason. I have a handler defined like so:
local function handleTarget(request)
print("Handling target")
print("Body: "..request.body)
...
end
fullmoon.setRoute("/*",
function (request)
print("SET OPTIONS")
request.headers["Access-Control-Allow-Origin"] = "*"
if request.method == "OPTIONS" then
return fullmoon.serveResponse(200, "")
end
return false
end
)
fullmoon.setRoute("/:target", handleTarget)
When sending a request without a body everything works fine; I get a 200 response. When I attach a body to the request it seems to trigger a CORS issue for some reason. Also, request.body
returns nil
. Using redbean's GetBody()
didn't work either. I'm using hoppscotch.io to test the API.
Is there some reason makeStorage
shouldn't support in memory databases?
I changed my makeStorage
to use sqlite3.open_memory
and my application appears to run fine without any changes. I'd like to use an in memory db when writing tests and be able to use the same interface as in the application code.
Hi Paul, I'm trying to check if a certain cookie is set and can't find a way of doing it without causing an error.
I've tried wrapping the call in a pcall
like this local status,value = pcall(r.cookies.my_cookie)
but that doesn't seem to work either, the program still crashes with a stack trace pointing at the GetCookie
function from RedBean with bad argument #1 to 'func' (string expected, got no value).
What's the correct/idiomatic way of reading a cookie with Fullmoon when you don't know if it's set or not? Or otherwise, is there a way to set a cookie value only if it does not exist?
The other two serve*
functions that take a status put it as the first argument while serveRedirect
puts it last. Is there any interest in changing to serveRedirect(status, loc)
or putting status last for the other two?
Hi,
I'm confused how makeStorage
should be used.
When creating a database instance with makeStorage
in .init.lua
a SQLite connection will be opened and kept open until redbean is stopped.
local fm = require"fullmoon"
local db = fm.makeStorage("test.db", "-- initialize...")
Now, using db
in a route, the SQLite connection will be the same, even while the current process is forked off the main process.
fm.setRoute("/", function(r)
db:execute(...) -- silently works but not valid?
...
end)
When adding <close>
to db
, a nice error message is shown when requesting /
, because the database is closed before forking.
Now we have to create a new storage for a new SQLite connection:
fm.setRoute("/", function(r)
local newdb <close> = fm.makeStorage("test.db")
newdb:execute(...) -- safe
...
end)
Is this how the database management layer should be used?
While it is a working solution, I'd rather have it either
:clone()
or similar:fm.setRoute("/", function(r)
local newdb <close> = db:clone()
end)
All of these proposals will bring additional complexity, I can imagine that's not desired...
Thanks.
I'm probably just making a mistake, but I can't figure out how to match query parameters in a route. The documentation says it's possible, but nothing I've tried works. Mostly I've tried stuff along these lines:
fm.setRoute("/submit%?action=:a", function(r)
print(r.params.a)
end)
-- When I visit /submit?action=1, this does not match. If I make
-- it optional, i.e. "/submit%?action=(:a)", then it prints false.
Am I missing something? If I am, perhaps I can help improve the documentation once I understand it for the next poor fool like me.
Hi, I'm trying to create a health endpoint using a template and would like to call redbean functions lis GetHostOS(), GetCPUCount(), etc but can't seem to find a way to do it.
All I get is global 'GetHostOS()' is not callable (a nil value)
. I'm sure I'm missing something obvious.
The template looks like
SetTemplate("health", [[
Os: {%= GetHostOS() %}
]]
Hi, I am seeing this error when playing around with the techbench example.
The below is for the /json
route but same happens with /db
.
zip redbean.com .init.lua .lua/fullmoon.lua .lua/techbench.lua && sh ./redbean.com -vvmbag
I2022-06-06T15:09:28+000027:tool/net/redbean.c:5999:redbean:71541] (req) received 127.0.0.1:65423 HTTP11 GET https://localhost:8080/json "" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.15; rv:99.0) Gecko/20100101 Firefox/99.0"
V2022-06-06T15:09:28+000028:tool/net/redbean.c:6201:redbean:71541] (rsp) 200 OK
V2022-06-06T15:09:28+000027:/zip/.lua/fullmoon.lua:56:redbean:71541] (fm) match 7 route(s) against '/json'
V2022-06-06T15:09:28+000017:/zip/.lua/fullmoon.lua:56:redbean:71541] (fm) route '/plaintext' not matched
I2022-06-06T15:09:28+000009:/zip/.lua/fullmoon.lua:57:redbean:71541] (fm) route '/json' matched
I2022-06-06T15:09:28+000029:/zip/.lua/fullmoon.lua:236:redbean:71541] (fm) render template 'json'
W2022-06-06T15:09:28+000021:/zip/.lua/fullmoon.lua:58:redbean:71541] (fm) unexpected result from action handler: 'table: 0x1000800a7710' (table)
D2022-06-06T15:09:28+000010:tool/net/redbean.c:6350:redbean:71541] (stat) "/json" latency 173µs
I2022-06-06T15:09:28+000003:tool/net/redbean.c:3496:redbean:71541] (stat) sending 138 byte message
HTTP/1.1 200 OK
Content-Type: application/json
Date: Mon, 06 Jun 2022 22:09:27 GMT
Server: redbean/1.5 fullmoon/0.25
Content-Length: 0
Hello Paul!
I'm building a website layout, and I use the render
function a lot. In the second argument, I need to specify each variable that I'll pass to the template.
fm.setTemplate({"/templates/", html = "fmt"})
fm.setRoute("/", fm.serveContent("layout", {view = "index", var1= 1, var2 = 2, --[[...]] } ))
--layout.html
{%& var1 %}
{% render(view, {var1 = var1, var2 = var2, --[[...]]}) %}
--view.html
{%& var2 %}
To solve this, I group all the variables in the serveContent:
fm.setRoute("/", fm.serveContent("layout", {view = "index", data = {var1= 1, var2 = 2, --[[...]] }} ))
--layout.html
{%& data.var1 %}
{% render(view, data) %}
--view.html
{%& var2 %}
This solution is elegant and works for me!
What if fullmoon define a variable pointing to the table passed in serveContent? This way, it wouldn't be necessary to group as above. I haven't found the best name (data
, all
, opts
, _
, _ENV
, ...). If it's possible to just pass a true
.
fm.setTemplate({"/templates/", html = "fmt"})
fm.setRoute("/", fm.serveContent("layout", {view = "index", var1= 1, var2 = 2, --[[...]] } ))
--layout.html
{%& var1 %}
{% render(view, all) %}
--view.html
{%& var2 %}
If the above idea isn't possible, adding my solution of grouping into a table in the documentation could help other users. :)
Thanks
I want to upload files with my fullmoon app. So I added an <input multiple name=images type=file>
element and changed the form to be enctype=multipart/form-data
. Now r.params
doesn't contain any of the form params. I see everything in r.body
but don't see anyway to handle that conveniently. Do I need to parse the r.body
string and pick out the form params myself or am I missing something?
Thanks for any help and working on fullmoon.
Hi,
when using Fullmoon at WeKan Studio https://github.com/wekan/wekanstudio , I'm using petclinic changes so that it's safer to use SQLite. Are those changes from your forkable storage branch? Can you merge all newest changes to your master branch? I'm not sure what code belogs where, I got lost when trying to merge code.
I still did not get my code to work, when I tried your example at Discord. This commit added core you suggested:
If there are newer commits, they may be without those changes, that prevent code from working properly.
I'm aware this isn't the proper forum for this issue, but I don't know of any redbean-specific forum or discussion area and I'm at my wit's end with this. I'm making a website for my wedding invitations, and I've got my fiance impatiently waiting, and questioning my skills, which won't do at all!
I've had enormous fun making a tiny/simple web app with redbean/fullmoon, but I've never done this before and I'm stuck at the final hurdle. I have rented a DigitalOcean VM ('droplet') and purchased a domain name. I pointed the domain at DigitalOcean's nameservers and created an 'A' record pointing at the VM's IP address. The VM is running an instance of redbean/fullmoon listening on port 8080.
Now when I visit the domain name ('http://ournames.wedding') I am redirected to the IP address - i.e. I now see http://42.42.42.42 in the address bar. All I want is for that to remain http://ournames.wedding. But I've been trying different things for days and I don't even know what I'm trying to accomplish!
The server's running Ubuntu 18.04 if that's relevant. I'm posting this from here because the only information I can find online suggest fiddling with Apache's VirtualHosts file or doing some Wordpress config, which suggests it needs to be solved at the app level. And I have no idea what to do with redbean/fullmoon. Any help would be most gratefully recieved, and I will of course close this issue immediately.
Hello! I'm using Fullmoon to run several sites on one server. Great web framework!
When setting the index for the first host, the second host becomes not found.
local fm = require("fullmoon")
fm.setRoute({
host = "host-b.com",
"/",
method = "GET"
}, function(r)
return "host-b.com OK" --> Not found here
end)
fm.setRoute({
host = "host-a.com",
"/",
method = "GET"
}, function(r)
return "host-a.com OK"
end)
fm.run()
Is there any way for the above code to work?
Suggestion: make the fm.setRoute function find routes using the host parameter as priority
Thanks in advance
Hi @pkulchenko,
Appreciate you creating this awesome library. I really enjoy uisng it.
I see setTemplate supports a table like "fmg" template loaded as external file, which is more concise than "fmt" for creating dynamic htmx segments. I'm wondering if fmg table can be passed directly in lua code as 2nd parameter in setTemplate call.
fm.setTemplate("tmpl-name", [[{
div{id="id", class="...", ...}
}]])
I realize that div{...}
is an unevalable function call syntax (thus passing it as table object won't work?), the table may have to be wrapped in [[ ]] as string. setTemplate should be able to distinsh it from "fmt" by checking the beginning of template string starts with '<' or '{' charactor.
What're your thoughs?
Thanks!
Hi,
great project!
I would like to make a feature request: implement CSRF protection on fullmoon
For this, it would be necessary to implement two functions: one to generate the token and another to validate it later.
Thanks!
Hello,
An interesting feature would be to implement: a DDOS protection system built into fullmoon, which we could easily activate. You can use the example in the redbean.dev documentation.
Thanks!
I'm trying to ensure only certain types of files are accepted. When validating a file upload you just get the raw the bytes.
local validator = fm.makeValidator({
{ "file", msg = "...", test = function (file)
-- file is the raw bytes; i.e. r.params.file.data
-- it'd be nice to have file.data, file.headers, etc.
end},
})
As a work around I'm using multi-file inputs everywhere and just processing the first file. This works because you get access to the full multipart table:
local validator = fm.makeValidator({
{ "files", msg = "...", test = function (files)
local file = files[1]
-- file.headers, filename, data etc. are there as expected
end},
})
It got me thinking maybe the validators for single and multi file uploads could be more similar:
-- single file upload
local validator = fm.makeValidator({
{ "file", msg = "...", test = function (file)
-- file.headers, filename, data etc.
end},
})
-- multiple file upload
-- test function is called for each file instead of once as it is now
local validator = fm.makeValidator({
{ "files", msg = "...", test = function (file)
-- file.headers, filename, data etc.
end},
})
I could live without calling the test function for each of a multi-file upload but it seemed like a nice convenience. Having the full multipart table in the single file case would be most welcome.
A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.