v2 (#36)
* Use PostgreSQL JSON-based data implementation * Fix back link on RSS settings page (#34) * Show theme upload messages (#28) * Fix admin page list paging (#35) * Add db migrations for all stores * Support both .NET 6 and 7
This commit is contained in:
parent
5f3daa1de9
commit
7b325dc19e
|
@ -1,12 +0,0 @@
|
|||
{
|
||||
"version": 1,
|
||||
"isRoot": true,
|
||||
"tools": {
|
||||
"fake-cli": {
|
||||
"version": "5.22.0",
|
||||
"commands": [
|
||||
"fake"
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
166
build.fs
Normal file
166
build.fs
Normal file
|
@ -0,0 +1,166 @@
|
|||
open System.IO
|
||||
open Fake.Core
|
||||
open Fake.DotNet
|
||||
open Fake.IO
|
||||
open Fake.IO.Globbing.Operators
|
||||
|
||||
let execContext = Context.FakeExecutionContext.Create false "build.fsx" []
|
||||
Context.setExecutionContext (Context.RuntimeContext.Fake execContext)
|
||||
|
||||
/// The output directory for release ZIPs
|
||||
let releasePath = "releases"
|
||||
|
||||
/// The path to the main project
|
||||
let projectPath = "src/MyWebLog"
|
||||
|
||||
/// The path and name of the main project
|
||||
let projName = $"{projectPath}/MyWebLog.fsproj"
|
||||
|
||||
/// The version being packaged (extracted from appsettings.json)
|
||||
let version =
|
||||
let settings = File.ReadAllText $"{projectPath}/appsettings.json"
|
||||
let generator = settings.Substring (settings.IndexOf "\"Generator\":")
|
||||
let appVersion = generator.Replace("\"Generator\": \"", "")
|
||||
let appVersion = appVersion.Substring (0, appVersion.IndexOf "\"")
|
||||
appVersion.Split ' ' |> Array.last
|
||||
|
||||
/// Zip a theme distributed with myWebLog
|
||||
let zipTheme (name : string) (_ : TargetParameter) =
|
||||
let path = $"src/{name}-theme"
|
||||
!! $"{path}/**/*"
|
||||
|> Zip.filesAsSpecs path
|
||||
|> Seq.filter (fun (_, name) -> not (name.EndsWith ".zip"))
|
||||
|> Zip.zipSpec $"{releasePath}/{name}-theme.zip"
|
||||
|
||||
/// Frameworks supported by this build
|
||||
let frameworks = [ "net6.0"; "net7.0" ]
|
||||
|
||||
/// Publish the project for the given runtime ID
|
||||
let publishFor rid (_ : TargetParameter) =
|
||||
frameworks
|
||||
|> List.iter (fun fwk ->
|
||||
DotNet.publish
|
||||
(fun opts ->
|
||||
{ opts with Runtime = Some rid; SelfContained = Some false; NoLogo = true; Framework = Some fwk })
|
||||
projName)
|
||||
|
||||
/// Package published output for the given runtime ID
|
||||
let packageFor rid (_ : TargetParameter) =
|
||||
frameworks
|
||||
|> List.iter (fun fwk ->
|
||||
let path = $"{projectPath}/bin/Release/{fwk}/%s{rid}/publish"
|
||||
let prodSettings = $"{path}/appsettings.Production.json"
|
||||
if File.exists prodSettings then File.delete prodSettings
|
||||
[ !! $"{path}/**/*"
|
||||
|> Zip.filesAsSpecs path
|
||||
|> Seq.map (fun (orig, dest) ->
|
||||
orig, if dest.StartsWith "MyWebLog" then dest.Replace ("MyWebLog", "myWebLog") else dest)
|
||||
Seq.singleton ($"{releasePath}/admin-theme.zip", "admin-theme.zip")
|
||||
Seq.singleton ($"{releasePath}/default-theme.zip", "default-theme.zip")
|
||||
]
|
||||
|> Seq.concat
|
||||
|> Zip.zipSpec $"{releasePath}/myWebLog-{version}.{fwk}.{rid}.zip")
|
||||
|
||||
|
||||
Target.create "Clean" (fun _ ->
|
||||
!! "src/**/bin"
|
||||
++ "src/**/obj"
|
||||
|> Shell.cleanDirs
|
||||
Shell.cleanDir releasePath
|
||||
)
|
||||
|
||||
Target.create "Build" (fun _ ->
|
||||
DotNet.build (fun opts -> { opts with NoLogo = true }) projName
|
||||
)
|
||||
|
||||
Target.create "ZipAdminTheme" (zipTheme "admin")
|
||||
Target.create "ZipDefaultTheme" (zipTheme "default")
|
||||
|
||||
Target.create "PublishWindows" (publishFor "win-x64")
|
||||
Target.create "PackageWindows" (packageFor "win-x64")
|
||||
|
||||
Target.create "PublishLinux" (publishFor "linux-x64")
|
||||
Target.create "PackageLinux" (packageFor "linux-x64")
|
||||
|
||||
Target.create "RepackageLinux" (fun _ ->
|
||||
let workDir = $"{releasePath}/linux"
|
||||
frameworks
|
||||
|> List.iter (fun fwk ->
|
||||
let zipArchive = $"{releasePath}/myWebLog-{version}.{fwk}.linux-x64.zip"
|
||||
let sh command args =
|
||||
CreateProcess.fromRawCommand command args
|
||||
|> CreateProcess.redirectOutput
|
||||
|> Proc.run
|
||||
|> ignore
|
||||
Shell.mkdir workDir
|
||||
Zip.unzip workDir zipArchive
|
||||
Shell.cd workDir
|
||||
sh "chmod" [ "+x"; "./myWebLog" ]
|
||||
sh "tar" [ "cfj"; $"../myWebLog-{version}.{fwk}.linux-x64.tar.bz2"; "." ]
|
||||
Shell.cd "../.."
|
||||
Shell.rm zipArchive)
|
||||
Shell.rm_rf workDir
|
||||
)
|
||||
|
||||
Target.create "All" ignore
|
||||
|
||||
Target.create "RemoveThemeArchives" (fun _ ->
|
||||
Shell.rm $"{releasePath}/admin-theme.zip"
|
||||
Shell.rm $"{releasePath}/default-theme.zip"
|
||||
)
|
||||
|
||||
Target.create "CI" ignore
|
||||
|
||||
open Fake.Core.TargetOperators
|
||||
|
||||
let dependencies = [
|
||||
"Clean"
|
||||
==> "All"
|
||||
|
||||
"Clean"
|
||||
?=> "Build"
|
||||
==> "All"
|
||||
|
||||
"Clean"
|
||||
?=> "ZipDefaultTheme"
|
||||
==> "All"
|
||||
|
||||
"Clean"
|
||||
?=> "ZipAdminTheme"
|
||||
==> "All"
|
||||
|
||||
"Build"
|
||||
==> "PublishWindows"
|
||||
==> "All"
|
||||
|
||||
"Build"
|
||||
==> "PublishLinux"
|
||||
==> "All"
|
||||
|
||||
"PublishWindows"
|
||||
==> "PackageWindows"
|
||||
==> "All"
|
||||
|
||||
"PublishLinux"
|
||||
==> "PackageLinux"
|
||||
==> "All"
|
||||
|
||||
"PackageLinux"
|
||||
==> "RepackageLinux"
|
||||
==> "All"
|
||||
|
||||
"All"
|
||||
==> "RemoveThemeArchives"
|
||||
==> "CI"
|
||||
]
|
||||
|
||||
[<EntryPoint>]
|
||||
let main args =
|
||||
try
|
||||
match args with
|
||||
| [| target |] -> Target.runOrDefault target
|
||||
| _ -> Target.runOrDefault "All"
|
||||
0
|
||||
with e ->
|
||||
printfn "%A" e
|
||||
1
|
20
build.fsproj
Normal file
20
build.fsproj
Normal file
|
@ -0,0 +1,20 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net7.0</TargetFramework>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="build.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Fake.Core.Target" Version="5.23.1" />
|
||||
<PackageReference Include="Fake.DotNet.Cli" Version="5.23.1" />
|
||||
<PackageReference Include="Fake.IO.FileSystem" Version="5.23.1" />
|
||||
<PackageReference Include="Fake.IO.Zip" Version="5.23.1" />
|
||||
<PackageReference Include="MSBuild.StructuredLogger" Version="2.1.768" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
147
build.fsx
147
build.fsx
|
@ -1,147 +0,0 @@
|
|||
#r "paket:
|
||||
nuget Fake.DotNet.Cli
|
||||
nuget Fake.IO.FileSystem
|
||||
nuget Fake.IO.Zip
|
||||
nuget Fake.Core.Target //"
|
||||
#load ".fake/build.fsx/intellisense.fsx"
|
||||
open System.IO
|
||||
open Fake.Core
|
||||
open Fake.DotNet
|
||||
open Fake.IO
|
||||
open Fake.IO.Globbing.Operators
|
||||
open Fake.Core.TargetOperators
|
||||
|
||||
Target.initEnvironment ()
|
||||
|
||||
/// The output directory for release ZIPs
|
||||
let releasePath = "releases"
|
||||
|
||||
/// The path to the main project
|
||||
let projectPath = "src/MyWebLog"
|
||||
|
||||
/// The path and name of the main project
|
||||
let projName = $"{projectPath}/MyWebLog.fsproj"
|
||||
|
||||
/// The version being packaged (extracted from appsettings.json)
|
||||
let version =
|
||||
let settings = File.ReadAllText $"{projectPath}/appsettings.json"
|
||||
let generator = settings.Substring (settings.IndexOf "\"Generator\":")
|
||||
let appVersion = generator.Replace("\"Generator\": \"", "")
|
||||
let appVersion = appVersion.Substring (0, appVersion.IndexOf "\"")
|
||||
appVersion.Split ' ' |> Array.last
|
||||
|
||||
/// Zip a theme distributed with myWebLog
|
||||
let zipTheme (name : string) (_ : TargetParameter) =
|
||||
let path = $"src/{name}-theme"
|
||||
!! $"{path}/**/*"
|
||||
|> Zip.filesAsSpecs path
|
||||
|> Seq.filter (fun (_, name) -> not (name.EndsWith ".zip"))
|
||||
|> Zip.zipSpec $"{releasePath}/{name}-theme.zip"
|
||||
|
||||
/// Publish the project for the given runtime ID
|
||||
let publishFor rid (_ : TargetParameter) =
|
||||
DotNet.publish (fun opts -> { opts with Runtime = Some rid; SelfContained = Some false; NoLogo = true }) projName
|
||||
|
||||
/// Package published output for the given runtime ID
|
||||
let packageFor (rid : string) (_ : TargetParameter) =
|
||||
let path = $"{projectPath}/bin/Release/net6.0/{rid}/publish"
|
||||
let prodSettings = $"{path}/appsettings.Production.json"
|
||||
if File.exists prodSettings then File.delete prodSettings
|
||||
[ !! $"{path}/**/*"
|
||||
|> Zip.filesAsSpecs path
|
||||
|> Seq.map (fun (orig, dest) ->
|
||||
orig, if dest.StartsWith "MyWebLog" then dest.Replace ("MyWebLog", "myWebLog") else dest)
|
||||
Seq.singleton ($"{releasePath}/admin-theme.zip", "admin-theme.zip")
|
||||
Seq.singleton ($"{releasePath}/default-theme.zip", "default-theme.zip")
|
||||
]
|
||||
|> Seq.concat
|
||||
|> Zip.zipSpec $"{releasePath}/myWebLog-{version}.{rid}.zip"
|
||||
|
||||
|
||||
Target.create "Clean" (fun _ ->
|
||||
!! "src/**/bin"
|
||||
++ "src/**/obj"
|
||||
|> Shell.cleanDirs
|
||||
Shell.cleanDir releasePath
|
||||
)
|
||||
|
||||
Target.create "Build" (fun _ ->
|
||||
DotNet.build (fun opts -> { opts with NoLogo = true }) projName
|
||||
)
|
||||
|
||||
Target.create "ZipAdminTheme" (zipTheme "admin")
|
||||
Target.create "ZipDefaultTheme" (zipTheme "default")
|
||||
|
||||
Target.create "PublishWindows" (publishFor "win-x64")
|
||||
Target.create "PackageWindows" (packageFor "win-x64")
|
||||
|
||||
Target.create "PublishLinux" (publishFor "linux-x64")
|
||||
Target.create "PackageLinux" (packageFor "linux-x64")
|
||||
|
||||
Target.create "RepackageLinux" (fun _ ->
|
||||
let workDir = $"{releasePath}/linux"
|
||||
let zipArchive = $"{releasePath}/myWebLog-{version}.linux-x64.zip"
|
||||
let sh command args =
|
||||
CreateProcess.fromRawCommand command args
|
||||
|> CreateProcess.redirectOutput
|
||||
|> Proc.run
|
||||
|> ignore
|
||||
Shell.mkdir workDir
|
||||
Zip.unzip workDir zipArchive
|
||||
Shell.cd workDir
|
||||
sh "chmod" [ "+x"; "./myWebLog" ]
|
||||
sh "tar" [ "cfj"; $"../myWebLog-{version}.linux-x64.tar.bz2"; "." ]
|
||||
Shell.cd "../.."
|
||||
Shell.rm zipArchive
|
||||
Shell.rm_rf workDir
|
||||
)
|
||||
|
||||
Target.create "All" ignore
|
||||
|
||||
Target.create "RemoveThemeArchives" (fun _ ->
|
||||
Shell.rm $"{releasePath}/admin-theme.zip"
|
||||
Shell.rm $"{releasePath}/default-theme.zip"
|
||||
)
|
||||
|
||||
Target.create "CI" ignore
|
||||
|
||||
"Clean"
|
||||
==> "All"
|
||||
|
||||
"Clean"
|
||||
?=> "Build"
|
||||
==> "All"
|
||||
|
||||
"Clean"
|
||||
?=> "ZipDefaultTheme"
|
||||
==> "All"
|
||||
|
||||
"Clean"
|
||||
?=> "ZipAdminTheme"
|
||||
==> "All"
|
||||
|
||||
"Build"
|
||||
==> "PublishWindows"
|
||||
==> "All"
|
||||
|
||||
"Build"
|
||||
==> "PublishLinux"
|
||||
==> "All"
|
||||
|
||||
"PublishWindows"
|
||||
==> "PackageWindows"
|
||||
==> "All"
|
||||
|
||||
"PublishLinux"
|
||||
==> "PackageLinux"
|
||||
==> "All"
|
||||
|
||||
"PackageLinux"
|
||||
==> "RepackageLinux"
|
||||
==> "All"
|
||||
|
||||
"All"
|
||||
==> "RemoveThemeArchives"
|
||||
==> "CI"
|
||||
|
||||
Target.runOrDefault "All"
|
227
build.fsx.lock
227
build.fsx.lock
|
@ -1,227 +0,0 @@
|
|||
STORAGE: NONE
|
||||
RESTRICTION: || (== net6.0) (== netstandard2.0)
|
||||
NUGET
|
||||
remote: https://api.nuget.org/v3/index.json
|
||||
BlackFox.VsWhere (1.1)
|
||||
FSharp.Core (>= 4.2.3)
|
||||
Microsoft.Win32.Registry (>= 4.7)
|
||||
Fake.Core.CommandLineParsing (5.22)
|
||||
FParsec (>= 1.1.1)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Context (5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Environment (5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.FakeVar (5.22)
|
||||
Fake.Core.Context (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Process (5.22)
|
||||
Fake.Core.Environment (>= 5.22)
|
||||
Fake.Core.FakeVar (>= 5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
Fake.IO.FileSystem (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
System.Collections.Immutable (>= 5.0)
|
||||
Fake.Core.SemVer (5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.String (5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Target (5.22)
|
||||
Fake.Core.CommandLineParsing (>= 5.22)
|
||||
Fake.Core.Context (>= 5.22)
|
||||
Fake.Core.Environment (>= 5.22)
|
||||
Fake.Core.FakeVar (>= 5.22)
|
||||
Fake.Core.Process (>= 5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
FSharp.Control.Reactive (>= 5.0.2)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Tasks (5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Trace (5.22)
|
||||
Fake.Core.Environment (>= 5.22)
|
||||
Fake.Core.FakeVar (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Core.Xml (5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.DotNet.Cli (5.22)
|
||||
Fake.Core.Environment (>= 5.22)
|
||||
Fake.Core.Process (>= 5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
Fake.DotNet.MSBuild (>= 5.22)
|
||||
Fake.DotNet.NuGet (>= 5.22)
|
||||
Fake.IO.FileSystem (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Mono.Posix.NETStandard (>= 1.0)
|
||||
Newtonsoft.Json (>= 13.0.1)
|
||||
Fake.DotNet.MSBuild (5.22)
|
||||
BlackFox.VsWhere (>= 1.1)
|
||||
Fake.Core.Environment (>= 5.22)
|
||||
Fake.Core.Process (>= 5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
Fake.IO.FileSystem (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
MSBuild.StructuredLogger (>= 2.1.545)
|
||||
Fake.DotNet.NuGet (5.22)
|
||||
Fake.Core.Environment (>= 5.22)
|
||||
Fake.Core.Process (>= 5.22)
|
||||
Fake.Core.SemVer (>= 5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
Fake.Core.Tasks (>= 5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
Fake.Core.Xml (>= 5.22)
|
||||
Fake.IO.FileSystem (>= 5.22)
|
||||
Fake.Net.Http (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Newtonsoft.Json (>= 13.0.1)
|
||||
NuGet.Protocol (>= 5.11)
|
||||
Fake.IO.FileSystem (5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.IO.Zip (5.22)
|
||||
Fake.Core.String (>= 5.22)
|
||||
Fake.IO.FileSystem (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
Fake.Net.Http (5.22)
|
||||
Fake.Core.Trace (>= 5.22)
|
||||
FSharp.Core (>= 6.0)
|
||||
FParsec (1.1.1)
|
||||
FSharp.Core (>= 4.3.4)
|
||||
FSharp.Control.Reactive (5.0.5)
|
||||
FSharp.Core (>= 4.7.2)
|
||||
System.Reactive (>= 5.0 < 6.0)
|
||||
FSharp.Core (6.0.5)
|
||||
Microsoft.Build (17.2)
|
||||
Microsoft.Build.Framework (>= 17.2) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
Microsoft.NET.StringTools (>= 1.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
Microsoft.Win32.Registry (>= 4.3) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Collections.Immutable (>= 5.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Configuration.ConfigurationManager (>= 4.7) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Reflection.Metadata (>= 1.6) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Security.Principal.Windows (>= 4.7) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Text.Encoding.CodePages (>= 4.0.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Text.Json (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Threading.Tasks.Dataflow (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
Microsoft.Build.Framework (17.2)
|
||||
Microsoft.Win32.Registry (>= 4.3)
|
||||
System.Security.Permissions (>= 4.7)
|
||||
Microsoft.Build.Tasks.Core (17.2)
|
||||
Microsoft.Build.Framework (>= 17.2)
|
||||
Microsoft.Build.Utilities.Core (>= 17.2)
|
||||
Microsoft.NET.StringTools (>= 1.0)
|
||||
Microsoft.Win32.Registry (>= 4.3)
|
||||
System.CodeDom (>= 4.4)
|
||||
System.Collections.Immutable (>= 5.0)
|
||||
System.Reflection.Metadata (>= 1.6)
|
||||
System.Resources.Extensions (>= 4.6)
|
||||
System.Security.Cryptography.Pkcs (>= 4.7)
|
||||
System.Security.Cryptography.Xml (>= 4.7)
|
||||
System.Security.Permissions (>= 4.7)
|
||||
System.Threading.Tasks.Dataflow (>= 6.0)
|
||||
Microsoft.Build.Utilities.Core (17.2)
|
||||
Microsoft.Build.Framework (>= 17.2)
|
||||
Microsoft.NET.StringTools (>= 1.0)
|
||||
Microsoft.Win32.Registry (>= 4.3)
|
||||
System.Collections.Immutable (>= 5.0)
|
||||
System.Configuration.ConfigurationManager (>= 4.7)
|
||||
System.Security.Permissions (>= 4.7) - restriction: == netstandard2.0
|
||||
System.Text.Encoding.CodePages (>= 4.0.1) - restriction: == netstandard2.0
|
||||
Microsoft.NET.StringTools (1.0)
|
||||
System.Memory (>= 4.5.4)
|
||||
System.Runtime.CompilerServices.Unsafe (>= 5.0)
|
||||
Microsoft.NETCore.Platforms (6.0.4) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard1.2)) (&& (== net6.0) (< netstandard1.3)) (&& (== net6.0) (< netstandard1.5)) (== netstandard2.0)
|
||||
Microsoft.NETCore.Targets (5.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard1.2)) (&& (== net6.0) (< netstandard1.3)) (&& (== net6.0) (< netstandard1.5)) (== netstandard2.0)
|
||||
Microsoft.Win32.Registry (5.0)
|
||||
System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monoandroid) (< netstandard1.3)) (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0)
|
||||
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0)
|
||||
System.Security.AccessControl (>= 5.0)
|
||||
System.Security.Principal.Windows (>= 5.0)
|
||||
Microsoft.Win32.SystemEvents (6.0.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1))
|
||||
Mono.Posix.NETStandard (1.0)
|
||||
MSBuild.StructuredLogger (2.1.669)
|
||||
Microsoft.Build (>= 16.10)
|
||||
Microsoft.Build.Framework (>= 16.10)
|
||||
Microsoft.Build.Tasks.Core (>= 16.10)
|
||||
Microsoft.Build.Utilities.Core (>= 16.10)
|
||||
Newtonsoft.Json (13.0.1)
|
||||
NuGet.Common (6.2.1)
|
||||
NuGet.Frameworks (>= 6.2.1)
|
||||
NuGet.Configuration (6.2.1)
|
||||
NuGet.Common (>= 6.2.1)
|
||||
System.Security.Cryptography.ProtectedData (>= 4.4)
|
||||
NuGet.Frameworks (6.2.1)
|
||||
NuGet.Packaging (6.2.1)
|
||||
Newtonsoft.Json (>= 13.0.1)
|
||||
NuGet.Configuration (>= 6.2.1)
|
||||
NuGet.Versioning (>= 6.2.1)
|
||||
System.Security.Cryptography.Cng (>= 5.0)
|
||||
System.Security.Cryptography.Pkcs (>= 5.0)
|
||||
NuGet.Protocol (6.2.1)
|
||||
NuGet.Packaging (>= 6.2.1)
|
||||
NuGet.Versioning (6.2.1)
|
||||
System.Buffers (4.5.1) - restriction: || (&& (== net6.0) (>= monoandroid) (< netstandard1.3)) (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0)
|
||||
System.CodeDom (6.0)
|
||||
System.Collections.Immutable (6.0)
|
||||
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0)
|
||||
System.Runtime.CompilerServices.Unsafe (>= 6.0)
|
||||
System.Configuration.ConfigurationManager (6.0)
|
||||
System.Security.Cryptography.ProtectedData (>= 6.0)
|
||||
System.Security.Permissions (>= 6.0)
|
||||
System.Drawing.Common (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1))
|
||||
Microsoft.Win32.SystemEvents (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1))
|
||||
System.Formats.Asn1 (6.0)
|
||||
System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0)
|
||||
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0)
|
||||
System.Memory (4.5.5)
|
||||
System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0)
|
||||
System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (== netstandard2.0)
|
||||
System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0)
|
||||
System.Numerics.Vectors (4.5) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0)
|
||||
System.Reactive (5.0)
|
||||
System.Runtime.InteropServices.WindowsRuntime (>= 4.3) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (== netstandard2.0)
|
||||
System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net472)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0)
|
||||
System.Reflection.Metadata (6.0.1)
|
||||
System.Collections.Immutable (>= 6.0)
|
||||
System.Resources.Extensions (6.0)
|
||||
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0)
|
||||
System.Runtime (4.3.1) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (== netstandard2.0)
|
||||
Microsoft.NETCore.Platforms (>= 1.1.1)
|
||||
Microsoft.NETCore.Targets (>= 1.1.3)
|
||||
System.Runtime.CompilerServices.Unsafe (6.0)
|
||||
System.Runtime.InteropServices.WindowsRuntime (4.3) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (== netstandard2.0)
|
||||
System.Runtime (>= 4.3)
|
||||
System.Security.AccessControl (6.0)
|
||||
System.Security.Principal.Windows (>= 5.0) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0)
|
||||
System.Security.Cryptography.Cng (5.0)
|
||||
System.Formats.Asn1 (>= 5.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.0))
|
||||
System.Security.Cryptography.Pkcs (6.0.1)
|
||||
System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0)
|
||||
System.Formats.Asn1 (>= 6.0)
|
||||
System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0)
|
||||
System.Security.Cryptography.Cng (>= 5.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0)
|
||||
System.Security.Cryptography.ProtectedData (6.0)
|
||||
System.Security.Cryptography.Xml (6.0)
|
||||
System.Memory (>= 4.5.4) - restriction: == netstandard2.0
|
||||
System.Security.AccessControl (>= 6.0)
|
||||
System.Security.Cryptography.Pkcs (>= 6.0)
|
||||
System.Security.Permissions (6.0)
|
||||
System.Security.AccessControl (>= 6.0)
|
||||
System.Windows.Extensions (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1))
|
||||
System.Security.Principal.Windows (5.0)
|
||||
System.Text.Encoding.CodePages (6.0)
|
||||
System.Runtime.CompilerServices.Unsafe (>= 6.0)
|
||||
System.Text.Encodings.Web (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Runtime.CompilerServices.Unsafe (>= 6.0)
|
||||
System.Text.Json (6.0.5) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0))
|
||||
System.Runtime.CompilerServices.Unsafe (>= 6.0)
|
||||
System.Text.Encodings.Web (>= 6.0)
|
||||
System.Threading.Tasks.Dataflow (6.0)
|
||||
System.Threading.Tasks.Extensions (4.5.4) - restriction: || (&& (== net6.0) (>= net472)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0)
|
||||
System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.0)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= wp8)) (== netstandard2.0)
|
||||
System.Windows.Extensions (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1))
|
||||
System.Drawing.Common (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1))
|
7
fake.sh
7
fake.sh
|
@ -1,7 +0,0 @@
|
|||
#!/usr/bin/env bash
|
||||
|
||||
set -eu
|
||||
set -o pipefail
|
||||
|
||||
dotnet tool restore
|
||||
dotnet fake "$@"
|
|
@ -1,10 +1,9 @@
|
|||
<Project>
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net6.0</TargetFramework>
|
||||
<TargetFrameworks>net6.0;net7.0</TargetFrameworks>
|
||||
<DebugType>embedded</DebugType>
|
||||
<AssemblyVersion>2.0.0.0</AssemblyVersion>
|
||||
<FileVersion>2.0.0.0</FileVersion>
|
||||
<Version>2.0.0</Version>
|
||||
<VersionSuffix>rc2</VersionSuffix>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
|
|
@ -165,6 +165,7 @@ module Json =
|
|||
Converters = ser.Converters,
|
||||
DefaultValueHandling = ser.DefaultValueHandling,
|
||||
DateFormatHandling = ser.DateFormatHandling,
|
||||
DateParseHandling = ser.DateParseHandling,
|
||||
MetadataPropertyHandling = ser.MetadataPropertyHandling,
|
||||
MissingMemberHandling = ser.MissingMemberHandling,
|
||||
NullValueHandling = ser.NullValueHandling,
|
||||
|
|
|
@ -5,19 +5,16 @@
|
|||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="6.0.8" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="6.0.0" />
|
||||
<PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="1.0.0-beta2" />
|
||||
<PackageReference Include="Microsoft.Data.Sqlite" Version="7.0.3" />
|
||||
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="7.0.0" />
|
||||
<PackageReference Include="Microsoft.FSharpLu.Json" Version="0.11.7" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
|
||||
<PackageReference Include="NodaTime" Version="3.1.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.0" />
|
||||
<PackageReference Include="Npgsql" Version="6.0.6" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="5.3.0" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="6.0.6" />
|
||||
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
|
||||
<PackageReference Include="NodaTime.Serialization.JsonNet" Version="3.0.1" />
|
||||
<PackageReference Include="Npgsql.NodaTime" Version="7.0.2" />
|
||||
<PackageReference Include="RethinkDb.Driver" Version="2.3.150" />
|
||||
<PackageReference Include="RethinkDb.Driver.FSharp" Version="0.9.0-beta-07" />
|
||||
<PackageReference Update="FSharp.Core" Version="6.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
@ -45,7 +42,7 @@
|
|||
<Compile Include="Postgres\PostgresUploadData.fs" />
|
||||
<Compile Include="Postgres\PostgresWebLogData.fs" />
|
||||
<Compile Include="Postgres\PostgresWebLogUserData.fs" />
|
||||
<Compile Include="PostgresData.fs" />
|
||||
<Compile Include="PostgresData.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
|
|
@ -2,6 +2,7 @@ namespace MyWebLog.Data.Postgres
|
|||
|
||||
open System.Threading
|
||||
open System.Threading.Tasks
|
||||
open BitBadger.Npgsql.FSharp.Documents
|
||||
open Microsoft.Extensions.Caching.Distributed
|
||||
open NodaTime
|
||||
open Npgsql.FSharp
|
||||
|
@ -40,32 +41,26 @@ module private Helpers =
|
|||
|
||||
|
||||
/// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog
|
||||
type DistributedCache (connStr : string) =
|
||||
type DistributedCache () =
|
||||
|
||||
// ~~~ INITIALIZATION ~~~
|
||||
|
||||
do
|
||||
task {
|
||||
let! exists =
|
||||
Sql.connect connStr
|
||||
|> Sql.query $"
|
||||
SELECT EXISTS
|
||||
Custom.scalar
|
||||
$"SELECT EXISTS
|
||||
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
|
||||
AS {existsName}"
|
||||
|> Sql.executeRowAsync Map.toExists
|
||||
AS {existsName}" [] Map.toExists
|
||||
if not exists then
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query
|
||||
do! Custom.nonQuery
|
||||
"CREATE TABLE session (
|
||||
id TEXT NOT NULL PRIMARY KEY,
|
||||
payload BYTEA NOT NULL,
|
||||
expire_at TIMESTAMPTZ NOT NULL,
|
||||
sliding_expiration INTERVAL,
|
||||
absolute_expiration TIMESTAMPTZ);
|
||||
CREATE INDEX idx_session_expiration ON session (expire_at)"
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
CREATE INDEX idx_session_expiration ON session (expire_at)" []
|
||||
} |> sync
|
||||
|
||||
// ~~~ SUPPORT FUNCTIONS ~~~
|
||||
|
@ -74,16 +69,13 @@ type DistributedCache (connStr : string) =
|
|||
let getEntry key = backgroundTask {
|
||||
let idParam = "@id", Sql.string key
|
||||
let! tryEntry =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "SELECT * FROM session WHERE id = @id"
|
||||
|> Sql.parameters [ idParam ]
|
||||
|> Sql.executeAsync (fun row ->
|
||||
{ Id = row.string "id"
|
||||
Payload = row.bytea "payload"
|
||||
ExpireAt = row.fieldValue<Instant> "expire_at"
|
||||
SlidingExpiration = row.fieldValueOrNone<Duration> "sliding_expiration"
|
||||
AbsoluteExpiration = row.fieldValueOrNone<Instant> "absolute_expiration" })
|
||||
|> tryHead
|
||||
Custom.single "SELECT * FROM session WHERE id = @id" [ idParam ]
|
||||
(fun row ->
|
||||
{ Id = row.string "id"
|
||||
Payload = row.bytea "payload"
|
||||
ExpireAt = row.fieldValue<Instant> "expire_at"
|
||||
SlidingExpiration = row.fieldValueOrNone<Duration> "sliding_expiration"
|
||||
AbsoluteExpiration = row.fieldValueOrNone<Instant> "absolute_expiration" })
|
||||
match tryEntry with
|
||||
| Some entry ->
|
||||
let now = getNow ()
|
||||
|
@ -96,11 +88,8 @@ type DistributedCache (connStr : string) =
|
|||
true, { entry with ExpireAt = absExp }
|
||||
else true, { entry with ExpireAt = now.Plus slideExp }
|
||||
if needsRefresh then
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
||||
|> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
do! Custom.nonQuery "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|
||||
[ expireParam item.ExpireAt; idParam ]
|
||||
()
|
||||
return if item.ExpireAt > now then Some entry else None
|
||||
| None -> return None
|
||||
|
@ -113,26 +102,16 @@ type DistributedCache (connStr : string) =
|
|||
let purge () = backgroundTask {
|
||||
let now = getNow ()
|
||||
if lastPurge.Plus (Duration.FromMinutes 30L) < now then
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|
||||
|> Sql.parameters [ expireParam now ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
do! Custom.nonQuery "DELETE FROM session WHERE expire_at < @expireAt" [ expireParam now ]
|
||||
lastPurge <- now
|
||||
}
|
||||
|
||||
/// Remove a cache entry
|
||||
let removeEntry key = backgroundTask {
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query "DELETE FROM session WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.string key ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
}
|
||||
let removeEntry key =
|
||||
Delete.byId "session" key
|
||||
|
||||
/// Save an entry
|
||||
let saveEntry (opts : DistributedCacheEntryOptions) key payload = backgroundTask {
|
||||
let saveEntry (opts : DistributedCacheEntryOptions) key payload =
|
||||
let now = getNow ()
|
||||
let expireAt, slideExp, absExp =
|
||||
if opts.SlidingExpiration.HasValue then
|
||||
|
@ -148,27 +127,21 @@ type DistributedCache (connStr : string) =
|
|||
// Default to 1 hour sliding expiration
|
||||
let slide = Duration.FromHours 1
|
||||
now.Plus slide, Some slide, None
|
||||
let! _ =
|
||||
Sql.connect connStr
|
||||
|> Sql.query
|
||||
"INSERT INTO session (
|
||||
id, payload, expire_at, sliding_expiration, absolute_expiration
|
||||
) VALUES (
|
||||
@id, @payload, @expireAt, @slideExp, @absExp
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET payload = EXCLUDED.payload,
|
||||
expire_at = EXCLUDED.expire_at,
|
||||
sliding_expiration = EXCLUDED.sliding_expiration,
|
||||
absolute_expiration = EXCLUDED.absolute_expiration"
|
||||
|> Sql.parameters
|
||||
[ "@id", Sql.string key
|
||||
"@payload", Sql.bytea payload
|
||||
expireParam expireAt
|
||||
optParam "slideExp" slideExp
|
||||
optParam "absExp" absExp ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
}
|
||||
Custom.nonQuery
|
||||
"INSERT INTO session (
|
||||
id, payload, expire_at, sliding_expiration, absolute_expiration
|
||||
) VALUES (
|
||||
@id, @payload, @expireAt, @slideExp, @absExp
|
||||
) ON CONFLICT (id) DO UPDATE
|
||||
SET payload = EXCLUDED.payload,
|
||||
expire_at = EXCLUDED.expire_at,
|
||||
sliding_expiration = EXCLUDED.sliding_expiration,
|
||||
absolute_expiration = EXCLUDED.absolute_expiration"
|
||||
[ "@id", Sql.string key
|
||||
"@payload", Sql.bytea payload
|
||||
expireParam expireAt
|
||||
optParam "slideExp" slideExp
|
||||
optParam "absExp" absExp ]
|
||||
|
||||
// ~~~ IMPLEMENTATION FUNCTIONS ~~~
|
||||
|
||||
|
@ -200,11 +173,11 @@ type DistributedCache (connStr : string) =
|
|||
}
|
||||
|
||||
interface IDistributedCache with
|
||||
member this.Get key = get key CancellationToken.None |> sync
|
||||
member this.GetAsync (key, token) = get key token
|
||||
member this.Refresh key = refresh key CancellationToken.None |> sync
|
||||
member this.RefreshAsync (key, token) = refresh key token
|
||||
member this.Remove key = remove key CancellationToken.None |> sync
|
||||
member this.RemoveAsync (key, token) = remove key token
|
||||
member this.Set (key, value, options) = set key value options CancellationToken.None |> sync
|
||||
member this.SetAsync (key, value, options, token) = set key value options token
|
||||
member _.Get key = get key CancellationToken.None |> sync
|
||||
member _.GetAsync (key, token) = get key token
|
||||
member _.Refresh key = refresh key CancellationToken.None |> sync
|
||||
member _.RefreshAsync (key, token) = refresh key token
|
||||
member _.Remove key = remove key CancellationToken.None |> sync
|
||||
member _.RemoveAsync (key, token) = remove key token
|
||||
member _.Set (key, value, options) = set key value options CancellationToken.None |> sync
|
||||
member _.SetAsync (key, value, options, token) = set key value options token
|
||||
|
|
|
@ -1,34 +1,30 @@
|
|||
namespace MyWebLog.Data.Postgres
|
||||
|
||||
open BitBadger.Npgsql.FSharp.Documents
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
open MyWebLog.Data
|
||||
open Npgsql
|
||||
open Npgsql.FSharp
|
||||
|
||||
/// PostgreSQL myWebLog category data implementation
|
||||
type PostgresCategoryData (conn : NpgsqlConnection) =
|
||||
type PostgresCategoryData (log : ILogger) =
|
||||
|
||||
/// Count all categories for the given web log
|
||||
let countAll webLogId =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"SELECT COUNT(id) AS {countName} FROM category WHERE web_log_id = @webLogId"
|
||||
|> Sql.parameters [ webLogIdParam webLogId ]
|
||||
|> Sql.executeRowAsync Map.toCount
|
||||
log.LogTrace "Category.countAll"
|
||||
Count.byContains Table.Category (webLogDoc webLogId)
|
||||
|
||||
/// Count all top-level categories for the given web log
|
||||
let countTopLevel webLogId =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"SELECT COUNT(id) AS {countName} FROM category WHERE web_log_id = @webLogId AND parent_id IS NULL"
|
||||
|> Sql.parameters [ webLogIdParam webLogId ]
|
||||
|> Sql.executeRowAsync Map.toCount
|
||||
log.LogTrace "Category.countTopLevel"
|
||||
Count.byContains Table.Category {| webLogDoc webLogId with ParentId = None |}
|
||||
|
||||
/// Retrieve all categories for the given web log in a DotLiquid-friendly format
|
||||
let findAllForView webLogId = backgroundTask {
|
||||
log.LogTrace "Category.findAllForView"
|
||||
let! cats =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM category WHERE web_log_id = @webLogId ORDER BY LOWER(name)"
|
||||
|> Sql.parameters [ webLogIdParam webLogId ]
|
||||
|> Sql.executeAsync Map.toCategory
|
||||
Custom.list $"{selectWithCriteria Table.Category} ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')"
|
||||
[ webLogContains webLogId ] fromData<Category>
|
||||
let ordered = Utils.orderByHierarchy cats None None []
|
||||
let counts =
|
||||
ordered
|
||||
|
@ -40,18 +36,17 @@ type PostgresCategoryData (conn : NpgsqlConnection) =
|
|||
|> Seq.map (fun cat -> cat.Id)
|
||||
|> Seq.append (Seq.singleton it.Id)
|
||||
|> List.ofSeq
|
||||
|> inClause "AND pc.category_id" "id" id
|
||||
|> arrayContains (nameof Post.empty.CategoryIds) id
|
||||
let postCount =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"
|
||||
SELECT COUNT(DISTINCT p.id) AS {countName}
|
||||
FROM post p
|
||||
INNER JOIN post_category pc ON pc.post_id = p.id
|
||||
WHERE p.web_log_id = @webLogId
|
||||
AND p.status = 'Published'
|
||||
{catIdSql}"
|
||||
|> Sql.parameters (webLogIdParam webLogId :: catIdParams)
|
||||
|> Sql.executeRowAsync Map.toCount
|
||||
Custom.scalar
|
||||
$"""SELECT COUNT(DISTINCT id) AS {countName}
|
||||
FROM {Table.Post}
|
||||
WHERE {Query.whereDataContains "@criteria"}
|
||||
AND {catIdSql}"""
|
||||
[ "@criteria",
|
||||
Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
|
||||
catIdParams
|
||||
] Map.toCount
|
||||
|> Async.AwaitTask
|
||||
|> Async.RunSynchronously
|
||||
it.Id, postCount)
|
||||
|
@ -69,93 +64,75 @@ type PostgresCategoryData (conn : NpgsqlConnection) =
|
|||
}
|
||||
/// Find a category by its ID for the given web log
|
||||
let findById catId webLogId =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM category WHERE id = @id AND web_log_id = @webLogId"
|
||||
|> Sql.parameters [ "@id", Sql.string (CategoryId.toString catId); webLogIdParam webLogId ]
|
||||
|> Sql.executeAsync Map.toCategory
|
||||
|> tryHead
|
||||
log.LogTrace "Category.findById"
|
||||
Document.findByIdAndWebLog<CategoryId, Category> Table.Category catId CategoryId.toString webLogId
|
||||
|
||||
/// Find all categories for the given web log
|
||||
let findByWebLog webLogId =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "SELECT * FROM category WHERE web_log_id = @webLogId"
|
||||
|> Sql.parameters [ webLogIdParam webLogId ]
|
||||
|> Sql.executeAsync Map.toCategory
|
||||
log.LogTrace "Category.findByWebLog"
|
||||
Document.findByWebLog<Category> Table.Category webLogId
|
||||
|
||||
/// Create parameters for a category insert / update
|
||||
let catParameters (cat : Category) =
|
||||
Query.docParameters (CategoryId.toString cat.Id) cat
|
||||
|
||||
/// Delete a category
|
||||
let delete catId webLogId = backgroundTask {
|
||||
log.LogTrace "Category.delete"
|
||||
match! findById catId webLogId with
|
||||
| Some cat ->
|
||||
// Reassign any children to the category's parent category
|
||||
let parentParam = "@parentId", Sql.string (CategoryId.toString catId)
|
||||
let! hasChildren =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"SELECT EXISTS (SELECT 1 FROM category WHERE parent_id = @parentId) AS {existsName}"
|
||||
|> Sql.parameters [ parentParam ]
|
||||
|> Sql.executeRowAsync Map.toExists
|
||||
let! children = Find.byContains<Category> Table.Category {| ParentId = CategoryId.toString catId |}
|
||||
let hasChildren = not (List.isEmpty children)
|
||||
if hasChildren then
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query "UPDATE category SET parent_id = @newParentId WHERE parent_id = @parentId"
|
||||
|> Sql.parameters
|
||||
[ parentParam
|
||||
"@newParentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString) ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync [
|
||||
Query.Update.partialById Table.Category,
|
||||
children |> List.map (fun child -> [
|
||||
"@id", Sql.string (CategoryId.toString child.Id)
|
||||
"@data", Query.jsonbDocParam {| ParentId = cat.ParentId |}
|
||||
])
|
||||
]
|
||||
()
|
||||
// Delete the category off all posts where it is assigned, and the category itself
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query
|
||||
"DELETE FROM post_category
|
||||
WHERE category_id = @id
|
||||
AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId);
|
||||
DELETE FROM category WHERE id = @id"
|
||||
|> Sql.parameters [ "@id", Sql.string (CategoryId.toString catId); webLogIdParam webLogId ]
|
||||
|> Sql.executeNonQueryAsync
|
||||
// Delete the category off all posts where it is assigned
|
||||
let! posts =
|
||||
Custom.list $"SELECT data FROM {Table.Post} WHERE data -> '{nameof Post.empty.CategoryIds}' @> @id"
|
||||
[ "@id", Query.jsonbDocParam [| CategoryId.toString catId |] ] fromData<Post>
|
||||
if not (List.isEmpty posts) then
|
||||
let! _ =
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync [
|
||||
Query.Update.partialById Table.Post,
|
||||
posts |> List.map (fun post -> [
|
||||
"@id", Sql.string (PostId.toString post.Id)
|
||||
"@data", Query.jsonbDocParam
|
||||
{| CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) |}
|
||||
])
|
||||
]
|
||||
()
|
||||
// Delete the category itself
|
||||
do! Delete.byId Table.Category (CategoryId.toString catId)
|
||||
return if hasChildren then ReassignedChildCategories else CategoryDeleted
|
||||
| None -> return CategoryNotFound
|
||||
}
|
||||
|
||||
/// The INSERT statement for a category
|
||||
let catInsert =
|
||||
"INSERT INTO category (
|
||||
id, web_log_id, name, slug, description, parent_id
|
||||
) VALUES (
|
||||
@id, @webLogId, @name, @slug, @description, @parentId
|
||||
)"
|
||||
|
||||
/// Create parameters for a category insert / update
|
||||
let catParameters (cat : Category) = [
|
||||
webLogIdParam cat.WebLogId
|
||||
"@id", Sql.string (CategoryId.toString cat.Id)
|
||||
"@name", Sql.string cat.Name
|
||||
"@slug", Sql.string cat.Slug
|
||||
"@description", Sql.stringOrNone cat.Description
|
||||
"@parentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString)
|
||||
]
|
||||
|
||||
/// Save a category
|
||||
let save cat = backgroundTask {
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
|> Sql.query $"
|
||||
{catInsert} ON CONFLICT (id) DO UPDATE
|
||||
SET name = EXCLUDED.name,
|
||||
slug = EXCLUDED.slug,
|
||||
description = EXCLUDED.description,
|
||||
parent_id = EXCLUDED.parent_id"
|
||||
|> Sql.parameters (catParameters cat)
|
||||
|> Sql.executeNonQueryAsync
|
||||
()
|
||||
let save (cat : Category) = backgroundTask {
|
||||
log.LogTrace "Category.save"
|
||||
do! save Table.Category (CategoryId.toString cat.Id) cat
|
||||
}
|
||||
|
||||
/// Restore categories from a backup
|
||||
let restore cats = backgroundTask {
|
||||
log.LogTrace "Category.restore"
|
||||
let! _ =
|
||||
Sql.existingConnection conn
|
||||
Configuration.dataSource ()
|
||||
|> Sql.fromDataSource
|
||||
|> Sql.executeTransactionAsync [
|
||||
catInsert, cats |> List.map catParameters
|
||||
Query.insert Table.Category, cats |> List.map catParameters
|
||||
]
|
||||
()
|
||||
}
|
||||
|
|
|
@ -2,11 +2,68 @@
|
|||
[<AutoOpen>]
|
||||
module MyWebLog.Data.Postgres.PostgresHelpers
|
||||
|
||||
/// The table names used in the PostgreSQL implementation
|
||||
[<RequireQualifiedAccess>]
|
||||
module Table =
|
||||
|
||||
/// Categories
|
||||
[<Literal>]
|
||||
let Category = "category"
|
||||
|
||||
/// Database Version
|
||||
[<Literal>]
|
||||
let DbVersion = "db_version"
|
||||
|
||||
/// Pages
|
||||
[<Literal>]
|
||||
let Page = "page"
|
||||
|
||||
/// Page Revisions
|
||||
[<Literal>]
|
||||
let PageRevision = "page_revision"
|
||||
|
||||
/// Posts
|
||||
[<Literal>]
|
||||
let Post = "post"
|
||||
|
||||
/// Post Comments
|
||||
[<Literal>]
|
||||
let PostComment = "post_comment"
|
||||
|
||||
/// Post Revisions
|
||||
[<Literal>]
|
||||
let PostRevision = "post_revision"
|
||||
|
||||
/// Tag/URL Mappings
|
||||
[<Literal>]
|
||||
let TagMap = "tag_map"
|
||||
|
||||
/// Themes
|
||||
[<Literal>]
|
||||
let Theme = "theme"
|
||||
|
||||
/// Theme Assets
|
||||
[<Literal>]
|
||||
let ThemeAsset = "theme_asset"
|
||||
|
||||
/// Uploads
|
||||
[<Literal>]
|
||||
let Upload = "upload"
|
||||
|
||||
/// Web Logs
|
||||
[<Literal>]
|
||||
let WebLog = "web_log"
|
||||
|
||||
/// Users
|
||||
[<Literal>]
|
||||
let WebLogUser = "web_log_user"
|
||||
|
||||
|
||||
open System
|
||||
open System.Threading.Tasks
|
||||
open BitBadger.Npgsql.FSharp.Documents
|
||||
open MyWebLog
|
||||
open MyWebLog.Data
|
||||
open Newtonsoft.Json
|
||||
open NodaTime
|
||||
open Npgsql
|
||||
open Npgsql.FSharp
|
||||
|
@ -15,12 +72,24 @@ open Npgsql.FSharp
|
|||
let webLogIdParam webLogId =
|
||||
"@webLogId", Sql.string (WebLogId.toString webLogId)
|
||||
|
||||
/// Create an anonymous record with the given web log ID
|
||||
let webLogDoc (webLogId : WebLogId) =
|
||||
{| WebLogId = webLogId |}
|
||||
|
||||
/// Create a parameter for a web log document-contains query
|
||||
let webLogContains webLogId =
|
||||
"@criteria", Query.jsonbDocParam (webLogDoc webLogId)
|
||||
|
||||
/// The name of the field to select to be able to use Map.toCount
|
||||
let countName = "the_count"
|
||||
|
||||
/// The name of the field to select to be able to use Map.toExists
|
||||
let existsName = "does_exist"
|
||||
|
||||
/// A SQL string to select data from a table with the given JSON document contains criteria
|
||||
let selectWithCriteria tableName =
|
||||
$"""{Query.selectFromTable tableName} WHERE {Query.whereDataContains "@criteria"}"""
|
||||
|
||||
/// Create the SQL and parameters for an IN clause
|
||||
let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : 'T list) =
|
||||
if List.isEmpty items then "", []
|
||||
|
@ -37,22 +106,11 @@ let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : '
|
|||
|> Seq.head)
|
||||
|> function sql, ps -> $"{sql})", ps
|
||||
|
||||
/// Create the SQL and parameters for the array equivalent of an IN clause
|
||||
let arrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) =
|
||||
if List.isEmpty items then "TRUE = FALSE", []
|
||||
else
|
||||
let mutable idx = 0
|
||||
items
|
||||
|> List.skip 1
|
||||
|> List.fold (fun (itemS, itemP) it ->
|
||||
idx <- idx + 1
|
||||
$"{itemS} OR %s{name} && ARRAY[@{name}{idx}]",
|
||||
($"@{name}{idx}", Sql.string (valueFunc it)) :: itemP)
|
||||
(Seq.ofList items
|
||||
|> Seq.map (fun it ->
|
||||
$"{name} && ARRAY[@{name}0]", [ $"@{name}0", Sql.string (valueFunc it) ])
|
||||
|> Seq.head)
|
||||
|
||||
/// Create the SQL and parameters for match-any array query
|
||||
let arrayContains<'T> name (valueFunc : 'T -> string) (items : 'T list) =
|
||||
$"data['{name}'] ?| @{name}Values",
|
||||
($"@{name}Values", Sql.stringArray (items |> List.map valueFunc |> Array.ofList))
|
||||
|
||||
/// Get the first result of the given query
|
||||
let tryHead<'T> (query : Task<'T list>) = backgroundTask {
|
||||
let! results = query
|
||||
|
@ -71,113 +129,24 @@ let optParam<'T> name (it : 'T option) =
|
|||
/// Mapping functions for SQL queries
|
||||
module Map =
|
||||
|
||||
/// Map an id field to a category ID
|
||||
let toCategoryId (row : RowReader) =
|
||||
CategoryId (row.string "id")
|
||||
|
||||
/// Create a category from the current row
|
||||
let toCategory (row : RowReader) : Category =
|
||||
{ Id = toCategoryId row
|
||||
WebLogId = row.string "web_log_id" |> WebLogId
|
||||
Name = row.string "name"
|
||||
Slug = row.string "slug"
|
||||
Description = row.stringOrNone "description"
|
||||
ParentId = row.stringOrNone "parent_id" |> Option.map CategoryId
|
||||
}
|
||||
|
||||
/// Get a count from a row
|
||||
let toCount (row : RowReader) =
|
||||
row.int countName
|
||||
|
||||
/// Create a custom feed from the current row
|
||||
let toCustomFeed (ser : JsonSerializer) (row : RowReader) : CustomFeed =
|
||||
{ Id = row.string "id" |> CustomFeedId
|
||||
Source = row.string "source" |> CustomFeedSource.parse
|
||||
Path = row.string "path" |> Permalink
|
||||
Podcast = row.stringOrNone "podcast" |> Option.map (Utils.deserialize ser)
|
||||
}
|
||||
|
||||
/// Get a true/false value as to whether an item exists
|
||||
let toExists (row : RowReader) =
|
||||
row.bool existsName
|
||||
|
||||
/// Create a meta item from the current row
|
||||
let toMetaItem (row : RowReader) : MetaItem =
|
||||
{ Name = row.string "name"
|
||||
Value = row.string "value"
|
||||
}
|
||||
|
||||
/// Create a permalink from the current row
|
||||
let toPermalink (row : RowReader) =
|
||||
Permalink (row.string "permalink")
|
||||
|
||||
/// Create a page from the current row
|
||||
let toPage (ser : JsonSerializer) (row : RowReader) : Page =
|
||||
{ Page.empty with
|
||||
Id = row.string "id" |> PageId
|
||||
WebLogId = row.string "web_log_id" |> WebLogId
|
||||
AuthorId = row.string "author_id" |> WebLogUserId
|
||||
Title = row.string "title"
|
||||
Permalink = toPermalink row
|
||||
PriorPermalinks = row.stringArray "prior_permalinks" |> Array.map Permalink |> List.ofArray
|
||||
PublishedOn = row.fieldValue<Instant> "published_on"
|
||||
UpdatedOn = row.fieldValue<Instant> "updated_on"
|
||||
IsInPageList = row.bool "is_in_page_list"
|
||||
Template = row.stringOrNone "template"
|
||||
Text = row.string "page_text"
|
||||
Metadata = row.stringOrNone "meta_items"
|
||||
|> Option.map (Utils.deserialize ser)
|
||||
|> Option.defaultValue []
|
||||
}
|
||||
|
||||
/// Create a post from the current row
|
||||
let toPost (ser : JsonSerializer) (row : RowReader) : Post =
|
||||
{ Post.empty with
|
||||
Id = row.string "id" |> PostId
|
||||
WebLogId = row.string "web_log_id" |> WebLogId
|
||||
AuthorId = row.string "author_id" |> WebLogUserId
|
||||
Status = row.string "status" |> PostStatus.parse
|
||||
Title = row.string "title"
|
||||
Permalink = toPermalink row
|
||||
PriorPermalinks = row.stringArray "prior_permalinks" |> Array.map Permalink |> List.ofArray
|
||||
PublishedOn = row.fieldValueOrNone<Instant> "published_on"
|
||||
UpdatedOn = row.fieldValue<Instant> "updated_on"
|
||||
Template = row.stringOrNone "template"
|
||||
Text = row.string "post_text"
|
||||
Episode = row.stringOrNone "episode" |> Option.map (Utils.deserialize ser)
|
||||
CategoryIds = row.stringArrayOrNone "category_ids"
|
||||
|> Option.map (Array.map CategoryId >> List.ofArray)
|
||||
|> Option.defaultValue []
|
||||
Tags = row.stringArrayOrNone "tags"
|
||||
|> Option.map List.ofArray
|
||||
|> Option.defaultValue []
|
||||
Metadata = row.stringOrNone "meta_items"
|
||||
|> Option.map (Utils.deserialize ser)
|
||||
|> Option.defaultValue []
|
||||
}
|
||||
|
||||
/// Create a revision from the current row
|
||||
let toRevision (row : RowReader) : Revision =
|
||||
{ AsOf = row.fieldValue<Instant> "as_of"
|
||||
Text = row.string "revision_text" |> MarkupText.parse
|
||||
}
|
||||
|
||||
/// Create a tag mapping from the current row
|
||||
let toTagMap (row : RowReader) : TagMap =
|
||||
{ Id = row.string "id" |> TagMapId
|
||||
WebLogId = row.string "web_log_id" |> WebLogId
|
||||
Tag = row.string "tag"
|
||||
UrlValue = row.string "url_value"
|
||||
}
|
||||
|
||||
/// Create a theme from the current row (excludes templates)
|
||||
let toTheme (row : RowReader) : Theme =
|
||||
{ Theme.empty with
|
||||
Id = row.string "id" |> ThemeId
|
||||
Name = row.string "name"
|
||||
Version = row.string "version"
|
||||
}
|
||||
|
||||
/// Create a theme asset from the current row
|
||||
let toThemeAsset includeData (row : RowReader) : ThemeAsset =
|
||||
{ Id = ThemeAssetId (ThemeId (row.string "theme_id"), row.string "path")
|
||||
|
@ -185,12 +154,6 @@ module Map =
|
|||
Data = if includeData then row.bytea "data" else [||]
|
||||
}
|
||||
|
||||
/// Create a theme template from the current row
|
||||
let toThemeTemplate includeText (row : RowReader) : ThemeTemplate =
|
||||
{ Name = row.string "name"
|
||||
Text = if includeText then row.string "template" else ""
|
||||
}
|
||||
|
||||
/// Create an uploaded file from the current row
|
||||
let toUpload includeData (row : RowReader) : Upload =
|
||||
{ Id = row.string "id" |> UploadId
|
||||
|
@ -199,42 +162,75 @@ module Map =
|
|||
UpdatedOn = row.fieldValue<Instant> "updated_on"
|
||||
Data = if includeData then row.bytea "data" else [||]
|
||||
}
|
||||
|
||||
/// Document manipulation functions
|
||||
module Document =
|
||||
|
||||
/// Create a web log from the current row
|
||||
let toWebLog (row : RowReader) : WebLog =
|
||||
{ Id = row.string "id" |> WebLogId
|
||||
Name = row.string "name"
|
||||
Slug = row.string "slug"
|
||||
Subtitle = row.stringOrNone "subtitle"
|
||||
DefaultPage = row.string "default_page"
|
||||
PostsPerPage = row.int "posts_per_page"
|
||||
ThemeId = row.string |