diff --git a/docs/docs/config.html b/docs/docs/config.html index 25faf4e..afe32dd 100644 --- a/docs/docs/config.html +++ b/docs/docs/config.html @@ -230,7 +230,7 @@

in_memory

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/get-started.html b/docs/docs/get-started.html index 976b5bd..629ed50 100644 --- a/docs/docs/get-started.html +++ b/docs/docs/get-started.html @@ -166,7 +166,7 @@

Sample Applications

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/host.html b/docs/docs/host.html index 3649238..e868763 100644 --- a/docs/docs/host.html +++ b/docs/docs/host.html @@ -731,7 +731,7 @@

use_middleware

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/index.html b/docs/docs/index.html index 2fbb484..f3acfae 100644 --- a/docs/docs/index.html +++ b/docs/docs/index.html @@ -72,7 +72,7 @@

Guides

Issue Tracker Discussion Twitter -
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/markup.html b/docs/docs/markup.html index e6556b1..4996eda 100644 --- a/docs/docs/markup.html +++ b/docs/docs/markup.html @@ -293,7 +293,7 @@

SVG

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/request.html b/docs/docs/request.html index e5b7cdb..4d8fb13 100644 --- a/docs/docs/request.html +++ b/docs/docs/request.html @@ -259,7 +259,7 @@

JSON

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/response.html b/docs/docs/response.html index b1fcd93..f7d1a50 100644 --- a/docs/docs/response.html +++ b/docs/docs/response.html @@ -240,7 +240,7 @@

Debugging Requests

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/routing.html b/docs/docs/routing.html index 272e40b..9059c49 100644 --- a/docs/docs/routing.html +++ b/docs/docs/routing.html @@ -219,7 +219,7 @@

Multi-method Endpoints

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/security.html b/docs/docs/security.html index 867351a..c9c1472 100644 --- a/docs/docs/security.html +++ b/docs/docs/security.html @@ -268,7 +268,7 @@

Cryptography

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/docs/tutorial.html b/docs/docs/tutorial.html index d13a5c5..8d77809 100644 --- a/docs/docs/tutorial.html +++ b/docs/docs/tutorial.html @@ -130,7 +130,7 @@

Coming soon!

-
© 2020-2023 Pim Brouwers & contributors.
+
© 2020-2024 Pim Brouwers & contributors.
diff --git a/docs/index.html b/docs/index.html index 62476d1..75fb8ce 100644 --- a/docs/index.html +++ b/docs/index.html @@ -41,12 +41,8 @@
docs - guide - code + code + help
@@ -182,7 +178,7 @@

License

diff --git a/samples/Tutorial/Program.fs b/samples/Tutorial/Program.fs index cb97392..ea29400 100644 --- a/samples/Tutorial/Program.fs +++ b/samples/Tutorial/Program.fs @@ -12,11 +12,10 @@ type EntrySummary = EntryDate : DateTime Summary : string } -module Db = +module Infrastructure = open System.Data - type IDbConnectionFactory = - abstract member CreateConnection : unit -> IDbConnection + type CreateDbConnection = unit -> IDbConnection type IDbCommand with member x.AddParameter(name : string, value : obj) : unit = @@ -39,8 +38,17 @@ module Db = use rd = x.ExecuteReader() [ while rd.Read() do yield map rd ] + module Sqlite = + open Microsoft.Data.Sqlite + + let createDbConnection (connecitonString : string) : CreateDbConnection = fun () -> + let conn = new SqliteConnection(connecitonString) + conn.Open() + conn + module EntryStore = let save (conn : IDbConnection) (entry : Entry) : unit = + printfn "%A" entry use cmd = conn.CreateCommand() cmd.CommandText <- " INSERT OR REPLACE INTO entry (entry_id, html_content, text_content, modified_date) @@ -83,16 +91,24 @@ module Db = EntryDate = rd.GetDateTime(rd.GetOrdinal("entry_date")) Summary = rd.GetString(rd.GetOrdinal("summary")) }) -module App = - open Db +module Web = + open Infrastructure - module Urls = + /// Kestrel endpoint routes + module Route = let index = "/" - let notFound = "/notfound" - + let notFound = "/not-found" let entryCreate = "/entry/create" - let entryEdit entryId = sprintf "/entry/edit/%O" entryId + let entryEdit = "/entry/edit/{entry_id}" + + /// Urls to be used within features + module Urls = + let index = Route.index + let notFound = Route.notFound + let entryCreate = Route.entryCreate + let entryEdit (entryId : Guid) = Route.entryEdit.Replace("{entry_id}", entryId.ToString("d")) + /// Shared markup module UI = open Falco.Markup @@ -158,15 +174,22 @@ module App = let mergedAttr = attr |> Attr.merge defaultAttr Elem.input mergedAttr - module ErrorPages = + module ErrorController = open Falco open UI - let notFound = + + let badRequest : HttpHandler = + Response.withStatusCode 400 + >> Response.ofPlainText "Bad request" + + let notFound : HttpHandler = let html = layout "Not Found" [ pageTitle "Not found" ] - Response.ofHtml html + + Response.withStatusCode 404 + >> Response.ofHtml html module EntryViews = open Falco.Markup @@ -187,11 +210,9 @@ module App = Elem.a [ Attr.href (Urls.entryEdit e.EntryId); Attr.class' "db mb4 no-underline white-90" ] [ Elem.div [ Attr.class' "mb1 f6 code moon-gray" ] [ Text.raw (e.EntryDate.ToString("yyyy/MM/dd HH:MM")) ] Elem.div [] [ Text.raw e.Summary ] - Elem.div [ Attr.class' "w1 mt3 bt b--moon-gray" ] [] ] ] + Elem.div [ Attr.class' "w1 mt3 bt b--moon-gray" ] [] ] ] ] - ] - - let save action entry = + let save action entry token = let title = "A place for your thoughts..." let htmlContent = @@ -220,19 +241,16 @@ module App = Elem.input [ Attr.type' "hidden"; Attr.name "entry_id"; Attr.value (string entry.EntryId) ] Elem.input [ Attr.type' "hidden"; Attr.name "html_content"; Attr.id "bullet-editor-html" ] - Elem.input [ Attr.type' "hidden"; Attr.name "text_content"; Attr.id "bullet-editor-text" ] - ] - ] + Elem.input [ Attr.type' "hidden"; Attr.name "text_content"; Attr.id "bullet-editor-text" ] ] ] module EntryController = open Falco /// GET / - let index : HttpHandler = fun ctx -> - let db = ctx.GetService() - use conn = db.CreateConnection() + let index (createDbConnection : CreateDbConnection) : HttpHandler = + use conn = createDbConnection () let entries = EntryStore.getAll conn - Response.ofHtml (EntryViews.index entries) ctx + Response.ofHtml (EntryViews.index entries) /// GET /entry/create let create : HttpHandler = fun ctx -> @@ -243,72 +261,51 @@ module App = let view = EntryViews.save Urls.entryCreate newEntry - Response.ofHtml view ctx + Response.ofHtmlCsrf view ctx /// GET /entry/edit/{id} - let edit : HttpHandler = fun ctx -> - let query = Request.getRoute ctx - let entryId = query.TryGetGuid "id" - - match entryId with - | Some entryId -> - let db = ctx.GetService() - use conn = db.CreateConnection() - match EntryStore.get conn entryId with - | Some entry -> - let html = EntryViews.save (Urls.entryEdit entryId) entry - Response.ofHtml html ctx - + let edit (createDbConneciton : CreateDbConnection) : HttpHandler = + let readRoute (route : RouteCollectionReader) = + route.TryGetGuid "entry_id" + + let handle (input : Guid option) = + match input with + | Some entryId -> + use conn = createDbConneciton () + match EntryStore.get conn entryId with + | Some entry -> + let html = EntryViews.save (Urls.entryEdit entryId) entry + Response.ofHtmlCsrf html + | None -> + ErrorController.notFound | None -> - ErrorPages.notFound ctx - - | None -> - Response.redirectTemporarily Urls.notFound ctx + Response.redirectTemporarily Urls.notFound + Request.mapRoute readRoute handle /// POST /entry/create, /entry/edit/{id} - let save : HttpHandler = fun ctx -> - task { - let! form = Request.getForm ctx - let entry : Entry = { - EntryId = form.GetGuid "entry_id" - HtmlContent = form.GetString "html_content" - TextContent = form.GetString "text_content" } + let save (createDbConnection : CreateDbConnection) : HttpHandler = + let readForm (form : FormCollectionReader) = + { EntryId = form.GetGuid "entry_id" + HtmlContent = form.GetString "html_content" + TextContent = form.GetString "text_content" } - let db = ctx.GetService() - use conn = db.CreateConnection() - EntryStore.save conn entry + let handle (input : Entry) = + use conn = createDbConnection () + EntryStore.save conn input + Response.redirectTemporarily Urls.index - return Response.redirectTemporarily Urls.index ctx - } + Request.mapForm readForm handle module Program = open Falco open Falco.Routing open Falco.HostBuilder - open Microsoft.AspNetCore.Builder open Microsoft.Extensions.Configuration - open Microsoft.Extensions.DependencyInjection - open Microsoft.Data.Sqlite - open Db - open App - - type DbConnectionFactory (connectionString : string) = - interface IDbConnectionFactory with - member _.CreateConnection () = - let conn = new SqliteConnection(connectionString) - conn.Open() - conn - - module Endpoints = - let index = Urls.index - let notFound = Urls.notFound - - let entryCreate = Urls.entryCreate - let entryEdit = Urls.entryEdit "{id}" + open Infrastructure + open Web [] let main args = - /// App configuration, loaded on startup let config = configuration [||] { required_json "appsettings.json" } @@ -318,31 +315,20 @@ module Program = if String.IsNullOrWhiteSpace(connectionString) then failwith "Invalid connection string [EMPTY]" - let dbConnectionService (svc : IServiceCollection) = - svc.AddSingleton(fun _ -> - DbConnectionFactory(connectionString)) + let createDbConneciton = Sqlite.createDbConnection connectionString webHost args { - use_ifnot FalcoExtensions.IsDevelopment HstsBuilderExtensions.UseHsts - use_https use_static_files - - add_service dbConnectionService - - not_found ErrorPages.notFound - + not_found ErrorController.notFound endpoints [ - get Endpoints.index EntryController.index - - all Endpoints.entryCreate [ + get Route.index (EntryController.index createDbConneciton) + get Route.notFound ErrorController.notFound + all Route.entryCreate [ GET, EntryController.create - POST, EntryController.save ] - - all Endpoints.entryEdit [ - GET, EntryController.edit - POST, EntryController.save ] - - get Endpoints.notFound ErrorPages.notFound + POST, (EntryController.save createDbConneciton) ] + all Route.entryEdit [ + GET, (EntryController.edit createDbConneciton) + POST, (EntryController.save createDbConneciton) ] ] } 0 \ No newline at end of file diff --git a/samples/Tutorial/wwwroot/index.js b/samples/Tutorial/wwwroot/index.js index e4eaff1..583daf2 100644 --- a/samples/Tutorial/wwwroot/index.js +++ b/samples/Tutorial/wwwroot/index.js @@ -9,8 +9,8 @@ window.onload = () => { bulletEditor.focus() bulletEditor.addEventListener('input', (e) => { - bulletEditorHtml.value = e.srcElement.innerHTML.trim() - bulletEditorText.value = e.srcElement.innerText.trim() + // bulletEditorHtml.value = e.target.innerHTML.trim() + bulletEditorText.value = e.target.innerText.trim() }) } } \ No newline at end of file diff --git a/site/templates/layout.html b/site/templates/layout.html index e7dfd33..f71621c 100644 --- a/site/templates/layout.html +++ b/site/templates/layout.html @@ -43,12 +43,8 @@
docs - guide - code + code + help