Giter Club home page Giter Club logo

fullmoon's People

Contributors

dkulchenko avatar jimt avatar lolgab avatar mierenhoop avatar pkulchenko avatar vishalsodani avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

fullmoon's Issues

[Suggestion] Disable Listing

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

Processing layouts

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.

[help] can not load techbench module ?

Error message

❯ .\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

hand over route back to the redbean core if no matching fullmoon route

Hi, thank you very much for writing this awesome library!

Would you think it's a good idea to replace

return error2tmpl(404) -- use 404 template if available

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!

Any CRUD example for using SQLite?

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})

Provide DB synchronization

Background

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.

Terminology

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.

SQLite Session extension

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:

  • capture the changes (using create_session() and changeset() functions)
  • package those changes into a changepack (one or more changesets)
  • send the changes
  • apply the received changes (using the 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.

Design

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).

Changeset versioning

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.

Conflict handling

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).

Package format

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.

Requirements/Limitations

Sync-related constraints

  • use wal journal
  • set primary keys for all tracked tables and use NOT NULL for fields in primary keys
  • use random instead of autoincrement keys to minimize unnecessary conflicts
  • use cascade deletes to avoid/minimize fk constraint violations

Upgade-related constraints

  • set defaults on all (added) non-null fields (to allow upgrades for existing records)
  • use "create table if not exists" to facilitate schema upgrades, as the upgrades
    need to execute the current schema script without errors
  • use expanding schema evolution to add tables/fields
  • schema changes are not propagated, but can be supported with fullmoon upgrades
  • don't get too fancy with special characters in table names (because of upgrades,
    which require parsing of sql statements to support table changes)

Motivation/Goals

  • provide simple and efficient SQLite-based data synchronization
  • require minimal coding from developers (in the simplest case only one sync() call is needed)
  • provide redbean-based network and security layer
  • support direct (p2p), client-server, and relay-based models of data sync

using booleans in oneof rules

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.

0001-make-oneof-validation-boolean-safe.patch.txt

Getting the body of the request

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.

makeStorage support for in memory database

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.

Reading a cookie that has not been set causes an error

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?

serveRedirect parameter order

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?

Usage of makeStorage in requests

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

  1. Give an error when the database connection is used outside of the process it was created in (using unix.getpid?).
  2. Have the makeStorage instance decide for itself when it should create/close connections, again using the process id, freely allowing the user to access it anywhere.
  3. Have a method :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.

Uncertain how to obtain query parameters

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.

call Redbean API functions from template

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() %}
]]

Issue with techbench example

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

[Feature Request] Grouping variables passed to the template to be passed in the render

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

Support for multipart/form-data to allow uploading files

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.

How to merge newest changes of Fullmoon, SQLite etc?

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:

wekan/wekanstudio@1392d30

If there are newer commits, they may be without those changes, that prevent code from working properly.

Any help with DNS/virtual hosting/ip address?

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.

Index using 2 different hosts

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

Does setTemplate support fmg table in lua code directly

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!

[Feature Request] CSRF protection

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!

multipart form validation

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.

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.