Huge repo reorganization
In no particular order... - Created projects using F# generator, using Paket and FAKE - Split "entities" into their own project, and created interface for data functions required on those entities - Renamed "data" project and used it as an implementation of data access - Created "logic" layer that takes the data interface, and does the non-persistence-related manipulation of items - Moved "web" project to "app", and modified Nancy modules to utilize Logic project and data interface instead of Data project and RethinkDB connection - Created test placeholder project; will be filling that out shortly (TAD?)
This commit is contained in:
21
src/MyWebLog.Data.RethinkDB/AssemblyInfo.fs
Normal file
21
src/MyWebLog.Data.RethinkDB/AssemblyInfo.fs
Normal file
@@ -0,0 +1,21 @@
|
||||
namespace myWebLog.Data.AssemblyInfo
|
||||
|
||||
open System.Reflection
|
||||
open System.Runtime.CompilerServices
|
||||
open System.Runtime.InteropServices
|
||||
|
||||
[<assembly: AssemblyTitle("MyWebLog.Data.RethinkDB")>]
|
||||
[<assembly: AssemblyDescription("RethinkDB data access for myWebLog")>]
|
||||
[<assembly: AssemblyConfiguration("")>]
|
||||
[<assembly: AssemblyCompany("DJS Consulting")>]
|
||||
[<assembly: AssemblyProduct("MyWebLog.Data.RethinkDB")>]
|
||||
[<assembly: AssemblyCopyright("Copyright © 2016")>]
|
||||
[<assembly: AssemblyTrademark("")>]
|
||||
[<assembly: AssemblyCulture("")>]
|
||||
[<assembly: ComVisible(false)>]
|
||||
[<assembly: Guid("1fba0b84-b09e-4b16-b9b6-5730dea27192")>]
|
||||
[<assembly: AssemblyVersion("0.9.2.0")>]
|
||||
[<assembly: AssemblyFileVersion("1.0.0.0")>]
|
||||
|
||||
do
|
||||
()
|
||||
100
src/MyWebLog.Data.RethinkDB/Category.fs
Normal file
100
src/MyWebLog.Data.RethinkDB/Category.fs
Normal file
@@ -0,0 +1,100 @@
|
||||
module MyWebLog.Data.RethinkDB.Category
|
||||
|
||||
open FSharp.Interop.Dynamic
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
open System.Dynamic
|
||||
|
||||
let private r = RethinkDb.Driver.RethinkDB.R
|
||||
|
||||
/// Shorthand to get a category by Id and filter by web log Id
|
||||
let private category (webLogId : string) (catId : string) =
|
||||
r.Table(Table.Category)
|
||||
.Get(catId)
|
||||
.Filter(fun c -> c.["WebLogId"].Eq(webLogId))
|
||||
|
||||
/// Get all categories for a web log
|
||||
let getAllCategories conn (webLogId : string) =
|
||||
r.Table(Table.Category)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy("Name")
|
||||
.RunListAsync<Category>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|
||||
/// Get a specific category by its Id
|
||||
let tryFindCategory conn webLogId catId : Category option =
|
||||
match (category webLogId catId)
|
||||
.RunAtomAsync<Category>(conn) |> await |> box with
|
||||
| null -> None
|
||||
| cat -> Some <| unbox cat
|
||||
|
||||
/// Add a category
|
||||
let addCategory conn (cat : Category) =
|
||||
r.Table(Table.Category)
|
||||
.Insert(cat)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
|
||||
/// Update a category
|
||||
let updateCategory conn (cat : Category) =
|
||||
let upd8 = ExpandoObject()
|
||||
upd8?Name <- cat.Name
|
||||
upd8?Slug <- cat.Slug
|
||||
upd8?Description <- cat.Description
|
||||
upd8?ParentId <- cat.ParentId
|
||||
(category cat.WebLogId cat.Id)
|
||||
.Update(upd8)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
|
||||
/// Update a category's children
|
||||
let updateChildren conn webLogId parentId (children : string list) =
|
||||
let upd8 = ExpandoObject()
|
||||
upd8?Children <- children
|
||||
(category webLogId parentId)
|
||||
.Update(upd8)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
|
||||
/// Delete a category
|
||||
let deleteCategory conn cat =
|
||||
// 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
|
||||
let newParent = ExpandoObject()
|
||||
newParent?ParentId <- cat.ParentId
|
||||
cat.Children
|
||||
|> List.iter (fun childId -> (category cat.WebLogId childId)
|
||||
.Update(newParent)
|
||||
.RunResultAsync(conn) |> await |> ignore)
|
||||
// Remove the category from posts where it is assigned
|
||||
r.Table(Table.Post)
|
||||
.GetAll(cat.WebLogId).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["CategoryIds"].Contains(cat.Id)))
|
||||
.RunCursorAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|> List.iter (fun post -> let newCats = ExpandoObject()
|
||||
newCats?CategoryIds <- post.CategoryIds
|
||||
|> List.filter (fun c -> c <> cat.Id)
|
||||
r.Table(Table.Post)
|
||||
.Get(post.Id)
|
||||
.Update(newCats)
|
||||
.RunResultAsync(conn) |> await |> ignore)
|
||||
// Now, delete the category
|
||||
r.Table(Table.Category)
|
||||
.Get(cat.Id)
|
||||
.Delete()
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
|
||||
/// Get a category by its slug
|
||||
let tryFindCategoryBySlug conn (webLogId : string) (slug : string) =
|
||||
r.Table(Table.Category)
|
||||
.GetAll(r.Array(webLogId, slug)).OptArg("index", "Slug")
|
||||
.RunCursorAsync<Category>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
58
src/MyWebLog.Data.RethinkDB/DataConfig.fs
Normal file
58
src/MyWebLog.Data.RethinkDB/DataConfig.fs
Normal file
@@ -0,0 +1,58 @@
|
||||
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 ensureHostname cfg = match cfg.Hostname with
|
||||
| null -> { cfg with Hostname = RethinkDBConstants.DefaultHostname }
|
||||
| _ -> cfg
|
||||
let ensurePort cfg = match cfg.Port with
|
||||
| 0 -> { cfg with Port = RethinkDBConstants.DefaultPort }
|
||||
| _ -> cfg
|
||||
let ensureAuthKey cfg = match cfg.AuthKey with
|
||||
| null -> { cfg with AuthKey = RethinkDBConstants.DefaultAuthkey }
|
||||
| _ -> cfg
|
||||
let ensureTimeout cfg = match cfg.Timeout with
|
||||
| 0 -> { cfg with Timeout = RethinkDBConstants.DefaultTimeout }
|
||||
| _ -> cfg
|
||||
let ensureDatabase 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() }
|
||||
config
|
||||
|> ensureHostname
|
||||
|> ensurePort
|
||||
|> ensureAuthKey
|
||||
|> ensureTimeout
|
||||
|> ensureDatabase
|
||||
|> connect
|
||||
11
src/MyWebLog.Data.RethinkDB/Extensions.fs
Normal file
11
src/MyWebLog.Data.RethinkDB/Extensions.fs
Normal file
@@ -0,0 +1,11 @@
|
||||
[<AutoOpen>]
|
||||
module MyWebLog.Data.RethinkDB.Extensions
|
||||
|
||||
open RethinkDb.Driver.Ast
|
||||
open RethinkDb.Driver.Net
|
||||
|
||||
let await task = task |> Async.AwaitTask |> Async.RunSynchronously
|
||||
|
||||
type ReqlExpr with
|
||||
/// Run a SUCCESS_ATOM response that returns multiple values
|
||||
member this.RunListAsync<'T> (conn : IConnection) = this.RunAtomAsync<System.Collections.Generic.List<'T>> conn
|
||||
161
src/MyWebLog.Data.RethinkDB/MyWebLog.Data.RethinkDB.fsproj
Normal file
161
src/MyWebLog.Data.RethinkDB/MyWebLog.Data.RethinkDB.fsproj
Normal file
@@ -0,0 +1,161 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Project ToolsVersion="12.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
|
||||
<Import Project="$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props" Condition="Exists('$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props')" />
|
||||
<PropertyGroup>
|
||||
<Configuration Condition=" '$(Configuration)' == '' ">Debug</Configuration>
|
||||
<Platform Condition=" '$(Platform)' == '' ">AnyCPU</Platform>
|
||||
<SchemaVersion>2.0</SchemaVersion>
|
||||
<ProjectGuid>d6c2be5e-883a-4f34-9905-b730543ca380</ProjectGuid>
|
||||
<OutputType>Library</OutputType>
|
||||
<RootNamespace>MyWebLog.Data.RethinkDB</RootNamespace>
|
||||
<AssemblyName>MyWebLog.Data.RethinkDB</AssemblyName>
|
||||
<TargetFrameworkVersion>v4.5.2</TargetFrameworkVersion>
|
||||
<TargetFSharpCoreVersion>4.4.0.0</TargetFSharpCoreVersion>
|
||||
<Name>MyWebLog.Data.RethinkDB</Name>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Debug|AnyCPU' ">
|
||||
<DebugSymbols>true</DebugSymbols>
|
||||
<DebugType>full</DebugType>
|
||||
<Optimize>false</Optimize>
|
||||
<Tailcalls>false</Tailcalls>
|
||||
<OutputPath>bin\Debug\</OutputPath>
|
||||
<DefineConstants>DEBUG;TRACE</DefineConstants>
|
||||
<WarningLevel>3</WarningLevel>
|
||||
<DocumentationFile>bin\Debug\MyWebLog.Data.RethinkDB.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
|
||||
<DebugType>pdbonly</DebugType>
|
||||
<Optimize>true</Optimize>
|
||||
<Tailcalls>true</Tailcalls>
|
||||
<OutputPath>bin\Release\</OutputPath>
|
||||
<DefineConstants>TRACE</DefineConstants>
|
||||
<WarningLevel>3</WarningLevel>
|
||||
<DocumentationFile>bin\Release\MyWebLog.Data.RethinkDB.xml</DocumentationFile>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<Reference Include="mscorlib" />
|
||||
<Reference Include="FSharp.Core, Version=$(TargetFSharpCoreVersion), Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a">
|
||||
<Private>True</Private>
|
||||
</Reference>
|
||||
<Reference Include="System" />
|
||||
<Reference Include="System.Core" />
|
||||
<Reference Include="System.Numerics" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<Compile Include="Extensions.fs" />
|
||||
<Compile Include="Table.fs" />
|
||||
<Compile Include="DataConfig.fs" />
|
||||
<Compile Include="Category.fs" />
|
||||
<Compile Include="Page.fs" />
|
||||
<Compile Include="Post.fs" />
|
||||
<Compile Include="User.fs" />
|
||||
<Compile Include="WebLog.fs" />
|
||||
<Compile Include="SetUp.fs" />
|
||||
<Compile Include="RethinkMyWebLogData.fs" />
|
||||
</ItemGroup>
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog.Entities\MyWebLog.Entities.fsproj">
|
||||
<Name>MyWebLog.Entities</Name>
|
||||
<Project>{a87f3cf5-2189-442b-8acf-929f5153ac22}</Project>
|
||||
<Private>True</Private>
|
||||
</ProjectReference>
|
||||
</ItemGroup>
|
||||
<PropertyGroup>
|
||||
<MinimumVisualStudioVersion Condition="'$(MinimumVisualStudioVersion)' == ''">11</MinimumVisualStudioVersion>
|
||||
</PropertyGroup>
|
||||
<Choose>
|
||||
<When Condition="'$(VisualStudioVersion)' == '11.0'">
|
||||
<PropertyGroup Condition="Exists('$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets')">
|
||||
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\..\Microsoft SDKs\F#\3.0\Framework\v4.0\Microsoft.FSharp.Targets</FSharpTargetsPath>
|
||||
</PropertyGroup>
|
||||
</When>
|
||||
<Otherwise>
|
||||
<PropertyGroup Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets')">
|
||||
<FSharpTargetsPath>$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\FSharp\Microsoft.FSharp.Targets</FSharpTargetsPath>
|
||||
</PropertyGroup>
|
||||
</Otherwise>
|
||||
</Choose>
|
||||
<Import Project="$(FSharpTargetsPath)" Condition="Exists('$(FSharpTargetsPath)')" />
|
||||
<!-- To modify your build process, add your task inside one of the targets below and uncomment it.
|
||||
Other similar extension points exist, see Microsoft.Common.targets.
|
||||
<Target Name="BeforeBuild">
|
||||
</Target>
|
||||
<Target Name="AfterBuild">
|
||||
</Target>
|
||||
-->
|
||||
<Choose>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.0' Or $(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.2')">
|
||||
<ItemGroup>
|
||||
<Reference Include="Common.Logging">
|
||||
<HintPath>..\packages\Common.Logging\lib\net40\Common.Logging.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Choose>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.0' Or $(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.2')">
|
||||
<ItemGroup>
|
||||
<Reference Include="Common.Logging.Core">
|
||||
<HintPath>..\packages\Common.Logging.Core\lib\net40\Common.Logging.Core.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Choose>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.0' Or $(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.2')">
|
||||
<ItemGroup>
|
||||
<Reference Include="Dynamitey">
|
||||
<HintPath>..\packages\Dynamitey\lib\net40\Dynamitey.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Choose>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.2')">
|
||||
<ItemGroup>
|
||||
<Reference Include="FSharp.Interop.Dynamic">
|
||||
<HintPath>..\packages\FSharp.Interop.Dynamic\lib\portable-net45+sl50+win\FSharp.Interop.Dynamic.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Choose>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And $(TargetFrameworkVersion) == 'v4.0'">
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>..\packages\Newtonsoft.Json\lib\net40\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.2')">
|
||||
<ItemGroup>
|
||||
<Reference Include="Newtonsoft.Json">
|
||||
<HintPath>..\packages\Newtonsoft.Json\lib\net45\Newtonsoft.Json.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
<Choose>
|
||||
<When Condition="$(TargetFrameworkIdentifier) == '.NETFramework' And ($(TargetFrameworkVersion) == 'v4.5' Or $(TargetFrameworkVersion) == 'v4.5.2')">
|
||||
<ItemGroup>
|
||||
<Reference Include="RethinkDb.Driver">
|
||||
<HintPath>..\packages\RethinkDb.Driver\lib\net45\RethinkDb.Driver.dll</HintPath>
|
||||
<Private>True</Private>
|
||||
<Paket>True</Paket>
|
||||
</Reference>
|
||||
</ItemGroup>
|
||||
</When>
|
||||
</Choose>
|
||||
</Project>
|
||||
70
src/MyWebLog.Data.RethinkDB/Page.fs
Normal file
70
src/MyWebLog.Data.RethinkDB/Page.fs
Normal file
@@ -0,0 +1,70 @@
|
||||
module MyWebLog.Data.RethinkDB.Page
|
||||
|
||||
open FSharp.Interop.Dynamic
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
open System.Dynamic
|
||||
|
||||
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 =
|
||||
let pg = r.Table(Table.Page)
|
||||
.Get(pageId)
|
||||
match (match includeRevs with
|
||||
| true -> pg.RunAtomAsync<Page>(conn)
|
||||
| _ -> pg.Without("Revisions").RunAtomAsync<Page>(conn)
|
||||
|> await |> box) with
|
||||
| null -> None
|
||||
| page -> let pg : Page = unbox page
|
||||
match pg.WebLogId = webLogId with true -> Some pg | _ -> None
|
||||
|
||||
/// Find a page by its permalink
|
||||
let tryFindPageByPermalink conn (webLogId : string) (permalink : string) =
|
||||
r.Table(Table.Page)
|
||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "Permalink")
|
||||
.Without("Revisions")
|
||||
.RunCursorAsync<Page>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Get a list of all pages (excludes page text and revisions)
|
||||
let findAllPages conn (webLogId : string) =
|
||||
r.Table(Table.Page)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy("Title")
|
||||
.Without("Text", "Revisions")
|
||||
.RunListAsync<Page>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|
||||
/// Add a page
|
||||
let addPage conn (page : Page) =
|
||||
r.Table(Table.Page)
|
||||
.Insert(page)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
|
||||
/// Update a page
|
||||
let updatePage conn (page : Page) =
|
||||
match tryFindPageById conn page.WebLogId page.Id false with
|
||||
| Some _ -> let upd8 = ExpandoObject()
|
||||
upd8?Title <- page.Title
|
||||
upd8?Permalink <- page.Permalink
|
||||
upd8?PublishedOn <- page.PublishedOn
|
||||
upd8?UpdatedOn <- page.UpdatedOn
|
||||
upd8?Text <- page.Text
|
||||
upd8?Revisions <- page.Revisions
|
||||
r.Table(Table.Page)
|
||||
.Get(page.Id)
|
||||
.Update(upd8)
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
| _ -> ()
|
||||
|
||||
/// Delete a page
|
||||
let deletePage conn webLogId pageId =
|
||||
match tryFindPageById conn webLogId pageId false with
|
||||
| Some _ -> r.Table(Table.Page)
|
||||
.Get(pageId)
|
||||
.Delete()
|
||||
.RunResultAsync(conn) |> await |> ignore
|
||||
| _ -> ()
|
||||
179
src/MyWebLog.Data.RethinkDB/Post.fs
Normal file
179
src/MyWebLog.Data.RethinkDB/Post.fs
Normal file
@@ -0,0 +1,179 @@
|
||||
module MyWebLog.Data.RethinkDB.Post
|
||||
|
||||
open FSharp.Interop.Dynamic
|
||||
open MyWebLog.Entities
|
||||
open RethinkDb.Driver.Ast
|
||||
open System.Dynamic
|
||||
|
||||
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")
|
||||
|
||||
/// Shorthand to sort posts by published date, slice for the given page, and return a list
|
||||
let private toPostList conn pageNbr nbrPerPage (filter : ReqlExpr) =
|
||||
filter
|
||||
.OrderBy(r.Desc("PublishedOn"))
|
||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||
.RunListAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|
||||
/// Shorthand to get a newer or older post
|
||||
let private adjacentPost conn post (theFilter : ReqlExpr -> obj) (sort : obj) =
|
||||
(publishedPosts post.WebLogId)
|
||||
.Filter(theFilter)
|
||||
.OrderBy(sort)
|
||||
.Limit(1)
|
||||
.RunListAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// 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
|
||||
r.Table(Table.Post)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.OrderBy(r.Desc("PublishedOn"))
|
||||
.Slice((pageNbr - 1) * nbrPerPage, pageNbr * nbrPerPage)
|
||||
.RunListAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.toList
|
||||
|
||||
/// Try to find a post by its Id and web log Id
|
||||
let tryFindPost conn webLogId postId : Post option =
|
||||
match r.Table(Table.Post)
|
||||
.Get(postId)
|
||||
.Filter(ReqlFunction1(fun p -> upcast p.["WebLogId"].Eq(webLogId)))
|
||||
.RunAtomAsync<Post>(conn)
|
||||
|> box with
|
||||
| null -> None
|
||||
| post -> Some <| unbox post
|
||||
|
||||
/// Try to find a post by its permalink
|
||||
let tryFindPostByPermalink conn webLogId permalink =
|
||||
r.Table(Table.Post)
|
||||
.GetAll(r.Array(webLogId, permalink)).OptArg("index", "Permalink")
|
||||
.Filter(fun p -> p.["Status"].Eq(PostStatus.Published))
|
||||
.Without("Revisions")
|
||||
.Merge(fun p -> r.HashMap("Categories", r.Table(Table.Category)
|
||||
.GetAll(p.["CategoryIds"])
|
||||
.Without("Children")
|
||||
.OrderBy("Name")
|
||||
.CoerceTo("array")))
|
||||
.Merge(fun p -> r.HashMap("Comments", r.Table(Table.Comment)
|
||||
.GetAll(p.["Id"]).OptArg("index", "PostId")
|
||||
.OrderBy("PostedOn")
|
||||
.CoerceTo("array")))
|
||||
.RunCursorAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Try to find a post by its prior permalink
|
||||
let tryFindPostByPriorPermalink conn (webLogId : string) (permalink : string) =
|
||||
r.Table(Table.Post)
|
||||
.GetAll(webLogId).OptArg("index", "WebLogId")
|
||||
.Filter(fun p -> p.["PriorPermalinks"].Contains(permalink).And(p.["Status"].Eq(PostStatus.Published)))
|
||||
.Without("Revisions")
|
||||
.RunCursorAsync<Post>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Get a set of posts for RSS
|
||||
let findFeedPosts conn webLogId nbr : (Post * User option) list =
|
||||
(publishedPosts webLogId)
|
||||
.Merge(fun post -> r.HashMap("Categories", r.Table(Table.Category)
|
||||
.GetAll(post.["CategoryIds"])
|
||||
.OrderBy("Name")
|
||||
.Pluck("Id", "Name")
|
||||
.CoerceTo("array")))
|
||||
|> toPostList conn 1 nbr
|
||||
|> List.map (fun post -> post, match r.Table(Table.User)
|
||||
.Get(post.AuthorId)
|
||||
.RunAtomAsync<User>(conn)
|
||||
|> await
|
||||
|> box with
|
||||
| null -> None
|
||||
| user -> Some <| unbox user)
|
||||
|
||||
/// Add a post
|
||||
let addPost conn post =
|
||||
r.Table(Table.Post)
|
||||
.Insert(post)
|
||||
.RunResultAsync(conn)
|
||||
|> ignore
|
||||
|
||||
/// Update a post
|
||||
let updatePost conn post =
|
||||
r.Table(Table.Post)
|
||||
.Get(post.Id)
|
||||
.Replace( { post with Categories = []
|
||||
Comments = [] } )
|
||||
.RunResultAsync(conn)
|
||||
|> ignore
|
||||
|
||||
/// Save a post
|
||||
let savePost conn post =
|
||||
match post.Id with
|
||||
| "new" -> let newPost = { post with Id = string <| System.Guid.NewGuid() }
|
||||
r.Table(Table.Post)
|
||||
.Insert(newPost)
|
||||
.RunResultAsync(conn)
|
||||
|> ignore
|
||||
newPost.Id
|
||||
| _ -> r.Table(Table.Post)
|
||||
.Get(post.Id)
|
||||
.Replace( { post with Categories = []
|
||||
Comments = [] } )
|
||||
.RunResultAsync(conn)
|
||||
|> ignore
|
||||
post.Id
|
||||
47
src/MyWebLog.Data.RethinkDB/RethinkMyWebLogData.fs
Normal file
47
src/MyWebLog.Data.RethinkDB/RethinkMyWebLogData.fs
Normal file
@@ -0,0 +1,47 @@
|
||||
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 this.SetUp = fun () -> SetUp.startUpCheck cfg
|
||||
|
||||
member this.AllCategories = Category.getAllCategories conn
|
||||
member this.CategoryById = Category.tryFindCategory conn
|
||||
member this.CategoryBySlug = Category.tryFindCategoryBySlug conn
|
||||
member this.AddCategory = Category.addCategory conn
|
||||
member this.UpdateCategory = Category.updateCategory conn
|
||||
member this.UpdateChildren = Category.updateChildren conn
|
||||
member this.DeleteCategory = Category.deleteCategory conn
|
||||
|
||||
member this.PageById = Page.tryFindPageById conn
|
||||
member this.PageByPermalink = Page.tryFindPageByPermalink conn
|
||||
member this.AllPages = Page.findAllPages conn
|
||||
member this.AddPage = Page.addPage conn
|
||||
member this.UpdatePage = Page.updatePage conn
|
||||
member this.DeletePage = Page.deletePage conn
|
||||
|
||||
member this.PageOfPublishedPosts = Post.findPageOfPublishedPosts conn
|
||||
member this.PageOfCategorizedPosts = Post.findPageOfCategorizedPosts conn
|
||||
member this.PageOfTaggedPosts = Post.findPageOfTaggedPosts conn
|
||||
member this.NewerPost = Post.tryFindNewerPost conn
|
||||
member this.NewerCategorizedPost = Post.tryFindNewerCategorizedPost conn
|
||||
member this.NewerTaggedPost = Post.tryFindNewerTaggedPost conn
|
||||
member this.OlderPost = Post.tryFindOlderPost conn
|
||||
member this.OlderCategorizedPost = Post.tryFindOlderCategorizedPost conn
|
||||
member this.OlderTaggedPost = Post.tryFindOlderTaggedPost conn
|
||||
member this.PageOfAllPosts = Post.findPageOfAllPosts conn
|
||||
member this.PostById = Post.tryFindPost conn
|
||||
member this.PostByPermalink = Post.tryFindPostByPermalink conn
|
||||
member this.PostByPriorPermalink = Post.tryFindPostByPriorPermalink conn
|
||||
member this.FeedPosts = Post.findFeedPosts conn
|
||||
member this.AddPost = Post.addPost conn
|
||||
member this.UpdatePost = Post.updatePost conn
|
||||
|
||||
member this.LogOn = User.tryUserLogOn conn
|
||||
|
||||
member this.WebLogByUrlBase = WebLog.tryFindWebLogByUrlBase conn
|
||||
member this.DashboardCounts = WebLog.findDashboardCounts conn
|
||||
|
||||
89
src/MyWebLog.Data.RethinkDB/SetUp.fs
Normal file
89
src/MyWebLog.Data.RethinkDB/SetUp.fs
Normal file
@@ -0,0 +1,89 @@
|
||||
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) =
|
||||
logStep "|> Checking database"
|
||||
let dbs = r.DbList().RunListAsync<string>(cfg.Conn) |> await
|
||||
match dbs.Contains cfg.Database with
|
||||
| true -> ()
|
||||
| _ -> logStepStart (sprintf " %s database not found - creating" cfg.Database)
|
||||
r.DbCreate(cfg.Database).RunResultAsync(cfg.Conn) |> await |> ignore
|
||||
logStepDone ()
|
||||
|
||||
/// Ensure all required tables exist
|
||||
let private checkTables cfg =
|
||||
logStep "|> Checking tables"
|
||||
let tables = r.Db(cfg.Database).TableList().RunListAsync<string>(cfg.Conn) |> await
|
||||
[ Table.Category; Table.Comment; Table.Page; Table.Post; Table.User; Table.WebLog ]
|
||||
|> List.map (fun tbl -> match tables.Contains tbl with true -> None | _ -> Some (tbl, r.TableCreate tbl))
|
||||
|> List.filter Option.isSome
|
||||
|> List.map Option.get
|
||||
|> List.iter (fun (tbl, create) -> logStepStart (sprintf " Creating table %s" tbl)
|
||||
create.RunResultAsync(cfg.Conn) |> await |> ignore
|
||||
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) =
|
||||
let idxName, idxFunc = index
|
||||
logStepStart (sprintf """ Creating index "%s" on table %s""" idxName table)
|
||||
(match idxFunc with
|
||||
| Some f -> (tbl cfg table).IndexCreate(idxName, f)
|
||||
| None -> (tbl cfg table).IndexCreate(idxName))
|
||||
.RunResultAsync(cfg.Conn)
|
||||
|> await |> ignore
|
||||
(tbl cfg table).IndexWait(idxName).RunAtomAsync(cfg.Conn) |> await |> ignore
|
||||
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) =
|
||||
let idx = (tbl cfg tblName).IndexList().RunListAsync<string>(cfg.Conn) |> await
|
||||
idxs
|
||||
|> List.iter (fun index -> match idx.Contains (fst index) with true -> () | _ -> createIndex cfg tblName index)
|
||||
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 =
|
||||
logStep "Database Start Up Checks Starting"
|
||||
checkDatabase cfg
|
||||
checkTables cfg
|
||||
checkIndexes cfg
|
||||
logStep "Database Start Up Checks Complete"
|
||||
21
src/MyWebLog.Data.RethinkDB/Table.fs
Normal file
21
src/MyWebLog.Data.RethinkDB/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"
|
||||
17
src/MyWebLog.Data.RethinkDB/User.fs
Normal file
17
src/MyWebLog.Data.RethinkDB/User.fs
Normal file
@@ -0,0 +1,17 @@
|
||||
module MyWebLog.Data.RethinkDB.User
|
||||
|
||||
open MyWebLog.Entities
|
||||
|
||||
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) =
|
||||
r.Table(Table.User)
|
||||
.GetAll(email).OptArg("index", "UserName")
|
||||
.Filter(fun u -> u.["PasswordHash"].Eq(passwordHash))
|
||||
.RunCursorAsync<User>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
29
src/MyWebLog.Data.RethinkDB/WebLog.fs
Normal file
29
src/MyWebLog.Data.RethinkDB/WebLog.fs
Normal file
@@ -0,0 +1,29 @@
|
||||
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) =
|
||||
r.Table(Table.WebLog)
|
||||
.GetAll(urlBase).OptArg("index", "UrlBase")
|
||||
.Merge(fun w -> r.HashMap("PageList", r.Table(Table.Page)
|
||||
.GetAll(w.["Id"]).OptArg("index", "WebLogId")
|
||||
.Filter(ReqlFunction1(fun pg -> upcast pg.["ShowInPageList"].Eq(true)))
|
||||
.OrderBy("Title")
|
||||
.Pluck("Title", "Permalink")
|
||||
.CoerceTo("array")))
|
||||
.RunCursorAsync<WebLog>(conn)
|
||||
|> await
|
||||
|> Seq.tryHead
|
||||
|
||||
/// Get counts for the admin dashboard
|
||||
let findDashboardCounts conn (webLogId : string) =
|
||||
r.Expr( r.HashMap("Pages", r.Table(Table.Page ).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.Merge(r.HashMap("Posts", r.Table(Table.Post ).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.Merge(r.HashMap("Categories", r.Table(Table.Category).GetAll(webLogId).OptArg("index", "WebLogId").Count()))
|
||||
.RunAtomAsync<DashboardCounts>(conn)
|
||||
|> await
|
||||
|
||||
2
src/MyWebLog.Data.RethinkDB/paket.references
Normal file
2
src/MyWebLog.Data.RethinkDB/paket.references
Normal file
@@ -0,0 +1,2 @@
|
||||
FSharp.Interop.Dynamic
|
||||
RethinkDb.Driver
|
||||
Reference in New Issue
Block a user