Brought in data model / dependencies
A combination of the previous effort plus a few things swiped from Quatro
This commit is contained in:
parent
864777a831
commit
da6910e055
36
src/App.fs
Normal file
36
src/App.fs
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
/// Main server module for myPrayerJournal
|
||||||
|
module MyPrayerJournal.App
|
||||||
|
|
||||||
|
open Reader
|
||||||
|
open System.IO
|
||||||
|
open Suave
|
||||||
|
open Suave.Filters
|
||||||
|
open Suave.Operators
|
||||||
|
|
||||||
|
/// Data Configuration singleton
|
||||||
|
let lazyCfg = lazy (DataConfig.FromJson <| try File.ReadAllText "data-config.json" with _ -> "{}")
|
||||||
|
/// RethinkDB connection singleton
|
||||||
|
let lazyConn = lazy lazyCfg.Force().CreateConnection ()
|
||||||
|
/// Application dependencies
|
||||||
|
let deps = {
|
||||||
|
new IDependencies with
|
||||||
|
member __.Conn with get () = lazyConn.Force ()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Suave application
|
||||||
|
let app : WebPart =
|
||||||
|
choose [
|
||||||
|
GET >=> Files.browseHome
|
||||||
|
GET >=> Files.browseFileHome "index.html"
|
||||||
|
RequestErrors.NOT_FOUND "Page not found."
|
||||||
|
]
|
||||||
|
let suaveCfg = { defaultConfig with homeFolder = Some (Path.GetFullPath "./wwwroot/") }
|
||||||
|
|
||||||
|
[<EntryPoint>]
|
||||||
|
let main argv =
|
||||||
|
// Establish the data environment
|
||||||
|
liftDep getConn (Data.establishEnvironment >> Async.RunSynchronously)
|
||||||
|
|> run deps
|
||||||
|
|
||||||
|
startWebServer suaveCfg app
|
||||||
|
0
|
130
src/Data.fs
Normal file
130
src/Data.fs
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
namespace MyPrayerJournal
|
||||||
|
|
||||||
|
open Chiron
|
||||||
|
open RethinkDb.Driver
|
||||||
|
open RethinkDb.Driver.Net
|
||||||
|
open System
|
||||||
|
|
||||||
|
type ConfigParameter =
|
||||||
|
| Hostname of string
|
||||||
|
| Port of int
|
||||||
|
| AuthKey of string
|
||||||
|
| Timeout of int
|
||||||
|
| Database of string
|
||||||
|
|
||||||
|
type DataConfig = { Parameters : ConfigParameter list }
|
||||||
|
with
|
||||||
|
member this.CreateConnection () : IConnection =
|
||||||
|
let folder (builder : Connection.Builder) block =
|
||||||
|
match block with
|
||||||
|
| Hostname x -> builder.Hostname x
|
||||||
|
| Port x -> builder.Port x
|
||||||
|
| AuthKey x -> builder.AuthKey x
|
||||||
|
| Timeout x -> builder.Timeout x
|
||||||
|
| Database x -> builder.Db x
|
||||||
|
let bldr =
|
||||||
|
this.Parameters
|
||||||
|
|> Seq.fold folder (RethinkDB.R.Connection ())
|
||||||
|
upcast bldr.Connect()
|
||||||
|
member this.Database =
|
||||||
|
match this.Parameters
|
||||||
|
|> List.filter (fun x -> match x with Database _ -> true | _ -> false)
|
||||||
|
|> List.tryHead with
|
||||||
|
| Some (Database x) -> x
|
||||||
|
| _ -> RethinkDBConstants.DefaultDbName
|
||||||
|
static member FromJson json =
|
||||||
|
match Json.parse json with
|
||||||
|
| Object config ->
|
||||||
|
let options =
|
||||||
|
config
|
||||||
|
|> Map.toList
|
||||||
|
|> List.map (fun item ->
|
||||||
|
match item with
|
||||||
|
| "Hostname", String x -> Hostname x
|
||||||
|
| "Port", Number x -> Port <| int x
|
||||||
|
| "AuthKey", String x -> AuthKey x
|
||||||
|
| "Timeout", Number x -> Timeout <| int x
|
||||||
|
| "Database", String x -> Database x
|
||||||
|
| key, value ->
|
||||||
|
invalidOp <| sprintf "Unrecognized RethinkDB configuration parameter %s (value %A)" key value)
|
||||||
|
{ Parameters = options }
|
||||||
|
| _ -> { Parameters = [] }
|
||||||
|
|
||||||
|
|
||||||
|
/// Tables for data storage
|
||||||
|
module DataTable =
|
||||||
|
/// The table for prayer requests
|
||||||
|
[<Literal>]
|
||||||
|
let Request = "Request"
|
||||||
|
/// The table for users
|
||||||
|
[<Literal>]
|
||||||
|
let User = "User"
|
||||||
|
|
||||||
|
/// Extensions for the RethinkDB connection
|
||||||
|
[<RequireQualifiedAccess>]
|
||||||
|
module Data =
|
||||||
|
|
||||||
|
let private r = RethinkDB.R
|
||||||
|
|
||||||
|
/// Set up the environment for MyPrayerJournal
|
||||||
|
let establishEnvironment (conn : IConnection) =
|
||||||
|
/// Shorthand for the database
|
||||||
|
let db () = r.Db "myPrayerJournal"
|
||||||
|
// Be chatty about what we're doing
|
||||||
|
let mkStep = sprintf "[MyPrayerJournal] %s"
|
||||||
|
let logStep = mkStep >> Console.WriteLine
|
||||||
|
let logStepStart = mkStep >> Console.Write
|
||||||
|
let logStepEnd () = Console.WriteLine " done"
|
||||||
|
/// Ensure the database exists
|
||||||
|
let checkDatabase () =
|
||||||
|
async {
|
||||||
|
logStep "|> Checking database"
|
||||||
|
let! dbList = r.DbList().RunResultAsync<string list> conn
|
||||||
|
match dbList |> List.contains "myPrayerJournal" with
|
||||||
|
| true -> ()
|
||||||
|
| _ ->
|
||||||
|
logStepStart " Database not found - creating..."
|
||||||
|
do! r.DbCreate("myPrayerJournal").RunResultAsync conn
|
||||||
|
logStepEnd ()
|
||||||
|
}
|
||||||
|
/// Ensure all tables exit
|
||||||
|
let checkTables () =
|
||||||
|
async {
|
||||||
|
logStep "|> Checking tables"
|
||||||
|
let! tables = db().TableList().RunResultAsync<string list> conn
|
||||||
|
[ DataTable.Request; DataTable.User ]
|
||||||
|
|> List.filter (fun tbl -> not (tables |> List.contains tbl))
|
||||||
|
|> List.map (fun tbl ->
|
||||||
|
async {
|
||||||
|
logStepStart <| sprintf " %s table not found - creating..." tbl
|
||||||
|
do! db().TableCreate(tbl).RunResultAsync conn
|
||||||
|
logStepEnd()
|
||||||
|
})
|
||||||
|
|> List.iter Async.RunSynchronously
|
||||||
|
}
|
||||||
|
/// Ensure the proper indexes exist
|
||||||
|
let checkIndexes () =
|
||||||
|
async {
|
||||||
|
logStep "|> Checking indexes"
|
||||||
|
let! reqIdx = db().Table(DataTable.Request).IndexList().RunResultAsync<string list> conn
|
||||||
|
match reqIdx |> List.contains "UserId" with
|
||||||
|
| true -> ()
|
||||||
|
| _ ->
|
||||||
|
logStepStart <| sprintf " %s.UserId index not found - creating..." DataTable.Request
|
||||||
|
do! db().Table(DataTable.Request).IndexCreate("UserId").RunResultAsync conn
|
||||||
|
logStepEnd ()
|
||||||
|
let! usrIdx = db().Table(DataTable.User).IndexList().RunResultAsync<string list> conn
|
||||||
|
match usrIdx |> List.contains "Email" with
|
||||||
|
| true -> ()
|
||||||
|
| _ ->
|
||||||
|
logStepStart <| sprintf " %s.Email index not found - creating..." DataTable.User
|
||||||
|
do! db().Table(DataTable.User).IndexCreate("Email").RunResultAsync conn
|
||||||
|
logStepEnd ()
|
||||||
|
}
|
||||||
|
async {
|
||||||
|
logStep "Database checks starting"
|
||||||
|
do! checkDatabase ()
|
||||||
|
do! checkTables ()
|
||||||
|
do! checkIndexes ()
|
||||||
|
logStep "Database checks complete"
|
||||||
|
}
|
48
src/Dependencies.fs
Normal file
48
src/Dependencies.fs
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
namespace MyPrayerJournal
|
||||||
|
|
||||||
|
open RethinkDb.Driver.Net
|
||||||
|
|
||||||
|
// -- begin code lifted from #er demo --
|
||||||
|
type ReaderM<'d, 'out> = 'd -> 'out
|
||||||
|
|
||||||
|
module Reader =
|
||||||
|
// basic operations
|
||||||
|
let run dep (rm : ReaderM<_,_>) = rm dep
|
||||||
|
let constant (c : 'c) : ReaderM<_,'c> = fun _ -> c
|
||||||
|
// lifting of functions and state
|
||||||
|
let lift1 (f : 'd -> 'a -> 'out) : 'a -> ReaderM<'d, 'out> = fun a dep -> f dep a
|
||||||
|
let lift2 (f : 'd -> 'a -> 'b -> 'out) : 'a -> 'b -> ReaderM<'d, 'out> = fun a b dep -> f dep a b
|
||||||
|
let lift3 (f : 'd -> 'a -> 'b -> 'c -> 'out) : 'a -> 'b -> 'c -> ReaderM<'d, 'out> = fun a b c dep -> f dep a b c
|
||||||
|
let liftDep (proj : 'd2 -> 'd1) (rm : ReaderM<'d1, 'output>) : ReaderM<'d2, 'output> = proj >> rm
|
||||||
|
// functor
|
||||||
|
let fmap (f : 'a -> 'b) (g : 'c -> 'a) : ('c -> 'b) = g >> f
|
||||||
|
let map (f : 'a -> 'b) (rm : ReaderM<'d, 'a>) : ReaderM<'d,'b> = rm >> f
|
||||||
|
let (<?>) = map
|
||||||
|
// applicative-functor
|
||||||
|
let apply (f : ReaderM<'d, 'a->'b>) (rm : ReaderM<'d, 'a>) : ReaderM<'d, 'b> =
|
||||||
|
fun dep ->
|
||||||
|
let f' = run dep f
|
||||||
|
let a = run dep rm
|
||||||
|
f' a
|
||||||
|
let (<*>) = apply
|
||||||
|
// monad
|
||||||
|
let bind (rm : ReaderM<'d, 'a>) (f : 'a -> ReaderM<'d,'b>) : ReaderM<'d, 'b> =
|
||||||
|
fun dep ->
|
||||||
|
f (rm dep)
|
||||||
|
|> run dep
|
||||||
|
let (>>=) = bind
|
||||||
|
type ReaderMBuilder internal () =
|
||||||
|
member __.Bind(m, f) = m >>= f
|
||||||
|
member __.Return(v) = constant v
|
||||||
|
member __.ReturnFrom(v) = v
|
||||||
|
member __.Delay(f) = f ()
|
||||||
|
let reader = ReaderMBuilder()
|
||||||
|
// -- end code lifted from #er demo --
|
||||||
|
|
||||||
|
type IDependencies =
|
||||||
|
abstract Conn : IConnection
|
||||||
|
|
||||||
|
[<AutoOpen>]
|
||||||
|
module DependencyExtraction =
|
||||||
|
|
||||||
|
let getConn (deps : IDependencies) = deps.Conn
|
74
src/Entities.fs
Normal file
74
src/Entities.fs
Normal file
@ -0,0 +1,74 @@
|
|||||||
|
namespace MyPrayerJournal
|
||||||
|
|
||||||
|
open Newtonsoft.Json
|
||||||
|
|
||||||
|
/// A user
|
||||||
|
type User = {
|
||||||
|
/// The Id of the user
|
||||||
|
[<JsonProperty("id")>]
|
||||||
|
Id : string
|
||||||
|
/// The user's e-mail address
|
||||||
|
Email : string
|
||||||
|
/// The user's name
|
||||||
|
Name : string
|
||||||
|
/// The time zone in which the user resides
|
||||||
|
TimeZone : string
|
||||||
|
/// The last time the user logged on
|
||||||
|
LastSeenOn : int64
|
||||||
|
}
|
||||||
|
with
|
||||||
|
/// An empty User
|
||||||
|
static member Empty =
|
||||||
|
{ Id = ""
|
||||||
|
Email = ""
|
||||||
|
Name = ""
|
||||||
|
TimeZone = ""
|
||||||
|
LastSeenOn = int64 0 }
|
||||||
|
|
||||||
|
|
||||||
|
/// Request history entry
|
||||||
|
type History = {
|
||||||
|
/// The instant at which the update was made
|
||||||
|
AsOf : int64
|
||||||
|
/// The action that was taken on the request
|
||||||
|
Action : string list
|
||||||
|
/// The status of the request (filled if it changed)
|
||||||
|
Status : string option
|
||||||
|
/// The text of the request (filled if it changed)
|
||||||
|
Text : string option
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A prayer request
|
||||||
|
type Request = {
|
||||||
|
/// The Id of the request
|
||||||
|
[<JsonProperty("id")>]
|
||||||
|
Id : string
|
||||||
|
/// The Id of the user to whom this request belongs
|
||||||
|
UserId : string
|
||||||
|
/// The instant this request was entered
|
||||||
|
EnteredOn : int64
|
||||||
|
/// The history for this request
|
||||||
|
History : History list
|
||||||
|
}
|
||||||
|
with
|
||||||
|
/// The current status of the prayer request
|
||||||
|
member this.Status =
|
||||||
|
this.History
|
||||||
|
|> List.sortBy (fun item -> -item.AsOf)
|
||||||
|
|> List.map (fun item -> item.Status)
|
||||||
|
|> List.filter Option.isSome
|
||||||
|
|> List.map Option.get
|
||||||
|
|> List.head
|
||||||
|
/// The current text of the prayer request
|
||||||
|
member this.Text =
|
||||||
|
this.History
|
||||||
|
|> List.sortBy (fun item -> -item.AsOf)
|
||||||
|
|> List.map (fun item -> item.Text)
|
||||||
|
|> List.filter Option.isSome
|
||||||
|
|> List.map Option.get
|
||||||
|
|> List.head
|
||||||
|
member this.LastActionOn =
|
||||||
|
this.History
|
||||||
|
|> List.sortBy (fun item -> -item.AsOf)
|
||||||
|
|> List.map (fun item -> item.AsOf)
|
||||||
|
|> List.head
|
16
src/Extensions.fs
Normal file
16
src/Extensions.fs
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
[<AutoOpen>]
|
||||||
|
module MyPrayerJournal.Extensions
|
||||||
|
|
||||||
|
open System.Threading.Tasks
|
||||||
|
|
||||||
|
// H/T: Suave
|
||||||
|
type AsyncBuilder with
|
||||||
|
/// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
|
||||||
|
/// a standard .NET task
|
||||||
|
member x.Bind(t : Task<'T>, f:'T -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f)
|
||||||
|
|
||||||
|
/// An extension method that overloads the standard 'Bind' of the 'async' builder. The new overload awaits on
|
||||||
|
/// a standard .NET task which does not commpute a value
|
||||||
|
member x.Bind(t : Task, f : unit -> Async<'R>) : Async<'R> = async.Bind (Async.AwaitTask t, f)
|
||||||
|
|
||||||
|
member x.ReturnFrom(t : Task<'T>) : Async<'T> = Async.AwaitTask t
|
@ -1,22 +0,0 @@
|
|||||||
// Learn more about F# at http://fsharp.org
|
|
||||||
|
|
||||||
open System.IO
|
|
||||||
open Suave
|
|
||||||
open Suave.Filters
|
|
||||||
open Suave.Operators
|
|
||||||
|
|
||||||
let app : WebPart =
|
|
||||||
choose [
|
|
||||||
//GET >=> path "/" >=> Files.file "index.html"
|
|
||||||
//GET >=> path "" >=> Files.file "index.html"
|
|
||||||
GET >=> Files.browseHome
|
|
||||||
GET >=> Files.browseFileHome "index.html"
|
|
||||||
RequestErrors.NOT_FOUND "Page not found."
|
|
||||||
]
|
|
||||||
[<EntryPoint>]
|
|
||||||
let main argv =
|
|
||||||
let config =
|
|
||||||
{ defaultConfig with homeFolder = Some (Path.GetFullPath "./wwwroot/") }
|
|
||||||
|
|
||||||
startWebServer config app
|
|
||||||
0 // return an integer exit code
|
|
@ -1,30 +1,44 @@
|
|||||||
{
|
{
|
||||||
"version": "1.0.0-*",
|
|
||||||
"buildOptions": {
|
"buildOptions": {
|
||||||
"debugType": "portable",
|
|
||||||
"emitEntryPoint": true,
|
|
||||||
"compilerName": "fsc",
|
|
||||||
"compile": {
|
"compile": {
|
||||||
"includeFiles": [
|
"includeFiles": [
|
||||||
"Program.fs"
|
"Extensions.fs",
|
||||||
|
"Entities.fs",
|
||||||
|
"Dependencies.fs",
|
||||||
|
"Data.fs",
|
||||||
|
"App.fs"
|
||||||
]
|
]
|
||||||
}
|
},
|
||||||
|
"compilerName": "fsc",
|
||||||
|
"debugType": "portable",
|
||||||
|
"emitEntryPoint": true,
|
||||||
|
"outputName": "myPrayerJournal"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"Chiron": "6.2.1",
|
||||||
|
"Newtonsoft.Json": "9.0.1",
|
||||||
|
"RethinkDb.Driver": "2.3.15",
|
||||||
"Suave": "2.0.0-rc2"
|
"Suave": "2.0.0-rc2"
|
||||||
},
|
},
|
||||||
"tools": {
|
|
||||||
"dotnet-compile-fsc": "1.0.0-preview2.1-*"
|
|
||||||
},
|
|
||||||
"frameworks": {
|
"frameworks": {
|
||||||
"netcoreapp1.1": {
|
"netcoreapp1.1": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-161111",
|
||||||
"Microsoft.NETCore.App": {
|
"Microsoft.NETCore.App": {
|
||||||
"type": "platform",
|
"type": "platform",
|
||||||
"version": "1.1.0"
|
"version": "1.1.0"
|
||||||
},
|
}
|
||||||
"Microsoft.FSharp.Core.netcore": "1.0.0-alpha-161111"
|
},
|
||||||
}
|
"imports": [
|
||||||
|
"portable-net45+win8+dnxcore50",
|
||||||
|
"portable-net45+win8",
|
||||||
|
"net452",
|
||||||
|
"dnxcore50"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
}
|
"tools": {
|
||||||
|
"dotnet-compile-fsc": "1.0.0-preview2.1-*"
|
||||||
|
},
|
||||||
|
"version": "0.8.1-*"
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user