From 3399a19ac8d1efefb82abfba524e21c6b6ebd349 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 20 Feb 2023 16:51:00 -0500 Subject: [PATCH] Switch to published doc lib --- src/MyWebLog.Data/MyWebLog.Data.fsproj | 2 +- .../Postgres/PostgresCategoryData.fs | 31 ++- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 52 ++-- .../Postgres/PostgresPageData.fs | 58 ++--- .../Postgres/PostgresPostData.fs | 140 +++++----- .../Postgres/PostgresTagMapData.fs | 34 +-- .../Postgres/PostgresThemeData.fs | 83 ++---- .../Postgres/PostgresUploadData.fs | 49 ++-- .../Postgres/PostgresWebLogData.fs | 28 +- .../Postgres/PostgresWebLogUserData.fs | 41 ++- src/MyWebLog.Data/PostgresData.fs | 38 ++- src/MyWebLog.sln | 6 - src/Npgsql.FSharp.Documents/Library.fs | 242 ------------------ .../Npgsql.FSharp.Documents.fsproj | 12 - 14 files changed, 220 insertions(+), 596 deletions(-) delete mode 100644 src/Npgsql.FSharp.Documents/Library.fs delete mode 100644 src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj 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 @@ - - - - - - - - - - - -