diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj
index 38a5455..840751d 100644
--- a/src/MyWebLog.Data/MyWebLog.Data.fsproj
+++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj
@@ -2,10 +2,10 @@
-
+
diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs
index cbed623..ba15a4e 100644
--- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs
@@ -1,14 +1,13 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
-open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog category data implementation
-type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresCategoryData (log : ILogger) =
/// Count all categories for the given web log
let countAll webLogId =
@@ -24,10 +23,8 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
let findAllForView webLogId = backgroundTask {
log.LogTrace "Category.findAllForView"
let! cats =
- Sql.fromDataSource source
- |> Sql.query $"{selectWithCriteria Table.Category} ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')"
- |> Sql.parameters [ webLogContains webLogId ]
- |> Sql.executeAsync fromData
+ Custom.list $"{selectWithCriteria Table.Category} ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')"
+ [ webLogContains webLogId ] fromData
let ordered = Utils.orderByHierarchy cats None None []
let counts =
ordered
@@ -41,7 +38,8 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
|> List.ofSeq
|> arrayContains (nameof Post.empty.CategoryIds) id
let postCount =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.query $"""
SELECT COUNT(DISTINCT id) AS {countName}
FROM {Table.Post}
@@ -71,7 +69,7 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
/// Find a category by its ID for the given web log
let findById catId webLogId =
log.LogTrace "Category.findById"
- Document.findByIdAndWebLog source Table.Category catId CategoryId.toString webLogId
+ Document.findByIdAndWebLog Table.Category catId CategoryId.toString webLogId
/// Find all categories for the given web log
let findByWebLog webLogId =
@@ -92,7 +90,8 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
let hasChildren = not (List.isEmpty children)
if hasChildren then
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.Update.partialById Table.Category,
children |> List.map (fun child -> [
@@ -103,13 +102,12 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
()
// Delete the category off all posts where it is assigned
let! posts =
- Sql.fromDataSource source
- |> Sql.query $"SELECT data FROM {Table.Post} WHERE data -> '{nameof Post.empty.CategoryIds}' @> @id"
- |> Sql.parameters [ "@id", Query.jsonbDocParam [| CategoryId.toString catId |] ]
- |> Sql.executeAsync fromData
+ Custom.list $"SELECT data FROM {Table.Post} WHERE data -> '{nameof Post.empty.CategoryIds}' @> @id"
+ [ "@id", Query.jsonbDocParam [| CategoryId.toString catId |] ] fromData
if not (List.isEmpty posts) then
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.Update.partialById Table.Post,
posts |> List.map (fun post -> [
@@ -135,7 +133,8 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
let restore cats = backgroundTask {
log.LogTrace "Category.restore"
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.insert Table.Category, cats |> List.map catParameters
]
diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs
index c413fd3..9204ab9 100644
--- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs
@@ -61,20 +61,20 @@ module Table =
open System
open System.Threading.Tasks
+open BitBadger.Npgsql.FSharp.Documents
open MyWebLog
open MyWebLog.Data
open NodaTime
open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// Create a SQL parameter for the web log ID
let webLogIdParam webLogId =
"@webLogId", Sql.string (WebLogId.toString webLogId)
/// Create an anonymous record with the given web log ID
-let webLogDoc webLogId =
- {| WebLogId = WebLogId.toString webLogId |}
+let webLogDoc (webLogId : WebLogId) =
+ {| WebLogId = webLogId |}
/// Create a parameter for a web log document-contains query
let webLogContains webLogId =
@@ -167,8 +167,9 @@ module Map =
module Document =
/// Determine whether a document exists with the given key for the given web log
- let existsByWebLog<'TKey> source table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId =
- Sql.fromDataSource source
+ let existsByWebLog<'TKey> table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId =
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.query $"""
SELECT EXISTS (
SELECT 1 FROM %s{table} WHERE id = @id AND {Query.whereDataContains "@criteria"}
@@ -177,12 +178,9 @@ module Document =
|> Sql.executeRowAsync Map.toExists
/// Find a document by its ID for the given web log
- let findByIdAndWebLog<'TKey, 'TDoc> source table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId =
- Sql.fromDataSource source
- |> Sql.query $"""{Query.selectFromTable table} WHERE id = @id AND {Query.whereDataContains "@criteria"}"""
- |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogContains webLogId ]
- |> Sql.executeAsync fromData<'TDoc>
- |> tryHead
+ let findByIdAndWebLog<'TKey, 'TDoc> table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId =
+ Custom.single $"""{Query.selectFromTable table} WHERE id = @id AND {Query.whereDataContains "@criteria"}"""
+ [ "@id", Sql.string (keyFunc key); webLogContains webLogId ] fromData<'TDoc>
/// Find a document by its ID for the given web log
let findByWebLog<'TDoc> table webLogId : Task<'TDoc list> =
@@ -193,23 +191,19 @@ module Document =
module Revisions =
/// Find all revisions for the given entity
- let findByEntityId<'TKey> source revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) =
- Sql.fromDataSource source
- |> Sql.query $"SELECT as_of, revision_text FROM %s{revTable} WHERE %s{entityTable}_id = @id ORDER BY as_of DESC"
- |> Sql.parameters [ "@id", Sql.string (keyFunc key) ]
- |> Sql.executeAsync Map.toRevision
+ let findByEntityId<'TKey> revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) =
+ Custom.list $"SELECT as_of, revision_text FROM %s{revTable} WHERE %s{entityTable}_id = @id ORDER BY as_of DESC"
+ [ "@id", Sql.string (keyFunc key) ] Map.toRevision
/// Find all revisions for all posts for the given web log
- let findByWebLog<'TKey> source revTable entityTable (keyFunc : string -> 'TKey) webLogId =
- Sql.fromDataSource source
- |> Sql.query $"""
- SELECT pr.*
- FROM %s{revTable} pr
- INNER JOIN %s{entityTable} p ON p.id = pr.{entityTable}_id
- WHERE p.{Query.whereDataContains "@criteria"}
- ORDER BY as_of DESC"""
- |> Sql.parameters [ webLogContains webLogId ]
- |> Sql.executeAsync (fun row -> keyFunc (row.string $"{entityTable}_id"), Map.toRevision row)
+ let findByWebLog<'TKey> revTable entityTable (keyFunc : string -> 'TKey) webLogId =
+ Custom.list
+ $"""SELECT pr.*
+ FROM %s{revTable} pr
+ INNER JOIN %s{entityTable} p ON p.id = pr.{entityTable}_id
+ WHERE p.{Query.whereDataContains "@criteria"}
+ ORDER BY as_of DESC"""
+ [ webLogContains webLogId ] (fun row -> keyFunc (row.string $"{entityTable}_id"), Map.toRevision row)
/// Parameters for a revision INSERT statement
let revParams<'TKey> (key : 'TKey) (keyFunc : 'TKey -> string) rev = [
@@ -223,12 +217,12 @@ module Revisions =
$"INSERT INTO %s{table} VALUES (@id, @asOf, @text)"
/// Update a page's revisions
- let update<'TKey>
- source revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) oldRevs newRevs = backgroundTask {
+ let update<'TKey> revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) oldRevs newRevs = backgroundTask {
let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs
if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
if not (List.isEmpty toDelete) then
$"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf",
diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs
index 01182a2..faa4c79 100644
--- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs
@@ -1,21 +1,20 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
-open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog page data implementation
-type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresPageData (log : ILogger) =
// SUPPORT FUNCTIONS
/// Append revisions to a page
let appendPageRevisions (page : Page) = backgroundTask {
log.LogTrace "Page.appendPageRevisions"
- let! revisions = Revisions.findByEntityId source Table.PageRevision Table.Page page.Id PageId.toString
+ let! revisions = Revisions.findByEntityId Table.PageRevision Table.Page page.Id PageId.toString
return { page with Revisions = revisions }
}
@@ -26,22 +25,20 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Update a page's revisions
let updatePageRevisions pageId oldRevs newRevs =
log.LogTrace "Page.updatePageRevisions"
- Revisions.update source Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs
+ Revisions.update Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs
/// Does the given page exist?
let pageExists pageId webLogId =
log.LogTrace "Page.pageExists"
- Document.existsByWebLog source Table.Page pageId PageId.toString webLogId
+ Document.existsByWebLog Table.Page pageId PageId.toString webLogId
// IMPLEMENTATION FUNCTIONS
/// Get all pages for a web log (without text or revisions)
let all webLogId =
log.LogTrace "Page.all"
- Sql.fromDataSource source
- |> Sql.query $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')"
- |> Sql.parameters [ webLogContains webLogId ]
- |> Sql.executeAsync fromData
+ Custom.list $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')"
+ [ webLogContains webLogId ] fromData
/// Count all pages for the given web log
let countAll webLogId =
@@ -56,7 +53,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Find a page by its ID (without revisions)
let findById pageId webLogId =
log.LogTrace "Page.findById"
- Document.findByIdAndWebLog source Table.Page pageId PageId.toString webLogId
+ Document.findByIdAndWebLog Table.Page pageId PageId.toString webLogId
/// Find a complete page by its ID
let findFullById pageId webLogId = backgroundTask {
@@ -92,22 +89,18 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
let linkSql, linkParam =
arrayContains (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks
return!
- Sql.fromDataSource source
- |> Sql.query $"""
- SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink
- FROM page
- WHERE {Query.whereDataContains "@criteria"}
- AND {linkSql}"""
- |> Sql.parameters [ webLogContains webLogId; linkParam ]
- |> Sql.executeAsync Map.toPermalink
- |> tryHead
+ Custom.single
+ $"""SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink
+ FROM page
+ WHERE {Query.whereDataContains "@criteria"}
+ AND {linkSql}""" [ webLogContains webLogId; linkParam ] Map.toPermalink
}
/// Get all complete pages for the given web log
let findFullByWebLog webLogId = backgroundTask {
log.LogTrace "Page.findFullByWebLog"
let! pages = Document.findByWebLog Table.Page webLogId
- let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId
+ let! revisions = Revisions.findByWebLog Table.PageRevision Table.Page PageId webLogId
return
pages
|> List.map (fun it ->
@@ -117,28 +110,27 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Get all listed pages for the given web log (without revisions or text)
let findListed webLogId =
log.LogTrace "Page.findListed"
- Sql.fromDataSource source
- |> Sql.query $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')"
- |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with IsInPageList = true |} ]
- |> Sql.executeAsync pageWithoutText
+ Custom.list $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')"
+ [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with IsInPageList = true |} ]
+ pageWithoutText
/// Get a page of pages for the given web log (without revisions)
let findPageOfPages webLogId pageNbr =
log.LogTrace "Page.findPageOfPages"
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Page}
- ORDER BY LOWER(data->>'{nameof Page.empty.Title}')
- LIMIT @pageSize OFFSET @toSkip"
- |> Sql.parameters [ webLogContains webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ]
- |> Sql.executeAsync fromData
+ Custom.list
+ $"{selectWithCriteria Table.Page}
+ ORDER BY LOWER(data->>'{nameof Page.empty.Title}')
+ LIMIT @pageSize OFFSET @toSkip"
+ [ webLogContains webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ]
+ fromData
/// Restore pages from a backup
let restore (pages : Page list) = backgroundTask {
log.LogTrace "Page.restore"
let revisions = pages |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r))
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.insert Table.Page,
pages
diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs
index 71a42a7..400c1fd 100644
--- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs
@@ -1,22 +1,21 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
open NodaTime.Text
-open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog post data implementation
-type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresPostData (log : ILogger) =
// SUPPORT FUNCTIONS
/// Append revisions to a post
let appendPostRevisions (post : Post) = backgroundTask {
log.LogTrace "Post.appendPostRevisions"
- let! revisions = Revisions.findByEntityId source Table.PostRevision Table.Post post.Id PostId.toString
+ let! revisions = Revisions.findByEntityId Table.PostRevision Table.Post post.Id PostId.toString
return { post with Revisions = revisions }
}
@@ -27,19 +26,20 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
/// Update a post's revisions
let updatePostRevisions postId oldRevs newRevs =
log.LogTrace "Post.updatePostRevisions"
- Revisions.update source Table.PostRevision Table.Post postId PostId.toString oldRevs newRevs
+ Revisions.update Table.PostRevision Table.Post postId PostId.toString oldRevs newRevs
/// Does the given post exist?
let postExists postId webLogId =
log.LogTrace "Post.postExists"
- Document.existsByWebLog source Table.Post postId PostId.toString webLogId
+ Document.existsByWebLog Table.Post postId PostId.toString webLogId
// IMPLEMENTATION FUNCTIONS
/// Count posts in a status for the given web log
let countByStatus status webLogId =
log.LogTrace "Post.countByStatus"
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.query
$"""SELECT COUNT(id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}"""
|> Sql.parameters
@@ -49,17 +49,15 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
/// Find a post by its ID for the given web log (excluding revisions)
let findById postId webLogId =
log.LogTrace "Post.findById"
- Document.findByIdAndWebLog source Table.Post postId PostId.toString webLogId
+ Document.findByIdAndWebLog Table.Post postId PostId.toString webLogId
/// Find a post by its permalink for the given web log (excluding revisions and prior permalinks)
let findByPermalink permalink webLogId =
log.LogTrace "Post.findByPermalink"
- Sql.fromDataSource source
- |> Sql.query (selectWithCriteria Table.Post)
- |> Sql.parameters
- [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} ]
- |> Sql.executeAsync fromData
- |> tryHead
+ Custom.single (selectWithCriteria Table.Post)
+ [ "@criteria",
+ Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |}
+ ] fromData
/// Find a complete post by its ID for the given web log
let findFullById postId webLogId = backgroundTask {
@@ -77,13 +75,10 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
match! postExists postId webLogId with
| true ->
let theId = PostId.toString postId
- let! _ =
- Sql.fromDataSource source
- |> Sql.query $"""
- DELETE FROM {Table.PostComment} WHERE {Query.whereDataContains "@criteria"};
- DELETE FROM {Table.Post} WHERE id = @id"""
- |> Sql.parameters [ "@id", Sql.string theId; "@criteria", Query.jsonbDocParam {| PostId = theId |} ]
- |> Sql.executeNonQueryAsync
+ do! Custom.nonQuery
+ $"""DELETE FROM {Table.PostComment} WHERE {Query.whereDataContains "@criteria"};
+ DELETE FROM {Table.Post} WHERE id = @id"""
+ [ "@id", Sql.string theId; "@criteria", Query.jsonbDocParam {| PostId = theId |} ]
return true
| false -> return false
}
@@ -96,22 +91,18 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
let linkSql, linkParam =
arrayContains (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks
return!
- Sql.fromDataSource source
- |> Sql.query $"""
- SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink
- FROM {Table.Post}
- WHERE {Query.whereDataContains "@criteria"}
- AND {linkSql}"""
- |> Sql.parameters [ webLogContains webLogId; linkParam ]
- |> Sql.executeAsync Map.toPermalink
- |> tryHead
+ Custom.single
+ $"""SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink
+ FROM {Table.Post}
+ WHERE {Query.whereDataContains "@criteria"}
+ AND {linkSql}""" [ webLogContains webLogId; linkParam ] Map.toPermalink
}
/// Get all complete posts for the given web log
let findFullByWebLog webLogId = backgroundTask {
log.LogTrace "Post.findFullByWebLog"
let! posts = Document.findByWebLog Table.Post webLogId
- let! revisions = Revisions.findByWebLog source Table.PostRevision Table.Post PostId webLogId
+ let! revisions = Revisions.findByWebLog Table.PostRevision Table.Post PostId webLogId
return
posts
|> List.map (fun it ->
@@ -122,83 +113,67 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage =
log.LogTrace "Post.findPageOfCategorizedPosts"
let catSql, catParam = arrayContains (nameof Post.empty.CategoryIds) CategoryId.toString categoryIds
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Post}
- AND {catSql}
- ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
- LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
- |> Sql.parameters
+ Custom.list
+ $"{selectWithCriteria Table.Post}
+ AND {catSql}
+ ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
catParam
- ]
- |> Sql.executeAsync fromData
+ ] fromData
/// Get a page of posts for the given web log (excludes text and revisions)
let findPageOfPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPosts"
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Post}
- ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC NULLS FIRST,
- data ->> '{nameof Post.empty.UpdatedOn}'
- LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
- |> Sql.parameters [ webLogContains webLogId ]
- |> Sql.executeAsync postWithoutText
+ Custom.list
+ $"{selectWithCriteria Table.Post}
+ ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC NULLS FIRST,
+ data ->> '{nameof Post.empty.UpdatedOn}'
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
+ [ webLogContains webLogId ] postWithoutText
/// Get a page of published posts for the given web log (excludes revisions)
let findPageOfPublishedPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPublishedPosts"
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Post}
- ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
- LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
- |> Sql.parameters
+ Custom.list
+ $"{selectWithCriteria Table.Post}
+ ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} ]
- |> Sql.executeAsync fromData
+ fromData
/// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks)
let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage =
log.LogTrace "Post.findPageOfTaggedPosts"
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Post}
- AND data['{nameof Post.empty.Tags}'] @> @tag
- ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
- LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
- |> Sql.parameters
+ Custom.list
+ $"{selectWithCriteria Table.Post}
+ AND data['{nameof Post.empty.Tags}'] @> @tag
+ ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
"@tag", Query.jsonbDocParam [| tag |]
- ]
- |> Sql.executeAsync fromData
+ ] fromData
/// Find the next newest and oldest post from a publish date for the given web log
let findSurroundingPosts webLogId publishedOn = backgroundTask {
log.LogTrace "Post.findSurroundingPosts"
- let queryParams () = Sql.parameters [
+ let queryParams () = [
"@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
"@publishedOn", Sql.string ((InstantPattern.General.Format publishedOn).Substring (0, 19))
]
let pubField = nameof Post.empty.PublishedOn
let! older =
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Post}
- AND SUBSTR(data ->> '{pubField}', 1, 19) < @publishedOn
- ORDER BY data ->> '{pubField}' DESC
- LIMIT 1"
- |> queryParams ()
- |> Sql.executeAsync fromData
+ Custom.list
+ $"{selectWithCriteria Table.Post}
+ AND SUBSTR(data ->> '{pubField}', 1, 19) < @publishedOn
+ ORDER BY data ->> '{pubField}' DESC
+ LIMIT 1" (queryParams ()) fromData
let! newer =
- Sql.fromDataSource source
- |> Sql.query $"
- {selectWithCriteria Table.Post}
- AND SUBSTR(data ->> '{pubField}', 1, 19) > @publishedOn
- ORDER BY data ->> '{pubField}'
- LIMIT 1"
- |> queryParams ()
- |> Sql.executeAsync fromData
+ Custom.list
+ $"{selectWithCriteria Table.Post}
+ AND SUBSTR(data ->> '{pubField}', 1, 19) > @publishedOn
+ ORDER BY data ->> '{pubField}'
+ LIMIT 1" (queryParams ()) fromData
return List.tryHead older, List.tryHead newer
}
@@ -215,7 +190,8 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
log.LogTrace "Post.restore"
let revisions = posts |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r))
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.insert Table.Post,
posts
diff --git a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs
index c4a5a4e..6c0aa52 100644
--- a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs
@@ -1,24 +1,23 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
-open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog tag mapping data implementation
-type PostgresTagMapData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresTagMapData (log : ILogger) =
/// Find a tag mapping by its ID for the given web log
let findById tagMapId webLogId =
log.LogTrace "TagMap.findById"
- Document.findByIdAndWebLog source Table.TagMap tagMapId TagMapId.toString webLogId
+ Document.findByIdAndWebLog Table.TagMap tagMapId TagMapId.toString webLogId
/// Delete a tag mapping for the given web log
let delete tagMapId webLogId = backgroundTask {
log.LogTrace "TagMap.delete"
- let! exists = Document.existsByWebLog source Table.TagMap tagMapId TagMapId.toString webLogId
+ let! exists = Document.existsByWebLog Table.TagMap tagMapId TagMapId.toString webLogId
if exists then
do! Delete.byId Table.TagMap (TagMapId.toString tagMapId)
return true
@@ -28,28 +27,22 @@ type PostgresTagMapData (source : NpgsqlDataSource, log : ILogger) =
/// Find a tag mapping by its URL value for the given web log
let findByUrlValue (urlValue : string) webLogId =
log.LogTrace "TagMap.findByUrlValue"
- Sql.fromDataSource source
- |> Sql.query (selectWithCriteria Table.TagMap)
- |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ]
- |> Sql.executeAsync fromData
- |> tryHead
-
+ Custom.single (selectWithCriteria Table.TagMap)
+ [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ]
+ fromData
+
/// Get all tag mappings for the given web log
let findByWebLog webLogId =
log.LogTrace "TagMap.findByWebLog"
- Sql.fromDataSource source
- |> Sql.query $"{selectWithCriteria Table.TagMap} ORDER BY data ->> 'tag'"
- |> Sql.parameters [ webLogContains webLogId ]
- |> Sql.executeAsync fromData
+ Custom.list $"{selectWithCriteria Table.TagMap} ORDER BY data ->> 'tag'" [ webLogContains webLogId ]
+ fromData
/// Find any tag mappings in a list of tags for the given web log
let findMappingForTags tags webLogId =
log.LogTrace "TagMap.findMappingForTags"
let tagSql, tagParam = arrayContains (nameof TagMap.empty.Tag) id tags
- Sql.fromDataSource source
- |> Sql.query $"{selectWithCriteria Table.TagMap} AND {tagSql}"
- |> Sql.parameters [ webLogContains webLogId; tagParam ]
- |> Sql.executeAsync fromData
+ Custom.list $"{selectWithCriteria Table.TagMap} AND {tagSql}" [ webLogContains webLogId; tagParam ]
+ fromData
/// Save a tag mapping
let save (tagMap : TagMap) =
@@ -58,7 +51,8 @@ type PostgresTagMapData (source : NpgsqlDataSource, log : ILogger) =
/// Restore tag mappings from a backup
let restore (tagMaps : TagMap list) = backgroundTask {
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.insert Table.TagMap,
tagMaps |> List.map (fun tagMap -> Query.docParameters (TagMapId.toString tagMap.Id) tagMap)
diff --git a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs
index 54166fc..00af329 100644
--- a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs
@@ -1,14 +1,13 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
-open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostreSQL myWebLog theme data implementation
-type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresThemeData (log : ILogger) =
/// Clear out the template text from a theme
let withoutTemplateText row =
@@ -18,9 +17,7 @@ type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) =
/// Retrieve all themes (except 'admin'; excludes template text)
let all () =
log.LogTrace "Theme.all"
- Sql.fromDataSource source
- |> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id <> 'admin' ORDER BY id"
- |> Sql.executeAsync withoutTemplateText
+ Custom.list $"{Query.selectFromTable Table.Theme} WHERE id <> 'admin' ORDER BY id" [] withoutTemplateText
/// Does a given theme exist?
let exists themeId =
@@ -35,11 +32,7 @@ type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) =
/// Find a theme by its ID (excludes the text of templates)
let findByIdWithoutText themeId =
log.LogTrace "Theme.findByIdWithoutText"
- Sql.fromDataSource source
- |> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id = @id"
- |> Sql.parameters [ "@id", Sql.string (ThemeId.toString themeId) ]
- |> Sql.executeAsync withoutTemplateText
- |> tryHead
+ Custom.single (Query.Find.byId Table.Theme) [ "@id", Sql.string (ThemeId.toString themeId) ] withoutTemplateText
/// Delete a theme by its ID
let delete themeId = backgroundTask {
@@ -66,74 +59,54 @@ type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) =
/// PostreSQL myWebLog theme data implementation
-type PostgresThemeAssetData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresThemeAssetData (log : ILogger) =
/// Get all theme assets (excludes data)
let all () =
log.LogTrace "ThemeAsset.all"
- Sql.fromDataSource source
- |> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}"
- |> Sql.executeAsync (Map.toThemeAsset false)
+ Custom.list $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}" [] (Map.toThemeAsset false)
/// Delete all assets for the given theme
- let deleteByTheme themeId = backgroundTask {
+ let deleteByTheme themeId =
log.LogTrace "ThemeAsset.deleteByTheme"
- let! _ =
- Sql.fromDataSource source
- |> Sql.query $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
- |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ]
- |> Sql.executeNonQueryAsync
- ()
- }
+ Custom.nonQuery $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
+ [ "@themeId", Sql.string (ThemeId.toString themeId) ]
/// Find a theme asset by its ID
let findById assetId =
log.LogTrace "ThemeAsset.findById"
let (ThemeAssetId (ThemeId themeId, path)) = assetId
- Sql.fromDataSource source
- |> Sql.query $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId AND path = @path"
- |> Sql.parameters [ "@themeId", Sql.string themeId; "@path", Sql.string path ]
- |> Sql.executeAsync (Map.toThemeAsset true)
- |> tryHead
+ Custom.single $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId AND path = @path"
+ [ "@themeId", Sql.string themeId; "@path", Sql.string path ] (Map.toThemeAsset true)
/// Get theme assets for the given theme (excludes data)
let findByTheme themeId =
log.LogTrace "ThemeAsset.findByTheme"
- Sql.fromDataSource source
- |> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
- |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ]
- |> Sql.executeAsync (Map.toThemeAsset false)
+ Custom.list $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
+ [ "@themeId", Sql.string (ThemeId.toString themeId) ] (Map.toThemeAsset false)
/// Get theme assets for the given theme
let findByThemeWithData themeId =
log.LogTrace "ThemeAsset.findByThemeWithData"
- Sql.fromDataSource source
- |> Sql.query $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
- |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ]
- |> Sql.executeAsync (Map.toThemeAsset true)
+ Custom.list $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
+ [ "@themeId", Sql.string (ThemeId.toString themeId) ] (Map.toThemeAsset true)
/// Save a theme asset
- let save (asset : ThemeAsset) = backgroundTask {
+ let save (asset : ThemeAsset) =
log.LogTrace "ThemeAsset.save"
let (ThemeAssetId (ThemeId themeId, path)) = asset.Id
- let! _ =
- Sql.fromDataSource source
- |> Sql.query $"
- INSERT INTO {Table.ThemeAsset} (
- theme_id, path, updated_on, data
- ) VALUES (
- @themeId, @path, @updatedOn, @data
- ) ON CONFLICT (theme_id, path) DO UPDATE
- SET updated_on = EXCLUDED.updated_on,
- data = EXCLUDED.data"
- |> Sql.parameters
- [ "@themeId", Sql.string themeId
- "@path", Sql.string path
- "@data", Sql.bytea asset.Data
- typedParam "updatedOn" asset.UpdatedOn ]
- |> Sql.executeNonQueryAsync
- ()
- }
+ Custom.nonQuery
+ $"INSERT INTO {Table.ThemeAsset} (
+ theme_id, path, updated_on, data
+ ) VALUES (
+ @themeId, @path, @updatedOn, @data
+ ) ON CONFLICT (theme_id, path) DO UPDATE
+ SET updated_on = EXCLUDED.updated_on,
+ data = EXCLUDED.data"
+ [ "@themeId", Sql.string themeId
+ "@path", Sql.string path
+ "@data", Sql.bytea asset.Data
+ typedParam "updatedOn" asset.UpdatedOn ]
interface IThemeAssetData with
member _.All () = all ()
diff --git a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs
index d713a19..97e36eb 100644
--- a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs
@@ -1,13 +1,13 @@
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 uploaded file data implementation
-type PostgresUploadData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresUploadData (log : ILogger) =
/// The INSERT statement for an uploaded file
let upInsert = $"
@@ -27,32 +27,19 @@ type PostgresUploadData (source : NpgsqlDataSource, log : ILogger) =
]
/// Save an uploaded file
- let add upload = backgroundTask {
+ let add upload =
log.LogTrace "Upload.add"
- let! _ =
- Sql.fromDataSource source
- |> Sql.query upInsert
- |> Sql.parameters (upParams upload)
- |> Sql.executeNonQueryAsync
- ()
- }
+ Custom.nonQuery upInsert (upParams upload)
/// Delete an uploaded file by its ID
let delete uploadId webLogId = backgroundTask {
log.LogTrace "Upload.delete"
let idParam = [ "@id", Sql.string (UploadId.toString uploadId) ]
let! path =
- Sql.fromDataSource source
- |> Sql.query $"SELECT path FROM {Table.Upload} WHERE id = @id AND web_log_id = @webLogId"
- |> Sql.parameters (webLogIdParam webLogId :: idParam)
- |> Sql.executeAsync (fun row -> row.string "path")
- |> tryHead
+ Custom.single $"SELECT path FROM {Table.Upload} WHERE id = @id AND web_log_id = @webLogId"
+ (webLogIdParam webLogId :: idParam) (fun row -> row.string "path")
if Option.isSome path then
- let! _ =
- Sql.fromDataSource source
- |> Sql.query (Documents.Query.Delete.byId Table.Upload)
- |> Sql.parameters idParam
- |> Sql.executeNonQueryAsync
+ do! Custom.nonQuery (Query.Delete.byId Table.Upload) idParam
return Ok path.Value
else return Error $"""Upload ID {UploadId.toString uploadId} not found"""
}
@@ -60,34 +47,28 @@ type PostgresUploadData (source : NpgsqlDataSource, log : ILogger) =
/// Find an uploaded file by its path for the given web log
let findByPath path webLogId =
log.LogTrace "Upload.findByPath"
- Sql.fromDataSource source
- |> Sql.query $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId AND path = @path"
- |> Sql.parameters [ webLogIdParam webLogId; "@path", Sql.string path ]
- |> Sql.executeAsync (Map.toUpload true)
- |> tryHead
+ Custom.single $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId AND path = @path"
+ [ webLogIdParam webLogId; "@path", Sql.string path ] (Map.toUpload true)
/// Find all uploaded files for the given web log (excludes data)
let findByWebLog webLogId =
log.LogTrace "Upload.findByWebLog"
- Sql.fromDataSource source
- |> Sql.query $"SELECT id, web_log_id, path, updated_on FROM {Table.Upload} WHERE web_log_id = @webLogId"
- |> Sql.parameters [ webLogIdParam webLogId ]
- |> Sql.executeAsync (Map.toUpload false)
+ Custom.list $"SELECT id, web_log_id, path, updated_on FROM {Table.Upload} WHERE web_log_id = @webLogId"
+ [ webLogIdParam webLogId ] (Map.toUpload false)
/// Find all uploaded files for the given web log
let findByWebLogWithData webLogId =
log.LogTrace "Upload.findByWebLogWithData"
- Sql.fromDataSource source
- |> Sql.query $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId"
- |> Sql.parameters [ webLogIdParam webLogId ]
- |> Sql.executeAsync (Map.toUpload true)
+ Custom.list $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId" [ webLogIdParam webLogId ]
+ (Map.toUpload true)
/// Restore uploads from a backup
let restore uploads = backgroundTask {
log.LogTrace "Upload.restore"
for batch in uploads |> List.chunkBySize 5 do
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [ upInsert, batch |> List.map upParams ]
()
}
diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs
index 67da00f..713005b 100644
--- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs
@@ -1,14 +1,12 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
-open Npgsql
-open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog web log data implementation
-type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresWebLogData (log : ILogger) =
/// Add a web log
let add (webLog : WebLog) =
@@ -18,15 +16,13 @@ type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) =
/// Retrieve all web logs
let all () =
log.LogTrace "WebLog.all"
- all Table.WebLog
+ Find.all Table.WebLog
/// Delete a web log by its ID
- let delete webLogId = backgroundTask {
+ let delete webLogId =
log.LogTrace "WebLog.delete"
- let! _ =
- Sql.fromDataSource source
- |> Sql.query $"""
- DELETE FROM {Table.PostComment}
+ Custom.nonQuery
+ $"""DELETE FROM {Table.PostComment}
WHERE data ->> '{nameof Comment.empty.PostId}' IN
(SELECT id FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"});
{Query.Delete.byContains Table.Post};
@@ -36,19 +32,13 @@ type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) =
{Query.Delete.byContains Table.WebLogUser};
DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId;
DELETE FROM {Table.WebLog} WHERE id = @webLogId"""
- |> Sql.parameters [ webLogIdParam webLogId; webLogContains webLogId ]
- |> Sql.executeNonQueryAsync
- ()
- }
+ [ webLogIdParam webLogId; webLogContains webLogId ]
/// Find a web log by its host (URL base)
let findByHost (url : string) =
log.LogTrace "WebLog.findByHost"
- Sql.fromDataSource source
- |> Sql.query (selectWithCriteria Table.WebLog)
- |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| UrlBase = url |} ]
- |> Sql.executeAsync fromData
- |> tryHead
+ Custom.single (selectWithCriteria Table.WebLog) [ "@criteria", Query.jsonbDocParam {| UrlBase = url |} ]
+ fromData
/// Find a web log by its ID
let findById webLogId =
diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs
index b1ea453..fd15654 100644
--- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs
+++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs
@@ -1,20 +1,18 @@
namespace MyWebLog.Data.Postgres
+open BitBadger.Npgsql.FSharp.Documents
open Microsoft.Extensions.Logging
open MyWebLog
open MyWebLog.Data
-open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog user data implementation
-type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) =
+type PostgresWebLogUserData (log : ILogger) =
/// Find a user by their ID for the given web log
let findById userId webLogId =
log.LogTrace "WebLogUser.findById"
- Document.findByIdAndWebLog
- source Table.WebLogUser userId WebLogUserId.toString webLogId
+ Document.findByIdAndWebLog Table.WebLogUser userId WebLogUserId.toString webLogId
/// Delete a user if they have no posts or pages
let delete userId webLogId = backgroundTask {
@@ -22,19 +20,19 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) =
match! findById userId webLogId with
| Some _ ->
let criteria = Query.whereDataContains "@criteria"
- let usrId = WebLogUserId.toString userId
let! isAuthor =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.query $"
SELECT ( EXISTS (SELECT 1 FROM {Table.Page} WHERE {criteria}
OR EXISTS (SELECT 1 FROM {Table.Post} WHERE {criteria}))
AS {existsName}"
- |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| AuthorId = usrId |} ]
+ |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| AuthorId = userId |} ]
|> Sql.executeRowAsync Map.toExists
if isAuthor then
return Error "User has pages or posts; cannot delete"
else
- do! Delete.byId Table.WebLogUser usrId
+ do! Delete.byId Table.WebLogUser (WebLogUserId.toString userId)
return Ok true
| None -> return Error "User does not exist"
}
@@ -42,30 +40,24 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) =
/// Find a user by their e-mail address for the given web log
let findByEmail (email : string) webLogId =
log.LogTrace "WebLogUser.findByEmail"
- Sql.fromDataSource source
- |> Sql.query (selectWithCriteria Table.WebLogUser)
- |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ]
- |> Sql.executeAsync fromData
- |> tryHead
+ Custom.single (selectWithCriteria Table.WebLogUser)
+ [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ]
+ fromData
/// Get all users for the given web log
let findByWebLog webLogId =
log.LogTrace "WebLogUser.findByWebLog"
- Sql.fromDataSource source
- |> Sql.query
+ Custom.list
$"{selectWithCriteria Table.WebLogUser} ORDER BY LOWER(data->>'{nameof WebLogUser.empty.PreferredName}')"
- |> Sql.parameters [ webLogContains webLogId ]
- |> Sql.executeAsync fromData
+ [ webLogContains webLogId ] fromData
/// Find the names of users by their IDs for the given web log
let findNames webLogId userIds = backgroundTask {
log.LogTrace "WebLogUser.findNames"
let idSql, idParams = inClause "AND id" "id" WebLogUserId.toString userIds
let! users =
- Sql.fromDataSource source
- |> Sql.query $"{selectWithCriteria Table.WebLogUser} {idSql}"
- |> Sql.parameters (webLogContains webLogId :: idParams)
- |> Sql.executeAsync fromData
+ Custom.list $"{selectWithCriteria Table.WebLogUser} {idSql}" (webLogContains webLogId :: idParams)
+ fromData
return
users
|> List.map (fun u -> { Name = WebLogUserId.toString u.Id; Value = WebLogUser.displayName u })
@@ -75,7 +67,8 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) =
let restore (users : WebLogUser list) = backgroundTask {
log.LogTrace "WebLogUser.restore"
let! _ =
- Sql.fromDataSource source
+ Configuration.dataSource ()
+ |> Sql.fromDataSource
|> Sql.executeTransactionAsync [
Query.insert Table.WebLogUser,
users |> List.map (fun user -> Query.docParameters (WebLogUserId.toString user.Id) user)
@@ -86,7 +79,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) =
/// Set a user's last seen date/time to now
let setLastSeen userId webLogId = backgroundTask {
log.LogTrace "WebLogUser.setLastSeen"
- match! Document.existsByWebLog source Table.WebLogUser userId WebLogUserId.toString webLogId with
+ match! Document.existsByWebLog Table.WebLogUser userId WebLogUserId.toString webLogId with
| true ->
do! Update.partialById Table.WebLogUser (WebLogUserId.toString userId) {| LastSeenOn = Some (Noda.now ()) |}
| false -> ()
diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs
index 81218c6..6bee4e6 100644
--- a/src/MyWebLog.Data/PostgresData.fs
+++ b/src/MyWebLog.Data/PostgresData.fs
@@ -1,12 +1,13 @@
namespace MyWebLog.Data
open Microsoft.Extensions.Logging
+open BitBadger.Npgsql.Documents
+open BitBadger.Npgsql.FSharp.Documents
open MyWebLog
open MyWebLog.Data.Postgres
open Newtonsoft.Json
open Npgsql
open Npgsql.FSharp
-open Npgsql.FSharp.Documents
/// Data implementation for PostgreSQL
type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : JsonSerializer) =
@@ -16,7 +17,7 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser :
// Set up the PostgreSQL document store
Configuration.useDataSource source
Configuration.useSerializer
- { new Documents.IDocumentSerializer with
+ { new IDocumentSerializer with
member _.Serialize<'T> (it : 'T) : string = Utils.serialize ser it
member _.Deserialize<'T> (it : string) : 'T = Utils.deserialize ser it
}
@@ -131,13 +132,8 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser :
}
/// Set a specific database version
- let setDbVersion version = backgroundTask {
- let! _ =
- Sql.fromDataSource source
- |> Sql.query $"DELETE FROM db_version; INSERT INTO db_version VALUES ('%s{version}')"
- |> Sql.executeNonQueryAsync
- ()
- }
+ let setDbVersion version =
+ Custom.nonQuery $"DELETE FROM db_version; INSERT INTO db_version VALUES ('%s{version}')" []
/// Do required data migration between versions
let migrate version = backgroundTask {
@@ -152,15 +148,15 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser :
interface IData with
- member _.Category = PostgresCategoryData (source, log)
- member _.Page = PostgresPageData (source, log)
- member _.Post = PostgresPostData (source, log)
- member _.TagMap = PostgresTagMapData (source, log)
- member _.Theme = PostgresThemeData (source, log)
- member _.ThemeAsset = PostgresThemeAssetData (source, log)
- member _.Upload = PostgresUploadData (source, log)
- member _.WebLog = PostgresWebLogData (source, log)
- member _.WebLogUser = PostgresWebLogUserData (source, log)
+ member _.Category = PostgresCategoryData log
+ member _.Page = PostgresPageData log
+ member _.Post = PostgresPostData log
+ member _.TagMap = PostgresTagMapData log
+ member _.Theme = PostgresThemeData log
+ member _.ThemeAsset = PostgresThemeAssetData log
+ member _.Upload = PostgresUploadData log
+ member _.WebLog = PostgresWebLogData log
+ member _.WebLogUser = PostgresWebLogUserData log
member _.Serializer = ser
@@ -168,11 +164,7 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser :
log.LogTrace "PostgresData.StartUp"
do! ensureTables ()
- let! version =
- Sql.fromDataSource source
- |> Sql.query "SELECT id FROM db_version"
- |> Sql.executeAsync (fun row -> row.string "id")
- |> tryHead
+ let! version = Custom.single "SELECT id FROM db_version" [] (fun row -> row.string "id")
match version with
| Some v when v = Utils.currentDbVersion -> ()
| Some _
diff --git a/src/MyWebLog.sln b/src/MyWebLog.sln
index f12f40f..a594b6e 100644
--- a/src/MyWebLog.sln
+++ b/src/MyWebLog.sln
@@ -9,8 +9,6 @@ Project("{6EC3EE1D-3C4E-46DD-8F32-0CC8E7565705}") = "MyWebLog.Data", "MyWebLog.D
EndProject
Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "MyWebLog", "MyWebLog\MyWebLog.fsproj", "{5655B63D-429F-4CCD-A14C-FBD74D987ECB}"
EndProject
-Project("{F2A71F9B-5D33-465A-A702-920D77279786}") = "Npgsql.FSharp.Documents", "Npgsql.FSharp.Documents\Npgsql.FSharp.Documents.fsproj", "{C5F5E68A-9C2E-4FC0-A8E3-D7A52CCE668F}"
-EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -29,10 +27,6 @@ Global
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{5655B63D-429F-4CCD-A14C-FBD74D987ECB}.Release|Any CPU.Build.0 = Release|Any CPU
- {C5F5E68A-9C2E-4FC0-A8E3-D7A52CCE668F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
- {C5F5E68A-9C2E-4FC0-A8E3-D7A52CCE668F}.Debug|Any CPU.Build.0 = Debug|Any CPU
- {C5F5E68A-9C2E-4FC0-A8E3-D7A52CCE668F}.Release|Any CPU.ActiveCfg = Release|Any CPU
- {C5F5E68A-9C2E-4FC0-A8E3-D7A52CCE668F}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/src/Npgsql.FSharp.Documents/Library.fs b/src/Npgsql.FSharp.Documents/Library.fs
deleted file mode 100644
index db3c624..0000000
--- a/src/Npgsql.FSharp.Documents/Library.fs
+++ /dev/null
@@ -1,242 +0,0 @@
-module Npgsql.FSharp.Documents
-
-/// The required document serialization implementation
-type IDocumentSerializer =
-
- /// Serialize an object to a JSON string
- abstract Serialize<'T> : 'T -> string
-
- /// Deserialize a JSON string into an object
- abstract Deserialize<'T> : string -> 'T
-
-
-/// The type of index to generate for the document
-type DocumentIndex =
- /// A GIN index with standard operations (all operators supported)
- | Full
- /// A GIN index with JSONPath operations (optimized for @>, @?, @@ operators)
- | Optimized
-
-
-/// Configuration for document handling
-module Configuration =
-
- open System.Text.Json
- open System.Text.Json.Serialization
-
- /// The default JSON serializer options to use with the stock serializer
- let private jsonDefaultOpts =
- let o = JsonSerializerOptions ()
- o.Converters.Add (JsonFSharpConverter ())
- o
-
- /// The serializer to use for document manipulation
- let mutable internal serializer =
- { new IDocumentSerializer with
- member _.Serialize<'T> (it : 'T) : string =
- JsonSerializer.Serialize (it, jsonDefaultOpts)
- member _.Deserialize<'T> (it : string) : 'T =
- JsonSerializer.Deserialize<'T> (it, jsonDefaultOpts)
- }
-
- /// Register a serializer to use for translating documents to domain types
- let useSerializer ser =
- serializer <- ser
-
- /// The data source to use for query execution
- let mutable private dataSourceValue : Npgsql.NpgsqlDataSource option = None
-
- /// Register a data source to use for query execution
- let useDataSource source =
- dataSourceValue <- Some source
-
- let internal dataSource () =
- match dataSourceValue with
- | Some source -> source
- | None -> invalidOp "Please provide a data source before attempting data access"
-
-
-/// Data definition
-[]
-module Definition =
-
- /// SQL statement to create a document table
- let createTable name =
- $"CREATE TABLE IF NOT EXISTS %s{name} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)"
-
- /// Create a document table
- let ensureTable name sqlProps = backgroundTask {
- let! _ = sqlProps |> Sql.query (createTable name) |> Sql.executeNonQueryAsync
- ()
- }
-
- /// SQL statement to create an index on documents in the specified table
- let createIndex (name : string) idxType =
- let extraOps = match idxType with Full -> "" | Optimized -> " jsonb_path_ops"
- let tableName = name.Split(".") |> Array.last
- $"CREATE INDEX IF NOT EXISTS idx_{tableName} ON {name} USING GIN (data{extraOps})"
-
- /// Create an index on documents in the specified table
- let ensureIndex (name : string) idxType sqlProps = backgroundTask {
- let! _ = sqlProps |> Sql.query (createIndex name idxType) |> Sql.executeNonQueryAsync
- ()
- }
-
-/// Create a domain item from a document, specifying the field in which the document is found
-let fromDocument<'T> field (row : RowReader) : 'T =
- Configuration.serializer.Deserialize<'T> (row.string field)
-
-/// Create a domain item from a document
-let fromData<'T> row : 'T =
- fromDocument "data" row
-
-/// Query construction functions
-[]
-module Query =
-
- open System.Threading.Tasks
-
- // ~~ BUILDING BLOCKS ~~
-
- /// Create a SELECT clause to retrieve the document data from the given table
- let selectFromTable tableName =
- $"SELECT data FROM %s{tableName}"
-
- /// Create a WHERE clause fragment to implement a @> (JSON contains) condition
- let whereDataContains paramName =
- $"data @> %s{paramName}"
-
- /// Create a WHERE clause fragment to implement a @? (JSON Path match) condition
- let whereJsonPathMatches paramName =
- $"data @? %s{paramName}"
-
- /// Create a JSONB document parameter
- let jsonbDocParam (it : obj) =
- Sql.jsonb (Configuration.serializer.Serialize it)
-
- /// Create ID and data parameters for a query
- let docParameters<'T> docId (doc : 'T) =
- [ "@id", Sql.string docId; "@data", jsonbDocParam doc ]
-
- // ~~ DOCUMENT RETRIEVAL QUERIES ~~
-
- /// Retrieve all documents in the given table
- let all<'T> tableName sqlProps : Task<'T list> =
- sqlProps
- |> Sql.query $"SELECT data FROM %s{tableName}"
- |> Sql.executeAsync fromData<'T>
-
- /// Count matching documents using @> (JSON contains)
- let countByContains tableName (criteria : obj) sqlProps : Task =
- sqlProps
- |> Sql.query $"""SELECT COUNT(id) AS row_count FROM %s{tableName} WHERE {whereDataContains "@criteria"}"""
- |> Sql.parameters [ "@criteria", jsonbDocParam criteria ]
- |> Sql.executeRowAsync (fun row -> row.int "row_count")
-
- /// Count matching documents using @? (JSON Path match)
- let countByJsonPath tableName jsonPath sqlProps : Task =
- sqlProps
- |> Sql.query $"""SELECT COUNT(id) AS row_count FROM %s{tableName} WHERE {whereJsonPathMatches "@jsonPath"}"""
- |> Sql.parameters [ "@jsonPath", Sql.string jsonPath ]
- |> Sql.executeRowAsync (fun row -> row.int "row_count")
-
- /// Determine if a document exists for the given ID
- let existsById tableName docId sqlProps : Task =
- sqlProps
- |> Sql.query $"SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE id = @id) AS xist"
- |> Sql.parameters [ "@id", Sql.string docId ]
- |> Sql.executeRowAsync (fun row -> row.bool "xist")
-
- /// Determine if a document exists using @> (JSON contains)
- let existsByContains tableName (criteria : obj) sqlProps : Task =
- sqlProps
- |> Sql.query $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereDataContains "@criteria"}) AS xist"""
- |> Sql.parameters [ "@criteria", jsonbDocParam criteria ]
- |> Sql.executeRowAsync (fun row -> row.bool "xist")
-
- /// Determine if a document exists using @? (JSON Path match)
- let existsByJsonPath tableName jsonPath sqlProps : Task =
- sqlProps
- |> Sql.query $"""SELECT EXISTS (SELECT 1 FROM %s{tableName} WHERE {whereJsonPathMatches "@jsonPath"}) AS xist"""
- |> Sql.parameters [ "@criteria", Sql.string jsonPath ]
- |> Sql.executeRowAsync (fun row -> row.bool "xist")
-
- /// Execute a @> (JSON contains) query
- let findByContains<'T> tableName value sqlProps : Task<'T list> =
- sqlProps
- |> Sql.query $"""{selectFromTable tableName} WHERE {whereDataContains "@criteria"}"""
- |> Sql.parameters [ "@criteria", jsonbDocParam value ]
- |> Sql.executeAsync fromData<'T>
-
- /// Execute a @? (JSON Path match) query
- let findByJsonPath<'T> tableName jsonPath sqlProps : Task<'T list> =
- sqlProps
- |> Sql.query $"""{selectFromTable tableName} WHERE {whereJsonPathMatches "@jsonPath"}"""
- |> Sql.parameters [ "@jsonPath", Sql.string jsonPath ]
- |> Sql.executeAsync fromData<'T>
-
- /// Retrieve a document by its ID
- let tryById<'T> tableName idValue sqlProps : Task<'T option> = backgroundTask {
- let! results =
- sqlProps
- |> Sql.query $"{selectFromTable tableName} WHERE id = @id"
- |> Sql.parameters [ "@id", Sql.string idValue ]
- |> Sql.executeAsync fromData<'T>
- return List.tryHead results
- }
-
- // ~~ DOCUMENT MANIPULATION QUERIES ~~
-
- /// Query to insert a document
- let insertQuery tableName =
- $"INSERT INTO %s{tableName} (id, data) VALUES (@id, @data)"
-
- /// Insert a new document
- let insert<'T> tableName docId (document : 'T) sqlProps = backgroundTask {
- let! _ =
- sqlProps
- |> Sql.query $"INSERT INTO %s{tableName} (id, data) VALUES (@id, @data)"
- |> Sql.parameters (docParameters docId document)
- |> Sql.executeNonQueryAsync
- ()
- }
-
- /// Query to update a document
- let updateQuery tableName =
- $"UPDATE %s{tableName} SET data = @data WHERE id = @id"
-
- /// Update new document
- let update<'T> tableName docId (document : 'T) sqlProps = backgroundTask {
- let! _ =
- sqlProps
- |> Sql.query (updateQuery tableName)
- |> Sql.parameters (docParameters docId document)
- |> Sql.executeNonQueryAsync
- ()
- }
-
- /// Query to save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
- let saveQuery tableName =
- $"INSERT INTO %s{tableName} (id, data) VALUES (@id, @data) ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data"
-
- /// Save a document, inserting it if it does not exist and updating it if it does (AKA "upsert")
- let save<'T> tableName docId (document : 'T) sqlProps = backgroundTask {
- let! _ =
- sqlProps
- |> Sql.query $"
- INSERT INTO %s{tableName} (id, data) VALUES (@id, @data)
- ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data"
- |> Sql.parameters (docParameters docId document)
- |> Sql.executeNonQueryAsync
- ()
- }
-
- /// Delete a document by its ID
- let deleteById tableName docId sqlProps = backgroundTask {
- let _ =
- sqlProps
- |> Sql.query $"DELETE FROM %s{tableName} WHERE id = @id"
- |> Sql.parameters [ "@id", Sql.string docId ]
- |> Sql.executeNonQueryAsync
- ()
- }
diff --git a/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj b/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj
deleted file mode 100644
index 795d55c..0000000
--- a/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj
+++ /dev/null
@@ -1,12 +0,0 @@
-
-
-
-
-
-
-
-
-
-
-
-