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
|
||||
|
||||
Reference in New Issue
Block a user