V2 #36
@ -2,10 +2,10 @@
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" />
|
||||
<ProjectReference Include="..\..\..\Npgsql.Documents\src\Npgsql.FSharp.Documents\Npgsql.FSharp.Documents.fsproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="1.0.0-beta" />
|
||||
<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" />
|
||||
|
@ -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<Category>
|
||||
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
|
||||
@ -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<CategoryId, Category> source Table.Category catId CategoryId.toString webLogId
|
||||
Document.findByIdAndWebLog<CategoryId, Category> 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<Post>
|
||||
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! _ =
|
||||
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
|
||||
]
|
||||
|
@ -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",
|
||||
|
@ -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<Page>
|
||||
Custom.list $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')"
|
||||
[ webLogContains webLogId ] fromData<Page>
|
||||
|
||||
/// 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<PageId, Page> source Table.Page pageId PageId.toString webLogId
|
||||
Document.findByIdAndWebLog<PageId, Page> 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<Page> 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<Page>
|
||||
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<Page>
|
||||
|
||||
/// 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
|
||||
|
@ -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<PostId, Post> source Table.Post postId PostId.toString webLogId
|
||||
Document.findByIdAndWebLog<PostId, Post> 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<Post>
|
||||
|> tryHead
|
||||
Custom.single (selectWithCriteria Table.Post)
|
||||
[ "@criteria",
|
||||
Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |}
|
||||
] fromData<Post>
|
||||
|
||||
/// 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<Post> 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<Post>
|
||||
] fromData<Post>
|
||||
|
||||
/// 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<Post>
|
||||
fromData<Post>
|
||||
|
||||
/// 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<Post>
|
||||
] fromData<Post>
|
||||
|
||||
/// 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<Post>
|
||||
Custom.list
|
||||
$"{selectWithCriteria Table.Post}
|
||||
AND SUBSTR(data ->> '{pubField}', 1, 19) < @publishedOn
|
||||
ORDER BY data ->> '{pubField}' DESC
|
||||
LIMIT 1" (queryParams ()) fromData<Post>
|
||||
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<Post>
|
||||
Custom.list
|
||||
$"{selectWithCriteria Table.Post}
|
||||
AND SUBSTR(data ->> '{pubField}', 1, 19) > @publishedOn
|
||||
ORDER BY data ->> '{pubField}'
|
||||
LIMIT 1" (queryParams ()) fromData<Post>
|
||||
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
|
||||
|
@ -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<TagMapId, TagMap> source Table.TagMap tagMapId TagMapId.toString webLogId
|
||||
Document.findByIdAndWebLog<TagMapId, TagMap> 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<TagMap>
|
||||
|> tryHead
|
||||
|
||||
Custom.single (selectWithCriteria Table.TagMap)
|
||||
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ]
|
||||
fromData<TagMap>
|
||||
|
||||
/// 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<TagMap>
|
||||
Custom.list $"{selectWithCriteria Table.TagMap} ORDER BY data ->> 'tag'" [ webLogContains webLogId ]
|
||||
fromData<TagMap>
|
||||
|
||||
/// 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<TagMap>
|
||||
Custom.list $"{selectWithCriteria Table.TagMap} AND {tagSql}" [ webLogContains webLogId; tagParam ]
|
||||
fromData<TagMap>
|
||||
|
||||
/// 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)
|
||||
|
@ -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 ()
|
||||
|
@ -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 ]
|
||||
()
|
||||
}
|
||||
|
@ -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<WebLog> Table.WebLog
|
||||
Find.all<WebLog> 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<WebLog>
|
||||
|> tryHead
|
||||
Custom.single (selectWithCriteria Table.WebLog) [ "@criteria", Query.jsonbDocParam {| UrlBase = url |} ]
|
||||
fromData<WebLog>
|
||||
|
||||
/// Find a web log by its ID
|
||||
let findById webLogId =
|
||||
|
@ -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<WebLogUserId, WebLogUser>
|
||||
source Table.WebLogUser userId WebLogUserId.toString webLogId
|
||||
Document.findByIdAndWebLog<WebLogUserId, WebLogUser> 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<WebLogUser>
|
||||
|> tryHead
|
||||
Custom.single (selectWithCriteria Table.WebLogUser)
|
||||
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ]
|
||||
fromData<WebLogUser>
|
||||
|
||||
/// 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<WebLogUser>
|
||||
[ webLogContains webLogId ] fromData<WebLogUser>
|
||||
|
||||
/// 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<WebLogUser>
|
||||
Custom.list $"{selectWithCriteria Table.WebLogUser} {idSql}" (webLogContains webLogId :: idParams)
|
||||
fromData<WebLogUser>
|
||||
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 -> ()
|
||||
|
@ -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<PostgresData>, ser : JsonSerializer) =
|
||||
@ -16,7 +17,7 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger<PostgresData>, 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<PostgresData>, 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<PostgresData>, 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<PostgresData>, 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 _
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
[<RequireQualifiedAccess>]
|
||||
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
|
||||
[<RequireQualifiedAccess>]
|
||||
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<int> =
|
||||
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<int> =
|
||||
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<bool> =
|
||||
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<bool> =
|
||||
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<bool> =
|
||||
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
|
||||
()
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<Compile Include="Library.fs" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FSharp.SystemTextJson" Version="1.1.23" />
|
||||
<PackageReference Include="Npgsql.FSharp" Version="5.6.0" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
Loading…
Reference in New Issue
Block a user