Combined all F# code into one project
Less to migrate, less to maintain, and I'll never swap these out as components; might as well get the ease of managing them all in one project.
This commit is contained in:
140
src/MyWebLog.App/Data/Category.fs
Normal file
140
src/MyWebLog.App/Data/Category.fs
Normal file
@@ -0,0 +1,140 @@
|
||||
module MyWebLog.Data.RethinkDB.Category
|
||||
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Get all categories for a web log
|
||||
let getAllCategories conn (webLogId : string) =
|
||||
async {
|
||||
return! r.Table(Table.Category)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy("Name")
|
||||
.RunResultAsync<Category list> conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Get a specific category by its Id
|
||||
let tryFindCategory conn webLogId catId : Category option =
|
||||
async {
|
||||
let! c =
|
||||
r.Table(Table.Category)
|
||||
.Get(catId)
|
||||
.RunResultAsync<Category> conn
|
||||
return
|
||||
match box c with
|
||||
| null -> None
|
||||
| catt ->
|
||||
let cat : Category = unbox catt
|
||||
match cat.WebLogId = webLogId with true -> Some cat | _ -> None
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Add a category
|
||||
let addCategory conn (cat : Category) =
|
||||
async {
|
||||
do! r.Table(Table.Category)
|
||||
.Insert(cat)
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
type CategoryUpdateRecord =
|
||||
{ Name : string
|
||||
Slug : string
|
||||
Description : string option
|
||||
ParentId : string option
|
||||
}
|
||||
/// Update a category
|
||||
let updateCategory conn (cat : Category) =
|
||||
match tryFindCategory conn cat.WebLogId cat.Id with
|
||||
| Some _ ->
|
||||
async {
|
||||
do! r.Table(Table.Category)
|
||||
.Get(cat.Id)
|
||||
.Update(
|
||||
{ CategoryUpdateRecord.Name = cat.Name
|
||||
Slug = cat.Slug
|
||||
Description = cat.Description
|
||||
ParentId = cat.ParentId
|
||||
})
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
| _ -> ()
|
||||
|
||||
/// Update a category's children
|
||||
let updateChildren conn webLogId parentId (children : string list) =
|
||||
match tryFindCategory conn webLogId parentId with
|
||||
| Some _ ->
|
||||
async {
|
||||
do! r.Table(Table.Category)
|
||||
.Get(parentId)
|
||||
.Update(dict [ "Children", children ])
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
| _ -> ()
|
||||
|
||||
/// Delete a category
|
||||
let deleteCategory conn (cat : Category) =
|
||||
async {
|
||||
// Remove the category from its parent
|
||||
match cat.ParentId with
|
||||
| Some parentId ->
|
||||
match tryFindCategory conn cat.WebLogId parentId with
|
||||
| Some parent -> parent.Children
|
||||
|> List.filter (fun childId -> childId <> cat.Id)
|
||||
|> updateChildren conn cat.WebLogId parentId
|
||||
| _ -> ()
|
||||
| _ -> ()
|
||||
// Move this category's children to its parent
|
||||
cat.Children
|
||||
|> List.map (fun childId ->
|
||||
match tryFindCategory conn cat.WebLogId childId with
|
||||
| Some _ ->
|
||||
async {
|
||||
do! r.Table(Table.Category)
|
||||
.Get(childId)
|
||||
.Update(dict [ "ParentId", cat.ParentId ])
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Some
|
||||
| _ -> None)
|
||||
|> List.filter Option.isSome
|
||||
|> List.map Option.get
|
||||
|> List.iter Async.RunSynchronously
|
||||
// Remove the category from posts where it is assigned
|
||||
let! posts =
|
||||
r.Table(Table.Post)
|
||||
.GetAll(cat.WebLogId).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1 (fun p -> upcast p.["CategoryIds"].Contains cat.Id))
|
||||
.RunResultAsync<Post list> conn
|
||||
|> Async.AwaitTask
|
||||
posts
|
||||
|> List.map (fun post ->
|
||||
async {
|
||||
do! r.Table(Table.Post)
|
||||
.Get(post.Id)
|
||||
.Update(dict [ "CategoryIds", post.CategoryIds |> List.filter (fun c -> c <> cat.Id) ])
|
||||
.RunResultAsync conn
|
||||
})
|
||||
|> List.iter Async.RunSynchronously
|
||||
// Now, delete the category
|
||||
do! r.Table(Table.Category)
|
||||
.Get(cat.Id)
|
||||
.Delete()
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Get a category by its slug
|
||||
let tryFindCategoryBySlug conn (webLogId : string) (slug : string) =
|
||||
async {
|
||||
let! cat = r.Table(Table.Category)
|
||||
.GetAll(r.Array (webLogId, slug)).OptArg("index", "Slug")
|
||||
.RunResultAsync<Category list> conn
|
||||
return cat |> List.tryHead
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
43
src/MyWebLog.App/Data/DataConfig.fs
Normal file
43
src/MyWebLog.App/Data/DataConfig.fs
Normal file
@@ -0,0 +1,43 @@
|
||||
namespace MyWebLog.Data.RethinkDB
|
||||
|
||||
open RethinkDb.Driver
|
||||
open RethinkDb.Driver.Net
|
||||
open Newtonsoft.Json
|
||||
|
||||
/// Data configuration
|
||||
type DataConfig =
|
||||
{ /// The hostname for the RethinkDB server
|
||||
[<JsonProperty("hostname")>]
|
||||
Hostname : string
|
||||
/// The port for the RethinkDB server
|
||||
[<JsonProperty("port")>]
|
||||
Port : int
|
||||
/// The authorization key to use when connecting to the server
|
||||
[<JsonProperty("authKey")>]
|
||||
AuthKey : string
|
||||
/// How long an attempt to connect to the server should wait before giving up
|
||||
[<JsonProperty("timeout")>]
|
||||
Timeout : int
|
||||
/// The name of the default database to use on the connection
|
||||
[<JsonProperty("database")>]
|
||||
Database : string
|
||||
/// A connection to the RethinkDB server using the configuration in this object
|
||||
[<JsonIgnore>]
|
||||
Conn : IConnection }
|
||||
with
|
||||
/// Use RethinkDB defaults for non-provided options, and connect to the server
|
||||
static member Connect config =
|
||||
let host cfg = match cfg.Hostname with null -> { cfg with Hostname = RethinkDBConstants.DefaultHostname } | _ -> cfg
|
||||
let port cfg = match cfg.Port with 0 -> { cfg with Port = RethinkDBConstants.DefaultPort } | _ -> cfg
|
||||
let auth cfg = match cfg.AuthKey with null -> { cfg with AuthKey = RethinkDBConstants.DefaultAuthkey } | _ -> cfg
|
||||
let timeout cfg = match cfg.Timeout with 0 -> { cfg with Timeout = RethinkDBConstants.DefaultTimeout } | _ -> cfg
|
||||
let db cfg = match cfg.Database with null -> { cfg with Database = RethinkDBConstants.DefaultDbName } | _ -> cfg
|
||||
let connect cfg =
|
||||
{ cfg with Conn = RethinkDB.R.Connection()
|
||||
.Hostname(cfg.Hostname)
|
||||
.Port(cfg.Port)
|
||||
.AuthKey(cfg.AuthKey)
|
||||
.Db(cfg.Database)
|
||||
.Timeout(cfg.Timeout)
|
||||
.Connect () }
|
||||
(host >> port >> auth >> timeout >> db >> connect) config
|
||||
16
src/MyWebLog.App/Data/Extensions.fs
Normal file
16
src/MyWebLog.App/Data/Extensions.fs
Normal file
@@ -0,0 +1,16 @@
|
||||
[<AutoOpen>]
|
||||
module MyWebLog.Data.RethinkDB.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.AwaitTask t
|
||||
98
src/MyWebLog.App/Data/Page.fs
Normal file
98
src/MyWebLog.App/Data/Page.fs
Normal file
@@ -0,0 +1,98 @@
|
||||
module MyWebLog.Data.RethinkDB.Page
|
||||
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Try to find a page by its Id, optionally including revisions
|
||||
let tryFindPageById conn webLogId (pageId : string) includeRevs =
|
||||
async {
|
||||
let q =
|
||||
r.Table(Table.Page)
|
||||
.Get pageId
|
||||
let! thePage =
|
||||
match includeRevs with
|
||||
| true -> q.RunResultAsync<Page> conn
|
||||
| _ -> q.Without("Revisions").RunResultAsync<Page> conn
|
||||
return
|
||||
match box thePage with
|
||||
| null -> None
|
||||
| page ->
|
||||
let pg : Page = unbox page
|
||||
match pg.WebLogId = webLogId with true -> Some pg | _ -> None
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Find a page by its permalink
|
||||
let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
|
||||
async {
|
||||
let! pg =
|
||||
r.Table(Table.Page)
|
||||
.GetAll(r.Array (webLogId, permalink)).OptArg("index", "Permalink")
|
||||
.Without("Revisions")
|
||||
.RunResultAsync<Page list> conn
|
||||
return List.tryHead pg
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Get a list of all pages (excludes page text and revisions)
|
||||
let findAllPages conn (webLogId : string) =
|
||||
async {
|
||||
return!
|
||||
r.Table(Table.Page)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy("Title")
|
||||
.Without("Text", "Revisions")
|
||||
.RunResultAsync<Page list> conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Add a page
|
||||
let addPage conn (page : Page) =
|
||||
async {
|
||||
do! r.Table(Table.Page)
|
||||
.Insert(page)
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> (Async.RunSynchronously >> ignore)
|
||||
|
||||
type PageUpdateRecord =
|
||||
{ Title : string
|
||||
Permalink : string
|
||||
PublishedOn : int64
|
||||
UpdatedOn : int64
|
||||
ShowInPageList : bool
|
||||
Text : string
|
||||
Revisions : Revision list }
|
||||
/// Update a page
|
||||
let updatePage conn (page : Page) =
|
||||
match tryFindPageById conn page.WebLogId page.Id false with
|
||||
| Some _ ->
|
||||
async {
|
||||
do! r.Table(Table.Page)
|
||||
.Get(page.Id)
|
||||
.Update({ PageUpdateRecord.Title = page.Title
|
||||
Permalink = page.Permalink
|
||||
PublishedOn = page.PublishedOn
|
||||
UpdatedOn = page.UpdatedOn
|
||||
ShowInPageList = page.ShowInPageList
|
||||
Text = page.Text
|
||||
Revisions = page.Revisions })
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> (Async.RunSynchronously >> ignore)
|
||||
| _ -> ()
|
||||
|
||||
/// Delete a page
|
||||
let deletePage conn webLogId pageId =
|
||||
match tryFindPageById conn webLogId pageId false with
|
||||
| Some _ ->
|
||||
async {
|
||||
do! r.Table(Table.Page)
|
||||
.Get(pageId)
|
||||
.Delete()
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> (Async.RunSynchronously >> ignore)
|
||||
| _ -> ()
|
||||
225
src/MyWebLog.App/Data/Post.fs
Normal file
225
src/MyWebLog.App/Data/Post.fs
Normal file
@@ -0,0 +1,225 @@
|
||||
module MyWebLog.Data.RethinkDB.Post
|
||||
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Shorthand to select all published posts for a web log
|
||||
let private publishedPosts (webLogId : string) =
|
||||
r.Table(Table.Post)
|
||||
.GetAll(r.Array (webLogId, PostStatus.Published)).OptArg("index", "WebLogAndStatus")
|
||||
.Without("Revisions")
|
||||
// This allows us to count comments without retrieving them all
|
||||
.Merge(ReqlFunction1 (fun p ->
|
||||
upcast r.HashMap(
|
||||
"Comments", r.Table(Table.Comment)
|
||||
.GetAll(p.["id"]).OptArg("index", "PostId")
|
||||
.Pluck("id")
|
||||
.CoerceTo("array"))))
|
||||
|
||||
|
||||
/// Shorthand to sort posts by published date, slice for the given page, and return a list
|
||||
let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) =
|
||||
async {
|
||||
return!
|
||||
filter
|
||||
.OrderBy(r.Desc "PublishedOn")
|
||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||
.RunResultAsync<Post list> conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Shorthand to get a newer or older post
|
||||
let private adjacentPost conn (post : Post) (theFilter : ReqlExpr -> obj) (sort : obj) =
|
||||
async {
|
||||
let! post =
|
||||
(publishedPosts post.WebLogId)
|
||||
.Filter(theFilter)
|
||||
.OrderBy(sort)
|
||||
.Limit(1)
|
||||
.RunResultAsync<Post list> conn
|
||||
return List.tryHead post
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Find a newer post
|
||||
let private newerPost conn post theFilter = adjacentPost conn post theFilter <| r.Asc "PublishedOn"
|
||||
|
||||
/// Find an older post
|
||||
let private olderPost conn post theFilter = adjacentPost conn post theFilter <| r.Desc "PublishedOn"
|
||||
|
||||
/// Get a page of published posts
|
||||
let findPageOfPublishedPosts conn webLogId pageNbr nbrPerPage =
|
||||
publishedPosts webLogId
|
||||
|> toPostList conn pageNbr nbrPerPage
|
||||
|
||||
/// Get a page of published posts assigned to a given category
|
||||
let findPageOfCategorizedPosts conn webLogId (categoryId : string) pageNbr nbrPerPage =
|
||||
(publishedPosts webLogId)
|
||||
.Filter(ReqlFunction1 (fun p -> upcast p.["CategoryIds"].Contains categoryId))
|
||||
|> toPostList conn pageNbr nbrPerPage
|
||||
|
||||
/// Get a page of published posts tagged with a given tag
|
||||
let findPageOfTaggedPosts conn webLogId (tag : string) pageNbr nbrPerPage =
|
||||
(publishedPosts webLogId)
|
||||
.Filter(ReqlFunction1 (fun p -> upcast p.["Tags"].Contains tag))
|
||||
|> toPostList conn pageNbr nbrPerPage
|
||||
|
||||
/// Try to get the next newest post from the given post
|
||||
let tryFindNewerPost conn post = newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt post.PublishedOn)
|
||||
|
||||
/// Try to get the next newest post assigned to the given category
|
||||
let tryFindNewerCategorizedPost conn (categoryId : string) post =
|
||||
newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn)
|
||||
.And(p.["CategoryIds"].Contains categoryId))
|
||||
|
||||
/// Try to get the next newest tagged post from the given tagged post
|
||||
let tryFindNewerTaggedPost conn (tag : string) post =
|
||||
newerPost conn post (fun p -> upcast p.["PublishedOn"].Gt(post.PublishedOn).And(p.["Tags"].Contains tag))
|
||||
|
||||
/// Try to get the next oldest post from the given post
|
||||
let tryFindOlderPost conn post = olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt post.PublishedOn)
|
||||
|
||||
/// Try to get the next oldest post assigned to the given category
|
||||
let tryFindOlderCategorizedPost conn (categoryId : string) post =
|
||||
olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn)
|
||||
.And(p.["CategoryIds"].Contains categoryId))
|
||||
|
||||
/// Try to get the next oldest tagged post from the given tagged post
|
||||
let tryFindOlderTaggedPost conn (tag : string) post =
|
||||
olderPost conn post (fun p -> upcast p.["PublishedOn"].Lt(post.PublishedOn).And(p.["Tags"].Contains tag))
|
||||
|
||||
/// Get a page of all posts in all statuses
|
||||
let findPageOfAllPosts conn (webLogId : string) pageNbr nbrPerPage =
|
||||
// FIXME: sort unpublished posts by their last updated date
|
||||
async {
|
||||
// .orderBy(r.desc(r.branch(r.row("Status").eq("Published"), r.row("PublishedOn"), r.row("UpdatedOn"))))
|
||||
return!
|
||||
r.Table(Table.Post)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy(r.Desc (ReqlFunction1 (fun p ->
|
||||
upcast r.Branch (p.["Status"].Eq("Published"), p.["PublishedOn"], p.["UpdatedOn"]))))
|
||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||
.RunResultAsync<Post list> conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Try to find a post by its Id and web log Id
|
||||
let tryFindPost conn webLogId postId : Post option =
|
||||
async {
|
||||
let! p =
|
||||
r.Table(Table.Post)
|
||||
.Get(postId)
|
||||
.RunAtomAsync<Post> conn
|
||||
return
|
||||
match box p with
|
||||
| null -> None
|
||||
| pst ->
|
||||
let post : Post = unbox pst
|
||||
match post.WebLogId = webLogId with true -> Some post | _ -> None
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Try to find a post by its permalink
|
||||
let tryFindPostByPermalink conn webLogId permalink =
|
||||
async {
|
||||
let! post =
|
||||
r.Table(Table.Post)
|
||||
.GetAll(r.Array (webLogId, permalink)).OptArg("index", "Permalink")
|
||||
.Filter(ReqlFunction1 (fun p -> upcast p.["Status"].Eq PostStatus.Published))
|
||||
.Without("Revisions")
|
||||
.Merge(ReqlFunction1 (fun p ->
|
||||
upcast r.HashMap(
|
||||
"Categories", r.Table(Table.Category)
|
||||
.GetAll(r.Args p.["CategoryIds"])
|
||||
.Without("Children")
|
||||
.OrderBy("Name")
|
||||
.CoerceTo("array")).With(
|
||||
"Comments", r.Table(Table.Comment)
|
||||
.GetAll(p.["id"]).OptArg("index", "PostId")
|
||||
.OrderBy("PostedOn")
|
||||
.CoerceTo("array"))))
|
||||
.RunResultAsync<Post list> conn
|
||||
return List.tryHead post
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Try to find a post by its prior permalink
|
||||
let tryFindPostByPriorPermalink conn (webLogId : string) (permalink : string) =
|
||||
async {
|
||||
let! post =
|
||||
r.Table(Table.Post)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1 (fun p ->
|
||||
upcast p.["PriorPermalinks"].Contains(permalink).And(p.["Status"].Eq PostStatus.Published)))
|
||||
.Without("Revisions")
|
||||
.RunResultAsync<Post list> conn
|
||||
return List.tryHead post
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Get a set of posts for RSS
|
||||
let findFeedPosts conn webLogId nbr : (Post * User option) list =
|
||||
let tryFindUser userId =
|
||||
async {
|
||||
let! u =
|
||||
r.Table(Table.User)
|
||||
.Get(userId)
|
||||
.RunAtomAsync<User> conn
|
||||
return match box u with null -> None | user -> Some <| unbox user
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
(publishedPosts webLogId)
|
||||
.Merge(ReqlFunction1 (fun post ->
|
||||
upcast r.HashMap(
|
||||
"Categories", r.Table(Table.Category)
|
||||
.GetAll(r.Args post.["CategoryIds"])
|
||||
.OrderBy("Name")
|
||||
.Pluck("id", "Name")
|
||||
.CoerceTo("array"))))
|
||||
|> toPostList conn 1 nbr
|
||||
|> List.map (fun post -> post, tryFindUser post.AuthorId)
|
||||
|
||||
/// Add a post
|
||||
let addPost conn post =
|
||||
async {
|
||||
do! r.Table(Table.Post)
|
||||
.Insert(post)
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> (Async.RunSynchronously >> ignore)
|
||||
|
||||
/// Update a post
|
||||
let updatePost conn (post : Post) =
|
||||
async {
|
||||
do! r.Table(Table.Post)
|
||||
.Get(post.Id)
|
||||
.Replace( { post with Categories = []
|
||||
Comments = [] } )
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> (Async.RunSynchronously >> ignore)
|
||||
|
||||
/// Save a post
|
||||
let savePost conn (post : Post) =
|
||||
match post.Id with
|
||||
| "new" ->
|
||||
let newPost = { post with Id = string <| System.Guid.NewGuid() }
|
||||
async {
|
||||
do! r.Table(Table.Post)
|
||||
.Insert(newPost)
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
newPost.Id
|
||||
| _ ->
|
||||
async {
|
||||
do! r.Table(Table.Post)
|
||||
.Get(post.Id)
|
||||
.Replace( { post with Categories = []
|
||||
Comments = [] } )
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
post.Id
|
||||
48
src/MyWebLog.App/Data/RethinkMyWebLogData.fs
Normal file
48
src/MyWebLog.App/Data/RethinkMyWebLogData.fs
Normal file
@@ -0,0 +1,48 @@
|
||||
namespace MyWebLog.Data.RethinkDB
|
||||
|
||||
open MyWebLog.Data
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
/// RethinkDB implementation of myWebLog data persistence
|
||||
type RethinkMyWebLogData(conn : IConnection, cfg : DataConfig) =
|
||||
interface IMyWebLogData with
|
||||
member __.SetUp = fun () -> SetUp.startUpCheck cfg
|
||||
|
||||
member __.AllCategories = Category.getAllCategories conn
|
||||
member __.CategoryById = Category.tryFindCategory conn
|
||||
member __.CategoryBySlug = Category.tryFindCategoryBySlug conn
|
||||
member __.AddCategory = Category.addCategory conn
|
||||
member __.UpdateCategory = Category.updateCategory conn
|
||||
member __.UpdateChildren = Category.updateChildren conn
|
||||
member __.DeleteCategory = Category.deleteCategory conn
|
||||
|
||||
member __.PageById = Page.tryFindPageById conn
|
||||
member __.PageByPermalink = Page.tryFindPageByPermalink conn
|
||||
member __.AllPages = Page.findAllPages conn
|
||||
member __.AddPage = Page.addPage conn
|
||||
member __.UpdatePage = Page.updatePage conn
|
||||
member __.DeletePage = Page.deletePage conn
|
||||
|
||||
member __.PageOfPublishedPosts = Post.findPageOfPublishedPosts conn
|
||||
member __.PageOfCategorizedPosts = Post.findPageOfCategorizedPosts conn
|
||||
member __.PageOfTaggedPosts = Post.findPageOfTaggedPosts conn
|
||||
member __.NewerPost = Post.tryFindNewerPost conn
|
||||
member __.NewerCategorizedPost = Post.tryFindNewerCategorizedPost conn
|
||||
member __.NewerTaggedPost = Post.tryFindNewerTaggedPost conn
|
||||
member __.OlderPost = Post.tryFindOlderPost conn
|
||||
member __.OlderCategorizedPost = Post.tryFindOlderCategorizedPost conn
|
||||
member __.OlderTaggedPost = Post.tryFindOlderTaggedPost conn
|
||||
member __.PageOfAllPosts = Post.findPageOfAllPosts conn
|
||||
member __.PostById = Post.tryFindPost conn
|
||||
member __.PostByPermalink = Post.tryFindPostByPermalink conn
|
||||
member __.PostByPriorPermalink = Post.tryFindPostByPriorPermalink conn
|
||||
member __.FeedPosts = Post.findFeedPosts conn
|
||||
member __.AddPost = Post.addPost conn
|
||||
member __.UpdatePost = Post.updatePost conn
|
||||
|
||||
member __.LogOn = User.tryUserLogOn conn
|
||||
member __.SetUserPassword = User.setUserPassword conn
|
||||
|
||||
member __.WebLogByUrlBase = WebLog.tryFindWebLogByUrlBase conn
|
||||
member __.DashboardCounts = WebLog.findDashboardCounts conn
|
||||
|
||||
100
src/MyWebLog.App/Data/SetUp.fs
Normal file
100
src/MyWebLog.App/Data/SetUp.fs
Normal file
@@ -0,0 +1,100 @@
|
||||
module MyWebLog.Data.RethinkDB.SetUp
|
||||
|
||||
open RethinkDb.Driver.Ast
|
||||
open System
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
let private logStep step = Console.Out.WriteLine (sprintf "[myWebLog] %s" step)
|
||||
let private logStepStart text = Console.Out.Write (sprintf "[myWebLog] %s..." text)
|
||||
let private logStepDone () = Console.Out.WriteLine (" done.")
|
||||
|
||||
/// Ensure the myWebLog database exists
|
||||
let private checkDatabase (cfg : DataConfig) =
|
||||
async {
|
||||
logStep "|> Checking database"
|
||||
let! dbs = r.DbList().RunResultAsync<string list> cfg.Conn
|
||||
match List.contains cfg.Database dbs with
|
||||
| true -> ()
|
||||
| _ -> logStepStart (sprintf " %s database not found - creating" cfg.Database)
|
||||
do! r.DbCreate(cfg.Database).RunResultAsync cfg.Conn
|
||||
logStepDone ()
|
||||
}
|
||||
|
||||
|
||||
/// Ensure all required tables exist
|
||||
let private checkTables cfg =
|
||||
async {
|
||||
logStep "|> Checking tables"
|
||||
let! tables = r.Db(cfg.Database).TableList().RunResultAsync<string list> cfg.Conn
|
||||
[ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ]
|
||||
|> List.filter (fun tbl -> not (List.contains tbl tables))
|
||||
|> List.iter (fun tbl -> logStepStart (sprintf " Creating table %s" tbl)
|
||||
async { do! (r.TableCreate tbl).RunResultAsync cfg.Conn } |> Async.RunSynchronously
|
||||
logStepDone ())
|
||||
}
|
||||
|
||||
/// Shorthand to get the table
|
||||
let private tbl cfg table = r.Db(cfg.Database).Table table
|
||||
|
||||
/// Create the given index
|
||||
let private createIndex cfg table (index : string * (ReqlExpr -> obj) option) =
|
||||
async {
|
||||
let idxName, idxFunc = index
|
||||
logStepStart (sprintf """ Creating index "%s" on table %s""" idxName table)
|
||||
do! (match idxFunc with
|
||||
| Some f -> (tbl cfg table).IndexCreate(idxName, f)
|
||||
| None -> (tbl cfg table).IndexCreate(idxName))
|
||||
.RunResultAsync cfg.Conn
|
||||
logStepDone ()
|
||||
}
|
||||
|
||||
/// Ensure that the given indexes exist, and create them if required
|
||||
let private ensureIndexes cfg (indexes : (string * (string * (ReqlExpr -> obj) option) list) list) =
|
||||
let ensureForTable (tblName, idxs) =
|
||||
async {
|
||||
let! idx = (tbl cfg tblName).IndexList().RunResultAsync<string list> cfg.Conn
|
||||
idxs
|
||||
|> List.filter (fun (idxName, _) -> not (List.contains idxName idx))
|
||||
|> List.map (fun index -> createIndex cfg tblName index)
|
||||
|> List.iter Async.RunSynchronously
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
indexes
|
||||
|> List.iter ensureForTable
|
||||
|
||||
/// Create an index on web log Id and the given field
|
||||
let private webLogField (name : string) : (ReqlExpr -> obj) option =
|
||||
Some <| fun row -> upcast r.Array(row.["WebLogId"], row.[name])
|
||||
|
||||
/// Ensure all the required indexes exist
|
||||
let private checkIndexes cfg =
|
||||
logStep "|> Checking indexes"
|
||||
[ Table.Category, [ "WebLogId", None
|
||||
"Slug", webLogField "Slug"
|
||||
]
|
||||
Table.Comment, [ "PostId", None
|
||||
]
|
||||
Table.Page, [ "WebLogId", None
|
||||
"Permalink", webLogField "Permalink"
|
||||
]
|
||||
Table.Post, [ "WebLogId", None
|
||||
"WebLogAndStatus", webLogField "Status"
|
||||
"Permalink", webLogField "Permalink"
|
||||
]
|
||||
Table.User, [ "UserName", None
|
||||
]
|
||||
Table.WebLog, [ "UrlBase", None
|
||||
]
|
||||
]
|
||||
|> ensureIndexes cfg
|
||||
|
||||
/// Start up checks to ensure the database, tables, and indexes exist
|
||||
let startUpCheck cfg =
|
||||
async {
|
||||
logStep "Database Start Up Checks Starting"
|
||||
do! checkDatabase cfg
|
||||
do! checkTables cfg
|
||||
checkIndexes cfg
|
||||
logStep "Database Start Up Checks Complete"
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
21
src/MyWebLog.App/Data/Table.fs
Normal file
21
src/MyWebLog.App/Data/Table.fs
Normal file
@@ -0,0 +1,21 @@
|
||||
/// Constants for tables used in myWebLog
|
||||
[<RequireQualifiedAccess>]
|
||||
module MyWebLog.Data.RethinkDB.Table
|
||||
|
||||
/// The Category table
|
||||
let Category = "Category"
|
||||
|
||||
/// The Comment table
|
||||
let Comment = "Comment"
|
||||
|
||||
/// The Page table
|
||||
let Page = "Page"
|
||||
|
||||
/// The Post table
|
||||
let Post = "Post"
|
||||
|
||||
/// The WebLog table
|
||||
let WebLog = "WebLog"
|
||||
|
||||
/// The User table
|
||||
let User = "User"
|
||||
31
src/MyWebLog.App/Data/User.fs
Normal file
31
src/MyWebLog.App/Data/User.fs
Normal file
@@ -0,0 +1,31 @@
|
||||
module MyWebLog.Data.RethinkDB.User
|
||||
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Log on a user
|
||||
// NOTE: The significant length of a RethinkDB index is 238 - [PK size]; as we're storing 1,024 characters of password,
|
||||
// including it in an index does not get any performance gain, and would unnecessarily bloat the index. See
|
||||
// http://rethinkdb.com/docs/secondary-indexes/java/ for more information.
|
||||
let tryUserLogOn conn (email : string) (passwordHash : string) =
|
||||
async {
|
||||
let! user =
|
||||
r.Table(Table.User)
|
||||
.GetAll(email).OptArg("index", "UserName")
|
||||
.Filter(ReqlFunction1 (fun u -> upcast u.["PasswordHash"].Eq passwordHash))
|
||||
.RunResultAsync<User list> conn
|
||||
return user |> List.tryHead
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Set a user's password
|
||||
let setUserPassword conn (email : string) (passwordHash : string) =
|
||||
async {
|
||||
do! r.Table(Table.User)
|
||||
.GetAll(email).OptArg("index", "UserName")
|
||||
.Update(dict [ "PasswordHash", passwordHash ])
|
||||
.RunResultAsync conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
39
src/MyWebLog.App/Data/WebLog.fs
Normal file
39
src/MyWebLog.App/Data/WebLog.fs
Normal file
@@ -0,0 +1,39 @@
|
||||
module MyWebLog.Data.RethinkDB.WebLog
|
||||
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Detemine the web log by the URL base
|
||||
let tryFindWebLogByUrlBase conn (urlBase : string) =
|
||||
async {
|
||||
let! cursor =
|
||||
r.Table(Table.WebLog)
|
||||
.GetAll(urlBase).OptArg("index", "UrlBase")
|
||||
.Merge(ReqlFunction1 (fun w ->
|
||||
upcast r.HashMap(
|
||||
"PageList", r.Table(Table.Page)
|
||||
.GetAll(w.G("id")).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1 (fun pg -> upcast pg.["ShowInPageList"].Eq true))
|
||||
.OrderBy("Title")
|
||||
.Pluck("Title", "Permalink")
|
||||
.CoerceTo("array"))))
|
||||
.RunCursorAsync<WebLog> conn
|
||||
return cursor |> Seq.tryHead
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
/// Get counts for the admin dashboard
|
||||
let findDashboardCounts conn (webLogId : string) =
|
||||
async {
|
||||
return!
|
||||
r.Expr(
|
||||
r.HashMap(
|
||||
"Pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "WebLogId").Count()).With(
|
||||
"Posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "WebLogId").Count()).With(
|
||||
"Categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.RunResultAsync<DashboardCounts> conn
|
||||
}
|
||||
|> Async.RunSynchronously
|
||||
|
||||
301
src/MyWebLog.App/Entities/Entities.fs
Normal file
301
src/MyWebLog.App/Entities/Entities.fs
Normal file
@@ -0,0 +1,301 @@
|
||||
namespace MyWebLog.Entities
|
||||
|
||||
open Newtonsoft.Json
|
||||
|
||||
// --- Constants ---
|
||||
|
||||
/// Constants to use for revision source language
|
||||
[<RequireQualifiedAccess>]
|
||||
module RevisionSource =
|
||||
[<Literal>]
|
||||
let Markdown = "markdown"
|
||||
[<Literal>]
|
||||
let HTML = "html"
|
||||
|
||||
/// Constants to use for authorization levels
|
||||
[<RequireQualifiedAccess>]
|
||||
module AuthorizationLevel =
|
||||
[<Literal>]
|
||||
let Administrator = "Administrator"
|
||||
[<Literal>]
|
||||
let User = "User"
|
||||
|
||||
/// Constants to use for post statuses
|
||||
[<RequireQualifiedAccess>]
|
||||
module PostStatus =
|
||||
[<Literal>]
|
||||
let Draft = "Draft"
|
||||
[<Literal>]
|
||||
let Published = "Published"
|
||||
|
||||
/// Constants to use for comment statuses
|
||||
[<RequireQualifiedAccess>]
|
||||
module CommentStatus =
|
||||
[<Literal>]
|
||||
let Approved = "Approved"
|
||||
[<Literal>]
|
||||
let Pending = "Pending"
|
||||
[<Literal>]
|
||||
let Spam = "Spam"
|
||||
|
||||
// --- Entities ---
|
||||
|
||||
/// A revision of a post or page
|
||||
type Revision =
|
||||
{ /// The instant which this revision was saved
|
||||
AsOf : int64
|
||||
/// The source language
|
||||
SourceType : string
|
||||
/// The text
|
||||
Text : string }
|
||||
with
|
||||
/// An empty revision
|
||||
static member Empty =
|
||||
{ AsOf = int64 0
|
||||
SourceType = RevisionSource.HTML
|
||||
Text = "" }
|
||||
|
||||
/// A page with static content
|
||||
type Page =
|
||||
{ /// The Id
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The Id of the web log to which this page belongs
|
||||
WebLogId : string
|
||||
/// The Id of the author of this page
|
||||
AuthorId : string
|
||||
/// The title of the page
|
||||
Title : string
|
||||
/// The link at which this page is displayed
|
||||
Permalink : string
|
||||
/// The instant this page was published
|
||||
PublishedOn : int64
|
||||
/// The instant this page was last updated
|
||||
UpdatedOn : int64
|
||||
/// Whether this page shows as part of the web log's navigation
|
||||
ShowInPageList : bool
|
||||
/// The current text of the page
|
||||
Text : string
|
||||
/// Revisions of this page
|
||||
Revisions : Revision list }
|
||||
with
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
WebLogId = ""
|
||||
AuthorId = ""
|
||||
Title = ""
|
||||
Permalink = ""
|
||||
PublishedOn = int64 0
|
||||
UpdatedOn = int64 0
|
||||
ShowInPageList = false
|
||||
Text = ""
|
||||
Revisions = []
|
||||
}
|
||||
|
||||
|
||||
/// An entry in the list of pages displayed as part of the web log (derived via query)
|
||||
type PageListEntry =
|
||||
{ Permalink : string
|
||||
Title : string }
|
||||
|
||||
/// A web log
|
||||
type WebLog =
|
||||
{ /// The Id
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The name
|
||||
Name : string
|
||||
/// The subtitle
|
||||
Subtitle : string option
|
||||
/// The default page ("posts" or a page Id)
|
||||
DefaultPage : string
|
||||
/// The path of the theme (within /views/themes)
|
||||
ThemePath : string
|
||||
/// The URL base
|
||||
UrlBase : string
|
||||
/// The time zone in which dates/times should be displayed
|
||||
TimeZone : string
|
||||
/// A list of pages to be rendered as part of the site navigation (not stored)
|
||||
PageList : PageListEntry list }
|
||||
with
|
||||
/// An empty web log
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
Name = ""
|
||||
Subtitle = None
|
||||
DefaultPage = ""
|
||||
ThemePath = "default"
|
||||
UrlBase = ""
|
||||
TimeZone = "America/New_York"
|
||||
PageList = [] }
|
||||
|
||||
|
||||
/// An authorization between a user and a web log
|
||||
type Authorization =
|
||||
{ /// The Id of the web log to which this authorization grants access
|
||||
WebLogId : string
|
||||
/// The level of access granted by this authorization
|
||||
Level : string }
|
||||
|
||||
|
||||
/// A user of myWebLog
|
||||
type User =
|
||||
{ /// The Id
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The user name (e-mail address)
|
||||
UserName : string
|
||||
/// The first name
|
||||
FirstName : string
|
||||
/// The last name
|
||||
LastName : string
|
||||
/// The user's preferred name
|
||||
PreferredName : string
|
||||
/// The hash of the user's password
|
||||
PasswordHash : string
|
||||
/// The URL of the user's personal site
|
||||
Url : string option
|
||||
/// The user's authorizations
|
||||
Authorizations : Authorization list }
|
||||
with
|
||||
/// An empty user
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
UserName = ""
|
||||
FirstName = ""
|
||||
LastName = ""
|
||||
PreferredName = ""
|
||||
PasswordHash = ""
|
||||
Url = None
|
||||
Authorizations = [] }
|
||||
|
||||
/// Claims for this user
|
||||
[<JsonIgnore>]
|
||||
member this.Claims = this.Authorizations
|
||||
|> List.map (fun auth -> sprintf "%s|%s" auth.WebLogId auth.Level)
|
||||
|
||||
|
||||
/// A category to which posts may be assigned
|
||||
type Category =
|
||||
{ /// The Id
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The Id of the web log to which this category belongs
|
||||
WebLogId : string
|
||||
/// The displayed name
|
||||
Name : string
|
||||
/// The slug (used in category URLs)
|
||||
Slug : string
|
||||
/// A longer description of the category
|
||||
Description : string option
|
||||
/// The parent Id of this category (if a subcategory)
|
||||
ParentId : string option
|
||||
/// The categories for which this category is the parent
|
||||
Children : string list }
|
||||
with
|
||||
/// An empty category
|
||||
static member Empty =
|
||||
{ Id = "new"
|
||||
WebLogId = ""
|
||||
Name = ""
|
||||
Slug = ""
|
||||
Description = None
|
||||
ParentId = None
|
||||
Children = [] }
|
||||
|
||||
|
||||
/// A comment (applies to a post)
|
||||
type Comment =
|
||||
{ /// The Id
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The Id of the post to which this comment applies
|
||||
PostId : string
|
||||
/// The Id of the comment to which this comment is a reply
|
||||
InReplyToId : string option
|
||||
/// The name of the commentor
|
||||
Name : string
|
||||
/// The e-mail address of the commentor
|
||||
Email : string
|
||||
/// The URL of the commentor's personal website
|
||||
Url : string option
|
||||
/// The status of the comment
|
||||
Status : string
|
||||
/// The instant the comment was posted
|
||||
PostedOn : int64
|
||||
/// The text of the comment
|
||||
Text : string }
|
||||
with
|
||||
static member Empty =
|
||||
{ Id = ""
|
||||
PostId = ""
|
||||
InReplyToId = None
|
||||
Name = ""
|
||||
Email = ""
|
||||
Url = None
|
||||
Status = CommentStatus.Pending
|
||||
PostedOn = int64 0
|
||||
Text = "" }
|
||||
|
||||
|
||||
/// A post
|
||||
type Post =
|
||||
{ /// The Id
|
||||
[<JsonProperty("id")>]
|
||||
Id : string
|
||||
/// The Id of the web log to which this post belongs
|
||||
WebLogId : string
|
||||
/// The Id of the author of this post
|
||||
AuthorId : string
|
||||
/// The status
|
||||
Status : string
|
||||
/// The title
|
||||
Title : string
|
||||
/// The link at which the post resides
|
||||
Permalink : string
|
||||
/// The instant on which the post was originally published
|
||||
PublishedOn : int64
|
||||
/// The instant on which the post was last updated
|
||||
UpdatedOn : int64
|
||||
/// The text of the post
|
||||
Text : string
|
||||
/// The Ids of the categories to which this is assigned
|
||||
CategoryIds : string list
|
||||
/// The tags for the post
|
||||
Tags : string list
|
||||
/// The permalinks at which this post may have once resided
|
||||
PriorPermalinks : string list
|
||||
/// Revisions of this post
|
||||
Revisions : Revision list
|
||||
/// The categories to which this is assigned (not stored in database)
|
||||
Categories : Category list
|
||||
/// The comments (not stored in database)
|
||||
Comments : Comment list }
|
||||
with
|
||||
static member Empty =
|
||||
{ Id = "new"
|
||||
WebLogId = ""
|
||||
AuthorId = ""
|
||||
Status = PostStatus.Draft
|
||||
Title = ""
|
||||
Permalink = ""
|
||||
PublishedOn = int64 0
|
||||
UpdatedOn = int64 0
|
||||
Text = ""
|
||||
CategoryIds = []
|
||||
Tags = []
|
||||
PriorPermalinks = []
|
||||
Revisions = []
|
||||
Categories = []
|
||||
Comments = [] }
|
||||
|
||||
// --- UI Support ---
|
||||
|
||||
/// Counts of items displayed on the admin dashboard
|
||||
type DashboardCounts =
|
||||
{ /// The number of pages for the web log
|
||||
Pages : int
|
||||
/// The number of pages for the web log
|
||||
Posts : int
|
||||
/// The number of categories for the web log
|
||||
Categories : int }
|
||||
117
src/MyWebLog.App/Entities/IMyWebLogData.fs
Normal file
117
src/MyWebLog.App/Entities/IMyWebLogData.fs
Normal file
@@ -0,0 +1,117 @@
|
||||
namespace MyWebLog.Data
|
||||
|
||||
open MyWebLog.Entities
|
||||
|
||||
/// Interface required to provide data to myWebLog's logic layer
|
||||
type IMyWebLogData =
|
||||
/// Function to set up the data store
|
||||
abstract SetUp : (unit -> unit)
|
||||
|
||||
// --- Category ---
|
||||
|
||||
/// Get all categories for a web log
|
||||
abstract AllCategories : (string -> Category list)
|
||||
|
||||
/// Try to find a category by its Id and web log Id (web log, category Ids)
|
||||
abstract CategoryById : (string -> string -> Category option)
|
||||
|
||||
/// Try to find a category by its slug (web log Id, slug)
|
||||
abstract CategoryBySlug : (string -> string -> Category option)
|
||||
|
||||
/// Add a category
|
||||
abstract AddCategory : (Category -> unit)
|
||||
|
||||
/// Update a category
|
||||
abstract UpdateCategory : (Category -> unit)
|
||||
|
||||
/// Update a category's children
|
||||
abstract UpdateChildren : (string -> string -> string list -> unit)
|
||||
|
||||
/// Delete a Category
|
||||
abstract DeleteCategory : (Category -> unit)
|
||||
|
||||
// --- Page ---
|
||||
|
||||
/// Try to find a page by its Id and web log Id (web log, page Ids), choosing whether to include revisions
|
||||
abstract PageById : (string -> string -> bool -> Page option)
|
||||
|
||||
/// Try to find a page by its permalink and web log Id (web log Id, permalink)
|
||||
abstract PageByPermalink : (string -> string -> Page option)
|
||||
|
||||
/// Get all pages for a web log
|
||||
abstract AllPages : (string -> Page list)
|
||||
|
||||
/// Add a page
|
||||
abstract AddPage : (Page -> unit)
|
||||
|
||||
/// Update a page
|
||||
abstract UpdatePage : (Page -> unit)
|
||||
|
||||
/// Delete a page by its Id and web log Id (web log, page Ids)
|
||||
abstract DeletePage : (string -> string -> unit)
|
||||
|
||||
// --- Post ---
|
||||
|
||||
/// Find a page of published posts for the given web log (web log Id, page #, # per page)
|
||||
abstract PageOfPublishedPosts : (string -> int -> int -> Post list)
|
||||
|
||||
/// Find a page of published posts within a given category (web log Id, cat Id, page #, # per page)
|
||||
abstract PageOfCategorizedPosts : (string -> string -> int -> int -> Post list)
|
||||
|
||||
/// Find a page of published posts tagged with a given tag (web log Id, tag, page #, # per page)
|
||||
abstract PageOfTaggedPosts : (string -> string -> int -> int -> Post list)
|
||||
|
||||
/// Try to find the next newer published post for the given post
|
||||
abstract NewerPost : (Post -> Post option)
|
||||
|
||||
/// Try to find the next newer published post within a given category
|
||||
abstract NewerCategorizedPost : (string -> Post -> Post option)
|
||||
|
||||
/// Try to find the next newer published post tagged with a given tag
|
||||
abstract NewerTaggedPost : (string -> Post -> Post option)
|
||||
|
||||
/// Try to find the next older published post for the given post
|
||||
abstract OlderPost : (Post -> Post option)
|
||||
|
||||
/// Try to find the next older published post within a given category
|
||||
abstract OlderCategorizedPost : (string -> Post -> Post option)
|
||||
|
||||
/// Try to find the next older published post tagged with a given tag
|
||||
abstract OlderTaggedPost : (string -> Post -> Post option)
|
||||
|
||||
/// Find a page of all posts for the given web log (web log Id, page #, # per page)
|
||||
abstract PageOfAllPosts : (string -> int -> int -> Post list)
|
||||
|
||||
/// Try to find a post by its Id and web log Id (web log, post Ids)
|
||||
abstract PostById : (string -> string -> Post option)
|
||||
|
||||
/// Try to find a post by its permalink (web log Id, permalink)
|
||||
abstract PostByPermalink : (string -> string -> Post option)
|
||||
|
||||
/// Try to find a post by a prior permalink (web log Id, permalink)
|
||||
abstract PostByPriorPermalink : (string -> string -> Post option)
|
||||
|
||||
/// Get posts for the RSS feed for the given web log and number of posts
|
||||
abstract FeedPosts : (string -> int -> (Post * User option) list)
|
||||
|
||||
/// Add a post
|
||||
abstract AddPost : (Post -> unit)
|
||||
|
||||
/// Update a post
|
||||
abstract UpdatePost : (Post -> unit)
|
||||
|
||||
// --- User ---
|
||||
|
||||
/// Attempt to log on a user
|
||||
abstract LogOn : (string -> string -> User option)
|
||||
|
||||
/// Set a user's password (e-mail, password hash)
|
||||
abstract SetUserPassword : (string -> string -> unit)
|
||||
|
||||
// --- WebLog ---
|
||||
|
||||
/// Get a web log by its URL base
|
||||
abstract WebLogByUrlBase : (string -> WebLog option)
|
||||
|
||||
/// Get dashboard counts for a web log
|
||||
abstract DashboardCounts : (string -> DashboardCounts)
|
||||
56
src/MyWebLog.App/Logic/Category.fs
Normal file
56
src/MyWebLog.App/Logic/Category.fs
Normal file
@@ -0,0 +1,56 @@
|
||||
module MyWebLog.Logic.Category
|
||||
|
||||
open MyWebLog.Data
|
||||
open MyWebLog.Entities
|
||||
|
||||
/// Sort categories by their name, with their children sorted below them, including an indent level
|
||||
let sortCategories categories =
|
||||
let rec getChildren (cat : Category) indent =
|
||||
seq {
|
||||
yield cat, indent
|
||||
for child in categories |> List.filter (fun c -> c.ParentId = Some cat.Id) do
|
||||
yield! getChildren child (indent + 1)
|
||||
}
|
||||
categories
|
||||
|> List.filter (fun c -> c.ParentId.IsNone)
|
||||
|> List.map (fun c -> getChildren c 0)
|
||||
|> Seq.collect id
|
||||
|> Seq.toList
|
||||
|
||||
/// Find all categories for a given web log
|
||||
let findAllCategories (data : IMyWebLogData) webLogId =
|
||||
data.AllCategories webLogId
|
||||
|> sortCategories
|
||||
|
||||
/// Try to find a category for a given web log Id and category Id
|
||||
let tryFindCategory (data : IMyWebLogData) webLogId catId = data.CategoryById webLogId catId
|
||||
|
||||
/// Try to find a category by its slug for a given web log
|
||||
let tryFindCategoryBySlug (data : IMyWebLogData) webLogId slug = data.CategoryBySlug webLogId slug
|
||||
|
||||
/// Save a category
|
||||
let saveCategory (data : IMyWebLogData) (cat : Category) =
|
||||
match cat.Id with
|
||||
| "new" -> let newCat = { cat with Id = string <| System.Guid.NewGuid() }
|
||||
data.AddCategory newCat
|
||||
newCat.Id
|
||||
| _ -> data.UpdateCategory cat
|
||||
cat.Id
|
||||
|
||||
/// Remove a category from its parent
|
||||
let removeCategoryFromParent (data : IMyWebLogData) webLogId parentId catId =
|
||||
match tryFindCategory data webLogId parentId with
|
||||
| Some parent -> parent.Children
|
||||
|> List.filter (fun childId -> childId <> catId)
|
||||
|> data.UpdateChildren webLogId parentId
|
||||
| None -> ()
|
||||
|
||||
/// Add a category to a given parent
|
||||
let addCategoryToParent (data : IMyWebLogData) webLogId parentId catId =
|
||||
match tryFindCategory data webLogId parentId with
|
||||
| Some parent -> catId :: parent.Children
|
||||
|> data.UpdateChildren webLogId parentId
|
||||
| None -> ()
|
||||
|
||||
/// Delete a category
|
||||
let deleteCategory (data : IMyWebLogData) cat = data.DeleteCategory cat
|
||||
29
src/MyWebLog.App/Logic/Page.fs
Normal file
29
src/MyWebLog.App/Logic/Page.fs
Normal file
@@ -0,0 +1,29 @@
|
||||
/// Logic for manipulating <see cref="Page" /> entities
|
||||
module MyWebLog.Logic.Page
|
||||
|
||||
open MyWebLog.Data
|
||||
open MyWebLog.Entities
|
||||
|
||||
/// Find a page by its Id and web log Id
|
||||
let tryFindPage (data : IMyWebLogData) webLogId pageId = data.PageById webLogId pageId true
|
||||
|
||||
/// Find a page by its Id and web log Id, without the revision list
|
||||
let tryFindPageWithoutRevisions (data : IMyWebLogData) webLogId pageId = data.PageById webLogId pageId false
|
||||
|
||||
/// Find a page by its permalink
|
||||
let tryFindPageByPermalink (data : IMyWebLogData) webLogId permalink = data.PageByPermalink webLogId permalink
|
||||
|
||||
/// Find a list of all pages (excludes text and revisions)
|
||||
let findAllPages (data : IMyWebLogData) webLogId = data.AllPages webLogId
|
||||
|
||||
/// Save a page
|
||||
let savePage (data : IMyWebLogData) (page : Page) =
|
||||
match page.Id with
|
||||
| "new" -> let newPg = { page with Id = string <| System.Guid.NewGuid () }
|
||||
data.AddPage newPg
|
||||
newPg.Id
|
||||
| _ -> data.UpdatePage page
|
||||
page.Id
|
||||
|
||||
/// Delete a page
|
||||
let deletePage (data : IMyWebLogData) webLogId pageId = data.DeletePage webLogId pageId
|
||||
60
src/MyWebLog.App/Logic/Post.fs
Normal file
60
src/MyWebLog.App/Logic/Post.fs
Normal file
@@ -0,0 +1,60 @@
|
||||
/// Logic for manipulating <see cref="Post" /> entities
|
||||
module MyWebLog.Logic.Post
|
||||
|
||||
open MyWebLog.Data
|
||||
open MyWebLog.Entities
|
||||
|
||||
/// Find a page of published posts
|
||||
let findPageOfPublishedPosts (data : IMyWebLogData) webLogId pageNbr nbrPerPage =
|
||||
data.PageOfPublishedPosts webLogId pageNbr nbrPerPage
|
||||
|
||||
/// Find a pages of published posts in a given category
|
||||
let findPageOfCategorizedPosts (data : IMyWebLogData) webLogId catId pageNbr nbrPerPage =
|
||||
data.PageOfCategorizedPosts webLogId catId pageNbr nbrPerPage
|
||||
|
||||
/// Find a page of published posts tagged with a given tag
|
||||
let findPageOfTaggedPosts (data : IMyWebLogData) webLogId tag pageNbr nbrPerPage =
|
||||
data.PageOfTaggedPosts webLogId tag pageNbr nbrPerPage
|
||||
|
||||
/// Find the next newer published post for the given post
|
||||
let tryFindNewerPost (data : IMyWebLogData) post = data.NewerPost post
|
||||
|
||||
/// Find the next newer published post in a given category for the given post
|
||||
let tryFindNewerCategorizedPost (data : IMyWebLogData) catId post = data.NewerCategorizedPost catId post
|
||||
|
||||
/// Find the next newer published post tagged with a given tag for the given post
|
||||
let tryFindNewerTaggedPost (data : IMyWebLogData) tag post = data.NewerTaggedPost tag post
|
||||
|
||||
/// Find the next older published post for the given post
|
||||
let tryFindOlderPost (data : IMyWebLogData) post = data.OlderPost post
|
||||
|
||||
/// Find the next older published post in a given category for the given post
|
||||
let tryFindOlderCategorizedPost (data : IMyWebLogData) catId post = data.OlderCategorizedPost catId post
|
||||
|
||||
/// Find the next older published post tagged with a given tag for the given post
|
||||
let tryFindOlderTaggedPost (data : IMyWebLogData) tag post = data.OlderTaggedPost tag post
|
||||
|
||||
/// Find a page of all posts for a web log
|
||||
let findPageOfAllPosts (data : IMyWebLogData) webLogId pageNbr nbrPerPage =
|
||||
data.PageOfAllPosts webLogId pageNbr nbrPerPage
|
||||
|
||||
/// Try to find a post by its Id
|
||||
let tryFindPost (data : IMyWebLogData) webLogId postId = data.PostById webLogId postId
|
||||
|
||||
/// Try to find a post by its permalink
|
||||
let tryFindPostByPermalink (data : IMyWebLogData) webLogId permalink = data.PostByPermalink webLogId permalink
|
||||
|
||||
/// Try to find a post by its prior permalink
|
||||
let tryFindPostByPriorPermalink (data : IMyWebLogData) webLogId permalink = data.PostByPriorPermalink webLogId permalink
|
||||
|
||||
/// Find posts for the RSS feed
|
||||
let findFeedPosts (data : IMyWebLogData) webLogId nbrOfPosts = data.FeedPosts webLogId nbrOfPosts
|
||||
|
||||
/// Save a post
|
||||
let savePost (data : IMyWebLogData) (post : Post) =
|
||||
match post.Id with
|
||||
| "new" -> let newPost = { post with Id = string <| System.Guid.NewGuid() }
|
||||
data.AddPost newPost
|
||||
newPost.Id
|
||||
| _ -> data.UpdatePost post
|
||||
post.Id
|
||||
9
src/MyWebLog.App/Logic/User.fs
Normal file
9
src/MyWebLog.App/Logic/User.fs
Normal file
@@ -0,0 +1,9 @@
|
||||
/// Logic for manipulating <see cref="User" /> entities
|
||||
module MyWebLog.Logic.User
|
||||
|
||||
open MyWebLog.Data
|
||||
|
||||
/// Try to log on a user
|
||||
let tryUserLogOn (data : IMyWebLogData) email passwordHash = data.LogOn email passwordHash
|
||||
|
||||
let setUserPassword (data : IMyWebLogData) = data.SetUserPassword
|
||||
11
src/MyWebLog.App/Logic/WebLog.fs
Normal file
11
src/MyWebLog.App/Logic/WebLog.fs
Normal file
@@ -0,0 +1,11 @@
|
||||
/// Logic for manipulating <see cref="WebLog" /> entities
|
||||
module MyWebLog.Logic.WebLog
|
||||
|
||||
open MyWebLog.Data
|
||||
open MyWebLog.Entities
|
||||
|
||||
/// Find a web log by its URL base
|
||||
let tryFindWebLogByUrlBase (data : IMyWebLogData) urlBase = data.WebLogByUrlBase urlBase
|
||||
|
||||
/// Find the counts for the admin dashboard
|
||||
let findDashboardCounts (data : IMyWebLogData) webLogId = data.DashboardCounts webLogId
|
||||
@@ -42,7 +42,7 @@ type PostModule (data : IMyWebLogData, clock : IClock) as this =
|
||||
let elem name (valu : string) = XElement (xn name, valu)
|
||||
let elems =
|
||||
items
|
||||
|> List.sortBy (fun i -> i.ReleaseDate)
|
||||
|> List.sortByDescending (fun i -> i.ReleaseDate)
|
||||
|> List.map (fun i ->
|
||||
XElement (
|
||||
xn "item",
|
||||
42
src/MyWebLog.App/Strings.fs
Normal file
42
src/MyWebLog.App/Strings.fs
Normal file
@@ -0,0 +1,42 @@
|
||||
module MyWebLog.Resources.Strings
|
||||
|
||||
open MyWebLog
|
||||
open Newtonsoft.Json
|
||||
open System.Collections.Generic
|
||||
open System.Reflection
|
||||
|
||||
/// The locales we'll try to load
|
||||
let private supportedLocales = [ "en-US" ]
|
||||
|
||||
/// The fallback locale, if a key is not found in a non-default locale
|
||||
let private fallbackLocale = "en-US"
|
||||
|
||||
/// Get an embedded JSON file as a string
|
||||
let private getEmbedded locale =
|
||||
use rdr =
|
||||
new System.IO.StreamReader
|
||||
(typeof<AppConfig>.GetTypeInfo().Assembly.GetManifestResourceStream(sprintf "MyWebLog.App.%s.json" locale))
|
||||
rdr.ReadToEnd()
|
||||
|
||||
/// The dictionary of localized strings
|
||||
let private strings =
|
||||
supportedLocales
|
||||
|> List.map (fun loc -> loc, JsonConvert.DeserializeObject<Dictionary<string, string>>(getEmbedded loc))
|
||||
|> dict
|
||||
|
||||
/// Get a key from the resources file for the given locale
|
||||
let getForLocale locale key =
|
||||
let getString thisLocale =
|
||||
match strings.ContainsKey thisLocale with
|
||||
| true -> match strings.[thisLocale].ContainsKey key with
|
||||
| true -> Some strings.[thisLocale].[key]
|
||||
| _ -> None
|
||||
| _ -> None
|
||||
match getString locale with
|
||||
| Some xlat -> Some xlat
|
||||
| _ when locale <> fallbackLocale -> getString fallbackLocale
|
||||
| _ -> None
|
||||
|> function Some xlat -> xlat | _ -> sprintf "%s.%s" locale key
|
||||
|
||||
/// Translate the key for the current locale
|
||||
let get key = getForLocale System.Globalization.CultureInfo.CurrentCulture.Name key
|
||||
83
src/MyWebLog.App/en-US.json
Normal file
83
src/MyWebLog.App/en-US.json
Normal file
@@ -0,0 +1,83 @@
|
||||
{
|
||||
"Action": "Action",
|
||||
"Added": "Added",
|
||||
"AddNew": "Add New",
|
||||
"AddNewCategory": "Add New Category",
|
||||
"AddNewPage": "Add New Page",
|
||||
"AddNewPost": "Add New Post",
|
||||
"Admin": "Admin",
|
||||
"AndPublished": " and Published",
|
||||
"andXMore": "and {0} more...",
|
||||
"at": "at",
|
||||
"BackToCategoryList": "Back to Category List",
|
||||
"BackToPageList": "Back to Page List",
|
||||
"BackToPostList": "Back to Post List",
|
||||
"Categories": "Categories",
|
||||
"Category": "Category",
|
||||
"CategoryDeleteWarning": "Are you sure you wish to delete the category",
|
||||
"Close": "Close",
|
||||
"Comments": "Comments",
|
||||
"Dashboard": "Dashboard",
|
||||
"Date": "Date",
|
||||
"Delete": "Delete",
|
||||
"Description": "Description",
|
||||
"Edit": "Edit",
|
||||
"EditCategory": "Edit Category",
|
||||
"EditPage": "Edit Page",
|
||||
"EditPost": "Edit Post",
|
||||
"EmailAddress": "E-mail Address",
|
||||
"ErrBadAppConfig": "Could not convert config.json to myWebLog configuration",
|
||||
"ErrBadLogOnAttempt": "Invalid e-mail address or password",
|
||||
"ErrDataConfig": "Could not convert data-config.json to RethinkDB connection",
|
||||
"ErrNotConfigured": "is not properly configured for myWebLog",
|
||||
"Error": "Error",
|
||||
"LastUpdated": "Last Updated",
|
||||
"LastUpdatedDate": "Last Updated Date",
|
||||
"ListAll": "List All",
|
||||
"LoadedIn": "Loaded in",
|
||||
"LogOff": "Log Off",
|
||||
"LogOn": "Log On",
|
||||
"MsgCategoryDeleted": "Deleted category {0} successfully",
|
||||
"MsgCategoryEditSuccess": "{0} category successfully",
|
||||
"MsgLogOffSuccess": "Log off successful | Have a nice day!",
|
||||
"MsgLogOnSuccess": "Log on successful | Welcome to myWebLog!",
|
||||
"MsgPageDeleted": "Deleted page successfully",
|
||||
"MsgPageEditSuccess": "{0} page successfully",
|
||||
"MsgPostEditSuccess": "{0}{1} post successfully",
|
||||
"Name": "Name",
|
||||
"NewerPosts": "Newer Posts",
|
||||
"NextPost": "Next Post",
|
||||
"NoComments": "No Comments",
|
||||
"NoParent": "No Parent",
|
||||
"OlderPosts": "Older Posts",
|
||||
"OneComment": "1 Comment",
|
||||
"PageDeleteWarning": "Are you sure you wish to delete the page",
|
||||
"PageDetails": "Page Details",
|
||||
"PageHash": "Page #",
|
||||
"Pages": "Pages",
|
||||
"ParentCategory": "Parent Category",
|
||||
"Password": "Password",
|
||||
"Permalink": "Permalink",
|
||||
"PermanentLinkTo": "Permanent Link to",
|
||||
"PostDetails": "Post Details",
|
||||
"Posts": "Posts",
|
||||
"PostsTagged": "Posts Tagged",
|
||||
"PostStatus": "Post Status",
|
||||
"PoweredBy": "Powered by",
|
||||
"PreviousPost": "Previous Post",
|
||||
"PublishedDate": "Published Date",
|
||||
"PublishThisPost": "Publish This Post",
|
||||
"Save": "Save",
|
||||
"Seconds": "Seconds",
|
||||
"ShowInPageList": "Show in Page List",
|
||||
"Slug": "Slug",
|
||||
"startingWith": "starting with",
|
||||
"Status": "Status",
|
||||
"Tags": "Tags",
|
||||
"Time": "Time",
|
||||
"Title": "Title",
|
||||
"Updated": "Updated",
|
||||
"View": "View",
|
||||
"Warning": "Warning",
|
||||
"XComments": "{0} Comments"
|
||||
}
|
||||
@@ -4,29 +4,48 @@
|
||||
"compile": {
|
||||
"includeFiles": [
|
||||
"AssemblyInfo.fs",
|
||||
"Entities/Entities.fs",
|
||||
"Entities/IMyWebLogData.fs",
|
||||
"Data/Extensions.fs",
|
||||
"Data/Table.fs",
|
||||
"Data/DataConfig.fs",
|
||||
"Data/Category.fs",
|
||||
"Data/Page.fs",
|
||||
"Data/Post.fs",
|
||||
"Data/User.fs",
|
||||
"Data/WebLog.fs",
|
||||
"Data/SetUp.fs",
|
||||
"Data/RethinkMyWebLogData.fs",
|
||||
"Logic/Category.fs",
|
||||
"Logic/Page.fs",
|
||||
"Logic/Post.fs",
|
||||
"Logic/User.fs",
|
||||
"Logic/WebLog.fs",
|
||||
"Keys.fs",
|
||||
"AppConfig.fs",
|
||||
"Strings.fs",
|
||||
"ViewModels.fs",
|
||||
"ModuleExtensions.fs",
|
||||
"AdminModule.fs",
|
||||
"CategoryModule.fs",
|
||||
"PageModule.fs",
|
||||
"PostModule.fs",
|
||||
"UserModule.fs",
|
||||
"Modules/ModuleExtensions.fs",
|
||||
"Modules/AdminModule.fs",
|
||||
"Modules/CategoryModule.fs",
|
||||
"Modules/PageModule.fs",
|
||||
"Modules/PostModule.fs",
|
||||
"Modules/UserModule.fs",
|
||||
"App.fs"
|
||||
]
|
||||
},
|
||||
"embed": {
|
||||
"include": [ "en-US.json" ]
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"MyWebLog.Data.RethinkDB": "0.9.2",
|
||||
"MyWebLog.Entities": "0.9.2",
|
||||
"MyWebLog.Logic": "0.9.2",
|
||||
"MyWebLog.Resources": "0.9.2",
|
||||
"Nancy": "2.0.0-barneyrubble",
|
||||
"Nancy.Authentication.Forms": "2.0.0-barneyrubble",
|
||||
"Nancy.Session.Persistable": "0.9.1-pre",
|
||||
"Nancy.Session.RethinkDB": "0.9.1-pre",
|
||||
"Newtonsoft.Json": "9.0.1",
|
||||
"NodaTime": "2.0.0-alpha20160729",
|
||||
"RethinkDb.Driver": "2.3.15",
|
||||
"Suave": "2.0.0-rc2"
|
||||
},
|
||||
"frameworks": {
|
||||
|
||||
Reference in New Issue
Block a user