From 52e279529a83d835b32c7cbb089932e0323d4545 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 2 Sep 2022 22:29:32 -0400 Subject: [PATCH 01/17] First cut at Postgres JSON implementation --- .../Postgres/PostgresCategoryData.fs | 131 +++--- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 382 +++++++++++------- .../Postgres/PostgresPageData.fs | 184 ++------- .../Postgres/PostgresPostData.fs | 244 +++-------- .../Postgres/PostgresTagMapData.fs | 71 +--- .../Postgres/PostgresThemeData.fs | 144 ++----- .../Postgres/PostgresUploadData.fs | 20 +- .../Postgres/PostgresWebLogData.fs | 215 ++-------- .../Postgres/PostgresWebLogUserData.fs | 97 ++--- src/MyWebLog.Data/PostgresData.fs | 210 +++------- src/MyWebLog.Data/RethinkDbData.fs | 2 +- 11 files changed, 588 insertions(+), 1112 deletions(-) diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index eec7703..c84ac76 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -2,33 +2,30 @@ open MyWebLog open MyWebLog.Data +open Newtonsoft.Json open Npgsql open Npgsql.FSharp /// PostgreSQL myWebLog category data implementation -type PostgresCategoryData (conn : NpgsqlConnection) = +type PostgresCategoryData (conn : NpgsqlConnection, ser : JsonSerializer) = + + /// Convert a data row to a category + let toCategory = Map.fromDoc ser /// Count all categories for the given web log let countAll webLogId = - Sql.existingConnection conn - |> Sql.query $"SELECT COUNT(id) AS {countName} FROM category WHERE web_log_id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toCount + Document.countByWebLog conn Table.Category webLogId None /// Count all top-level categories for the given web log let countTopLevel webLogId = - Sql.existingConnection conn - |> Sql.query $"SELECT COUNT(id) AS {countName} FROM category WHERE web_log_id = @webLogId AND parent_id IS NULL" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toCount + Document.countByWebLog conn Table.Category webLogId + (Some $"AND data -> '{nameof Category.empty.ParentId}' IS NULL") /// Retrieve all categories for the given web log in a DotLiquid-friendly format let findAllForView webLogId = backgroundTask { let! cats = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM category WHERE web_log_id = @webLogId ORDER BY LOWER(name)" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync Map.toCategory + Document.findByWebLog conn Table.Category webLogId toCategory + (Some $"ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')") let ordered = Utils.orderByHierarchy cats None None [] let counts = ordered @@ -40,16 +37,15 @@ type PostgresCategoryData (conn : NpgsqlConnection) = |> Seq.map (fun cat -> cat.Id) |> Seq.append (Seq.singleton it.Id) |> List.ofSeq - |> inClause "AND pc.category_id" "id" id + |> jsonArrayInClause (nameof Post.empty.CategoryIds) id let postCount = Sql.existingConnection conn |> Sql.query $" - SELECT COUNT(DISTINCT p.id) AS {countName} - FROM post p - INNER JOIN post_category pc ON pc.post_id = p.id - WHERE p.web_log_id = @webLogId - AND p.status = 'Published' - {catIdSql}" + SELECT COUNT(DISTINCT id) AS {countName} + FROM {Table.Post} + WHERE {webLogWhere} + AND data ->> '{nameof Post.empty.Status}' = '{PostStatus.toString Published}' + AND ({catIdSql})" |> Sql.parameters (webLogIdParam webLogId :: catIdParams) |> Sql.executeRowAsync Map.toCount |> Async.AwaitTask @@ -69,19 +65,17 @@ type PostgresCategoryData (conn : NpgsqlConnection) = } /// Find a category by its ID for the given web log let findById catId webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM category WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters [ "@id", Sql.string (CategoryId.toString catId); webLogIdParam webLogId ] - |> Sql.executeAsync Map.toCategory - |> tryHead + Document.findByIdAndWebLog conn Table.Category catId CategoryId.toString webLogId toCategory /// Find all categories for the given web log let findByWebLog webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM category WHERE web_log_id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync Map.toCategory + Document.findByWebLog conn Table.Category webLogId toCategory None + /// Create parameters for a category insert / update + let catParameters (cat : Category) = [ + "@id", Sql.string (CategoryId.toString cat.Id) + "@data", Sql.jsonb (Utils.serialize ser cat) + ] /// Delete a category let delete catId webLogId = backgroundTask { @@ -89,65 +83,50 @@ type PostgresCategoryData (conn : NpgsqlConnection) = | Some cat -> // Reassign any children to the category's parent category let parentParam = "@parentId", Sql.string (CategoryId.toString catId) - let! hasChildren = + let! children = Sql.existingConnection conn - |> Sql.query $"SELECT EXISTS (SELECT 1 FROM category WHERE parent_id = @parentId) AS {existsName}" + |> Sql.query + $"SELECT * FROM {Table.Category} WHERE data ->> '{nameof Category.empty.ParentId}' = @parentId" |> Sql.parameters [ parentParam ] - |> Sql.executeRowAsync Map.toExists + |> Sql.executeAsync toCategory + let hasChildren = not (List.isEmpty children) if hasChildren then let! _ = Sql.existingConnection conn - |> Sql.query "UPDATE category SET parent_id = @newParentId WHERE parent_id = @parentId" - |> Sql.parameters - [ parentParam - "@newParentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString) ] - |> Sql.executeNonQueryAsync + |> Sql.executeTransactionAsync [ + docUpdateSql Table.Category, + children |> List.map (fun child -> catParameters { child with ParentId = cat.ParentId }) + ] () - // Delete the category off all posts where it is assigned, and the category itself - let! _ = + // Delete the category off all posts where it is assigned + let! posts = Sql.existingConnection conn - |> Sql.query - "DELETE FROM post_category - WHERE category_id = @id - AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId); - DELETE FROM category WHERE id = @id" - |> Sql.parameters [ "@id", Sql.string (CategoryId.toString catId); webLogIdParam webLogId ] - |> Sql.executeNonQueryAsync + |> Sql.query $"SELECT * FROM {Table.Post} WHERE data -> '{nameof Post.empty.CategoryIds}' ? @id" + |> Sql.parameters [ "@id", Sql.jsonb (CategoryId.toString catId) ] + |> Sql.executeAsync (Map.fromDoc ser) + if not (List.isEmpty posts) then + let! _ = + Sql.existingConnection conn + |> Sql.executeTransactionAsync [ + docUpdateSql Table.Post, + posts |> List.map (fun post -> [ + "@id", Sql.string (PostId.toString post.Id) + "@data", Sql.jsonb (Utils.serialize ser { + post with + CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) + }) + ]) + ] + () + // Delete the category itself + do! Document.delete conn Table.Category (CategoryId.toString catId) return if hasChildren then ReassignedChildCategories else CategoryDeleted | None -> return CategoryNotFound } - /// The INSERT statement for a category - let catInsert = - "INSERT INTO category ( - id, web_log_id, name, slug, description, parent_id - ) VALUES ( - @id, @webLogId, @name, @slug, @description, @parentId - )" - - /// Create parameters for a category insert / update - let catParameters (cat : Category) = [ - webLogIdParam cat.WebLogId - "@id", Sql.string (CategoryId.toString cat.Id) - "@name", Sql.string cat.Name - "@slug", Sql.string cat.Slug - "@description", Sql.stringOrNone cat.Description - "@parentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString) - ] - /// Save a category let save cat = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query $" - {catInsert} ON CONFLICT (id) DO UPDATE - SET name = EXCLUDED.name, - slug = EXCLUDED.slug, - description = EXCLUDED.description, - parent_id = EXCLUDED.parent_id" - |> Sql.parameters (catParameters cat) - |> Sql.executeNonQueryAsync - () + do! Document.upsert conn Table.Category catParameters cat } /// Restore categories from a backup @@ -155,7 +134,7 @@ type PostgresCategoryData (conn : NpgsqlConnection) = let! _ = Sql.existingConnection conn |> Sql.executeTransactionAsync [ - catInsert, cats |> List.map catParameters + docInsertSql Table.Category, cats |> List.map catParameters ] () } diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 4f289ab..2280095 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -2,15 +2,74 @@ [] module MyWebLog.Data.Postgres.PostgresHelpers +/// The table names used in the PostgreSQL implementation +[] +module Table = + + /// Categories + [] + let Category = "category" + + /// Database Version + [] + let DbVersion = "db_version" + + /// Pages + [] + let Page = "page" + + /// Page Revisions + [] + let PageRevision = "page_revision" + + /// Posts + [] + let Post = "post" + + /// Post Comments + [] + let PostComment = "post_comment" + + /// Post Revisions + [] + let PostRevision = "post_revision" + + /// Tag/URL Mappings + [] + let TagMap = "tag_map" + + /// Themes + [] + let Theme = "theme" + + /// Theme Assets + [] + let ThemeAsset = "theme_asset" + + /// Uploads + [] + let Upload = "upload" + + /// Web Logs + [] + let WebLog = "web_log" + + /// Users + [] + let WebLogUser = "web_log_user" + + open System open System.Threading.Tasks open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open NodaTime open Npgsql open Npgsql.FSharp +/// Create a WHERE clause fragment for the web log ID +let webLogWhere = "data ->> 'WebLogId' = @webLogId" + /// Create a SQL parameter for the web log ID let webLogIdParam webLogId = "@webLogId", Sql.string (WebLogId.toString webLogId) @@ -37,8 +96,8 @@ let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : ' |> Seq.head) |> function sql, ps -> $"{sql})", ps -/// Create the SQL and parameters for the array equivalent of an IN clause -let arrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) = +/// Create the SQL and parameters for the array-in-JSON equivalent of an IN clause +let jsonArrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) = if List.isEmpty items then "TRUE = FALSE", [] else let mutable idx = 0 @@ -46,11 +105,11 @@ let arrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) = |> List.skip 1 |> List.fold (fun (itemS, itemP) it -> idx <- idx + 1 - $"{itemS} OR %s{name} && ARRAY[@{name}{idx}]", - ($"@{name}{idx}", Sql.string (valueFunc it)) :: itemP) + $"{itemS} OR data -> '%s{name}' ? @{name}{idx}", + ($"@{name}{idx}", Sql.jsonb (valueFunc it)) :: itemP) (Seq.ofList items |> Seq.map (fun it -> - $"{name} && ARRAY[@{name}0]", [ $"@{name}0", Sql.string (valueFunc it) ]) + $"data -> '{name}' ? @{name}0", [ $"@{name}0", Sql.string (valueFunc it) ]) |> Seq.head) /// Get the first result of the given query @@ -68,116 +127,63 @@ let optParam<'T> name (it : 'T option) = let p = NpgsqlParameter ($"@%s{name}", if Option.isSome it then box it.Value else DBNull.Value) p.ParameterName, Sql.parameter p +/// SQL statement to insert into a document table +let docInsertSql table = + $"INSERT INTO %s{table} VALUES (@id, @data)" + +/// SQL statement to select a document by its ID +let docSelectSql table = + $"SELECT * FROM %s{table} WHERE id = @id" + +/// SQL statement to select documents by their web log IDs +let docSelectForWebLogSql table = + $"SELECT * FROM %s{table} WHERE {webLogWhere}" + +/// SQL statement to update a document in a document table +let docUpdateSql table = + $"UPDATE %s{table} SET data = @data WHERE id = @id" + +/// SQL statement to insert or update a document in a document table +let docUpsertSql table = + $"{docInsertSql table} ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data" + +/// SQL statement to delete a document from a document table by its ID +let docDeleteSql table = + $"DELETE FROM %s{table} WHERE id = @id" + +/// SQL statement to count documents for a web log +let docCountForWebLogSql table = + $"SELECT COUNT(id) AS {countName} FROM %s{table} WHERE {webLogWhere}" + +/// SQL statement to determine if a document exists for a web log +let docExistsForWebLogSql table = + $"SELECT EXISTS (SELECT 1 FROM %s{table} WHERE id = @id AND {webLogWhere}) AS {existsName}" + /// Mapping functions for SQL queries module Map = - /// Map an id field to a category ID - let toCategoryId (row : RowReader) = - CategoryId (row.string "id") + /// Map an item by deserializing the document + let fromDoc<'T> ser (row : RowReader) = + Utils.deserialize<'T> ser (row.string "data") - /// Create a category from the current row - let toCategory (row : RowReader) : Category = - { Id = toCategoryId row - WebLogId = row.string "web_log_id" |> WebLogId - Name = row.string "name" - Slug = row.string "slug" - Description = row.stringOrNone "description" - ParentId = row.stringOrNone "parent_id" |> Option.map CategoryId - } - /// Get a count from a row let toCount (row : RowReader) = row.int countName - /// Create a custom feed from the current row - let toCustomFeed (ser : JsonSerializer) (row : RowReader) : CustomFeed = - { Id = row.string "id" |> CustomFeedId - Source = row.string "source" |> CustomFeedSource.parse - Path = row.string "path" |> Permalink - Podcast = row.stringOrNone "podcast" |> Option.map (Utils.deserialize ser) - } - /// Get a true/false value as to whether an item exists let toExists (row : RowReader) = row.bool existsName - /// Create a meta item from the current row - let toMetaItem (row : RowReader) : MetaItem = - { Name = row.string "name" - Value = row.string "value" - } - /// Create a permalink from the current row let toPermalink (row : RowReader) = Permalink (row.string "permalink") - /// Create a page from the current row - let toPage (ser : JsonSerializer) (row : RowReader) : Page = - { Page.empty with - Id = row.string "id" |> PageId - WebLogId = row.string "web_log_id" |> WebLogId - AuthorId = row.string "author_id" |> WebLogUserId - Title = row.string "title" - Permalink = toPermalink row - PriorPermalinks = row.stringArray "prior_permalinks" |> Array.map Permalink |> List.ofArray - PublishedOn = row.fieldValue "published_on" - UpdatedOn = row.fieldValue "updated_on" - IsInPageList = row.bool "is_in_page_list" - Template = row.stringOrNone "template" - Text = row.string "page_text" - Metadata = row.stringOrNone "meta_items" - |> Option.map (Utils.deserialize ser) - |> Option.defaultValue [] - } - - /// Create a post from the current row - let toPost (ser : JsonSerializer) (row : RowReader) : Post = - { Post.empty with - Id = row.string "id" |> PostId - WebLogId = row.string "web_log_id" |> WebLogId - AuthorId = row.string "author_id" |> WebLogUserId - Status = row.string "status" |> PostStatus.parse - Title = row.string "title" - Permalink = toPermalink row - PriorPermalinks = row.stringArray "prior_permalinks" |> Array.map Permalink |> List.ofArray - PublishedOn = row.fieldValueOrNone "published_on" - UpdatedOn = row.fieldValue "updated_on" - Template = row.stringOrNone "template" - Text = row.string "post_text" - Episode = row.stringOrNone "episode" |> Option.map (Utils.deserialize ser) - CategoryIds = row.stringArrayOrNone "category_ids" - |> Option.map (Array.map CategoryId >> List.ofArray) - |> Option.defaultValue [] - Tags = row.stringArrayOrNone "tags" - |> Option.map List.ofArray - |> Option.defaultValue [] - Metadata = row.stringOrNone "meta_items" - |> Option.map (Utils.deserialize ser) - |> Option.defaultValue [] - } - /// Create a revision from the current row let toRevision (row : RowReader) : Revision = { AsOf = row.fieldValue "as_of" Text = row.string "revision_text" |> MarkupText.parse } - /// Create a tag mapping from the current row - let toTagMap (row : RowReader) : TagMap = - { Id = row.string "id" |> TagMapId - WebLogId = row.string "web_log_id" |> WebLogId - Tag = row.string "tag" - UrlValue = row.string "url_value" - } - - /// Create a theme from the current row (excludes templates) - let toTheme (row : RowReader) : Theme = - { Theme.empty with - Id = row.string "id" |> ThemeId - Name = row.string "name" - Version = row.string "version" - } - /// Create a theme asset from the current row let toThemeAsset includeData (row : RowReader) : ThemeAsset = { Id = ThemeAssetId (ThemeId (row.string "theme_id"), row.string "path") @@ -185,12 +191,6 @@ module Map = Data = if includeData then row.bytea "data" else [||] } - /// Create a theme template from the current row - let toThemeTemplate includeText (row : RowReader) : ThemeTemplate = - { Name = row.string "name" - Text = if includeText then row.string "template" else "" - } - /// Create an uploaded file from the current row let toUpload includeData (row : RowReader) : Upload = { Id = row.string "id" |> UploadId @@ -199,42 +199,150 @@ module Map = UpdatedOn = row.fieldValue "updated_on" Data = if includeData then row.bytea "data" else [||] } + +/// Document manipulation functions +module Document = - /// Create a web log from the current row - let toWebLog (row : RowReader) : WebLog = - { Id = row.string "id" |> WebLogId - Name = row.string "name" - Slug = row.string "slug" - Subtitle = row.stringOrNone "subtitle" - DefaultPage = row.string "default_page" - PostsPerPage = row.int "posts_per_page" - ThemeId = row.string "theme_id" |> ThemeId - UrlBase = row.string "url_base" - TimeZone = row.string "time_zone" - AutoHtmx = row.bool "auto_htmx" - Uploads = row.string "uploads" |> UploadDestination.parse - Rss = { - IsFeedEnabled = row.bool "is_feed_enabled" - FeedName = row.string "feed_name" - ItemsInFeed = row.intOrNone "items_in_feed" - IsCategoryEnabled = row.bool "is_category_enabled" - IsTagEnabled = row.bool "is_tag_enabled" - Copyright = row.stringOrNone "copyright" - CustomFeeds = [] - } - } + /// Convert extra SQL to a for that can be appended to a query + let private moreSql sql = sql |> Option.map (fun it -> $" %s{it}") |> Option.defaultValue "" - /// Create a web log user from the current row - let toWebLogUser (row : RowReader) : WebLogUser = - { Id = row.string "id" |> WebLogUserId - WebLogId = row.string "web_log_id" |> WebLogId - Email = row.string "email" - FirstName = row.string "first_name" - LastName = row.string "last_name" - PreferredName = row.string "preferred_name" - PasswordHash = row.string "password_hash" - Url = row.stringOrNone "url" - AccessLevel = row.string "access_level" |> AccessLevel.parse - CreatedOn = row.fieldValue "created_on" - LastSeenOn = row.fieldValueOrNone "last_seen_on" - } + /// Count documents for a web log + let countByWebLog conn table webLogId extraSql = + Sql.existingConnection conn + |> Sql.query $"{docCountForWebLogSql table}{moreSql extraSql}" + |> Sql.parameters [ webLogIdParam webLogId ] + |> Sql.executeRowAsync Map.toCount + + /// Delete a document + let delete conn table idParam = backgroundTask { + let! _ = + Sql.existingConnection conn + |> Sql.query (docDeleteSql table) + |> Sql.parameters [ "@id", Sql.string idParam ] + |> Sql.executeNonQueryAsync + () + } + + /// Determine if a document with the given ID exists + let exists<'TKey> conn table (key : 'TKey) (keyFunc : 'TKey -> string) = + Sql.existingConnection conn + |> Sql.query $"SELECT EXISTS (SELECT 1 FROM %s{table} WHERE id = @id) AS {existsName}" + |> Sql.parameters [ "@id", Sql.string (keyFunc key) ] + |> Sql.executeRowAsync Map.toExists + + /// Determine whether a document exists with the given key for the given web log + let existsByWebLog<'TKey> conn table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId = + Sql.existingConnection conn + |> Sql.query (docExistsForWebLogSql table) + |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogIdParam webLogId ] + |> Sql.executeRowAsync Map.toExists + + /// Find a document by its ID + let findById<'TKey, 'TDoc> conn table (key : 'TKey) (keyFunc : 'TKey -> string) (docFunc : RowReader -> 'TDoc) = + Sql.existingConnection conn + |> Sql.query (docSelectSql table) + |> Sql.parameters [ "@id", Sql.string (keyFunc key) ] + |> Sql.executeAsync docFunc + |> tryHead + + /// Find a document by its ID for the given web log + let findByIdAndWebLog<'TKey, 'TDoc> conn table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId + (docFunc : RowReader -> 'TDoc) = + Sql.existingConnection conn + |> Sql.query $"{docSelectSql table} AND {webLogWhere}" + |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogIdParam webLogId ] + |> Sql.executeAsync docFunc + |> tryHead + + /// Find all documents for the given web log + let findByWebLog<'TDoc> conn table webLogId (docFunc : RowReader -> 'TDoc) extraSql = + Sql.existingConnection conn + |> Sql.query $"{docSelectForWebLogSql table}{moreSql extraSql}" + |> Sql.parameters [ webLogIdParam webLogId ] + |> Sql.executeAsync docFunc + + /// Insert a new document + let insert<'T> conn table (paramFunc : 'T -> (string * SqlValue) list) (doc : 'T) = task { + let! _ = + Sql.existingConnection conn + |> Sql.query (docInsertSql table) + |> Sql.parameters (paramFunc doc) + |> Sql.executeNonQueryAsync + () + } + + /// Update an existing document + let update<'T> conn table (paramFunc : 'T -> (string * SqlValue) list) (doc : 'T) = task { + let! _ = + Sql.existingConnection conn + |> Sql.query (docUpdateSql table) + |> Sql.parameters (paramFunc doc) + |> Sql.executeNonQueryAsync + () + } + + /// Insert or update a document + let upsert<'T> conn table (paramFunc : 'T -> (string * SqlValue) list) (doc : 'T) = task { + let! _ = + Sql.existingConnection conn + |> Sql.query (docUpsertSql table) + |> Sql.parameters (paramFunc doc) + |> Sql.executeNonQueryAsync + () + } + + +/// Functions to support revisions +module Revisions = + + /// Find all revisions for the given entity + let findByEntityId<'TKey> conn revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) = + Sql.existingConnection conn + |> 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 + + /// Find all revisions for all posts for the given web log + let findByWebLog<'TKey> conn revTable entityTable (keyFunc : string -> 'TKey) webLogId = + Sql.existingConnection conn + |> Sql.query $" + SELECT pr.* + FROM %s{revTable} pr + INNER JOIN %s{entityTable} p ON p.id = pr.{entityTable}_id + WHERE p.{webLogWhere} + ORDER BY as_of DESC" + |> Sql.parameters [ webLogIdParam webLogId ] + |> Sql.executeAsync (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 = [ + typedParam "asOf" rev.AsOf + "@id", Sql.string (keyFunc key) + "@text", Sql.string (MarkupText.toString rev.Text) + ] + + /// The SQL statement to insert a revision + let insertSql table = + $"INSERT INTO %s{table} VALUES (@id, @asOf, @text)" + + /// Update a page's revisions + let update<'TKey> + conn 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.existingConnection conn + |> Sql.executeTransactionAsync [ + if not (List.isEmpty toDelete) then + $"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf", + toDelete + |> List.map (fun it -> [ + "@id", Sql.string (keyFunc key) + typedParam "asOf" it.AsOf + ]) + if not (List.isEmpty toAdd) then + insertSql revTable, toAdd |> List.map (revParams key keyFunc) + ] + () + } + diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 48ab3c3..3a82203 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -11,94 +11,45 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = // SUPPORT FUNCTIONS - /// Append revisions and permalinks to a page + /// Append revisions to a page let appendPageRevisions (page : Page) = backgroundTask { - let! revisions = - Sql.existingConnection conn - |> Sql.query "SELECT as_of, revision_text FROM page_revision WHERE page_id = @pageId ORDER BY as_of DESC" - |> Sql.parameters [ "@pageId", Sql.string (PageId.toString page.Id) ] - |> Sql.executeAsync Map.toRevision + let! revisions = Revisions.findByEntityId conn Table.PageRevision Table.Page page.Id PageId.toString return { page with Revisions = revisions } } /// Shorthand to map to a page - let toPage = Map.toPage ser + let toPage = Map.fromDoc ser /// Return a page with no text or revisions let pageWithoutText row = { toPage row with Text = "" } - /// The INSERT statement for a page revision - let revInsert = "INSERT INTO page_revision VALUES (@pageId, @asOf, @text)" - - /// Parameters for a revision INSERT statement - let revParams pageId rev = [ - typedParam "asOf" rev.AsOf - "@pageId", Sql.string (PageId.toString pageId) - "@text", Sql.string (MarkupText.toString rev.Text) - ] - /// Update a page's revisions - let updatePageRevisions pageId oldRevs newRevs = backgroundTask { - let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs - if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then - let! _ = - Sql.existingConnection conn - |> Sql.executeTransactionAsync [ - if not (List.isEmpty toDelete) then - "DELETE FROM page_revision WHERE page_id = @pageId AND as_of = @asOf", - toDelete - |> List.map (fun it -> [ - "@pageId", Sql.string (PageId.toString pageId) - typedParam "asOf" it.AsOf - ]) - if not (List.isEmpty toAdd) then - revInsert, toAdd |> List.map (revParams pageId) - ] - () - } + let updatePageRevisions pageId oldRevs newRevs = + Revisions.update conn Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs /// Does the given page exist? let pageExists pageId webLogId = - Sql.existingConnection conn - |> Sql.query $"SELECT EXISTS (SELECT 1 FROM page WHERE id = @id AND web_log_id = @webLogId) AS {existsName}" - |> Sql.parameters [ "@id", Sql.string (PageId.toString pageId); webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toExists + Document.existsByWebLog conn Table.Page pageId PageId.toString webLogId // IMPLEMENTATION FUNCTIONS - /// Get all pages for a web log (without text, revisions, prior permalinks, or metadata) + /// Get all pages for a web log (without text or revisions) let all webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId ORDER BY LOWER(title)" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync pageWithoutText + Document.findByWebLog conn Table.Page webLogId pageWithoutText + (Some $"ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')") /// Count all pages for the given web log let countAll webLogId = - Sql.existingConnection conn - |> Sql.query $"SELECT COUNT(id) AS {countName} FROM page WHERE web_log_id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toCount + Document.countByWebLog conn Table.Page webLogId None /// Count all pages shown in the page list for the given web log let countListed webLogId = - Sql.existingConnection conn - |> Sql.query $" - SELECT COUNT(id) AS {countName} - FROM page - WHERE web_log_id = @webLogId - AND is_in_page_list = TRUE" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toCount + Document.countByWebLog conn Table.Page webLogId (Some $"AND data -> '{nameof Page.empty.IsInPageList}' = TRUE") /// Find a page by its ID (without revisions) let findById pageId webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM page WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters [ "@id", Sql.string (PageId.toString pageId); webLogIdParam webLogId ] - |> Sql.executeAsync toPage - |> tryHead + Document.findByIdAndWebLog conn Table.Page pageId PageId.toString webLogId toPage /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { @@ -113,13 +64,7 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = let delete pageId webLogId = backgroundTask { match! pageExists pageId webLogId with | true -> - let! _ = - Sql.existingConnection conn - |> Sql.query - "DELETE FROM page_revision WHERE page_id = @id; - DELETE FROM page WHERE id = @id" - |> Sql.parameters [ "@id", Sql.string (PageId.toString pageId) ] - |> Sql.executeNonQueryAsync + do! Document.delete conn Table.Page (PageId.toString pageId) return true | false -> return false } @@ -127,7 +72,7 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Find a page by its permalink for the given web log let findByPermalink permalink webLogId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId AND permalink = @link" + |> Sql.query $"{docSelectForWebLogSql Table.Page} AND data ->> '{nameof Page.empty.Permalink}' = @link" |> Sql.parameters [ webLogIdParam webLogId; "@link", Sql.string (Permalink.toString permalink) ] |> Sql.executeAsync toPage |> tryHead @@ -136,10 +81,15 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = let findCurrentPermalink permalinks webLogId = backgroundTask { if List.isEmpty permalinks then return None else - let linkSql, linkParams = arrayInClause "prior_permalinks" Permalink.toString permalinks + let linkSql, linkParams = + jsonArrayInClause (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks return! Sql.existingConnection conn - |> Sql.query $"SELECT permalink FROM page WHERE web_log_id = @webLogId AND ({linkSql})" + |> Sql.query $" + SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink + FROM page + WHERE {webLogWhere} + AND ({linkSql})" |> Sql.parameters (webLogIdParam webLogId :: linkParams) |> Sql.executeAsync Map.toPermalink |> tryHead @@ -147,21 +97,8 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Get all complete pages for the given web log let findFullByWebLog webLogId = backgroundTask { - let! pages = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync toPage - let! revisions = - Sql.existingConnection conn - |> Sql.query - "SELECT * - FROM page_revision pr - INNER JOIN page p ON p.id = pr.page_id - WHERE p.web_log_id = @webLogId - ORDER BY pr.as_of DESC" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync (fun row -> PageId (row.string "page_id"), Map.toRevision row) + let! pages = Document.findByWebLog conn Table.Page webLogId toPage None + let! revisions = Revisions.findByWebLog conn Table.PageRevision Table.Page PageId webLogId return pages |> List.map (fun it -> @@ -171,46 +108,27 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Get all listed pages for the given web log (without revisions or text) let findListed webLogId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId AND is_in_page_list = TRUE ORDER BY LOWER(title)" + |> Sql.query $" + {docSelectForWebLogSql Table.Page} + AND data -> '{nameof Page.empty.IsInPageList}' = TRUE + ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')" |> Sql.parameters [ webLogIdParam webLogId ] |> Sql.executeAsync pageWithoutText /// Get a page of pages for the given web log (without revisions) let findPageOfPages webLogId pageNbr = Sql.existingConnection conn - |> Sql.query - "SELECT * - FROM page - WHERE web_log_id = @webLogId - ORDER BY LOWER(title) - LIMIT @pageSize OFFSET @toSkip" + |> Sql.query $" + {docSelectForWebLogSql Table.Page} + ORDER BY LOWER(data ->> '{nameof Page.empty.Title}') + LIMIT @pageSize OFFSET @toSkip" |> Sql.parameters [ webLogIdParam webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ] |> Sql.executeAsync toPage - /// The INSERT statement for a page - let pageInsert = - "INSERT INTO page ( - id, web_log_id, author_id, title, permalink, prior_permalinks, published_on, updated_on, is_in_page_list, - template, page_text, meta_items - ) VALUES ( - @id, @webLogId, @authorId, @title, @permalink, @priorPermalinks, @publishedOn, @updatedOn, @isInPageList, - @template, @text, @metaItems - )" - /// The parameters for saving a page let pageParams (page : Page) = [ - webLogIdParam page.WebLogId - "@id", Sql.string (PageId.toString page.Id) - "@authorId", Sql.string (WebLogUserId.toString page.AuthorId) - "@title", Sql.string page.Title - "@permalink", Sql.string (Permalink.toString page.Permalink) - "@isInPageList", Sql.bool page.IsInPageList - "@template", Sql.stringOrNone page.Template - "@text", Sql.string page.Text - "@metaItems", Sql.jsonb (Utils.serialize ser page.Metadata) - "@priorPermalinks", Sql.stringArray (page.PriorPermalinks |> List.map Permalink.toString |> Array.ofList) - typedParam "publishedOn" page.PublishedOn - typedParam "updatedOn" page.UpdatedOn + "@id", Sql.string (PageId.toString page.Id) + "@data", Sql.jsonb (Utils.serialize ser page) ] /// Restore pages from a backup @@ -219,8 +137,9 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = let! _ = Sql.existingConnection conn |> Sql.executeTransactionAsync [ - pageInsert, pages |> List.map pageParams - revInsert, revisions |> List.map (fun (pageId, rev) -> revParams pageId rev) + docInsertSql Table.Page, pages |> List.map pageParams + Revisions.insertSql Table.PageRevision, + revisions |> List.map (fun (pageId, rev) -> Revisions.revParams pageId PageId.toString rev) ] () } @@ -228,39 +147,18 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Save a page let save (page : Page) = backgroundTask { let! oldPage = findFullById page.Id page.WebLogId - let! _ = - Sql.existingConnection conn - |> Sql.query $" - {pageInsert} ON CONFLICT (id) DO UPDATE - SET author_id = EXCLUDED.author_id, - title = EXCLUDED.title, - permalink = EXCLUDED.permalink, - prior_permalinks = EXCLUDED.prior_permalinks, - published_on = EXCLUDED.published_on, - updated_on = EXCLUDED.updated_on, - is_in_page_list = EXCLUDED.is_in_page_list, - template = EXCLUDED.template, - page_text = EXCLUDED.page_text, - meta_items = EXCLUDED.meta_items" - |> Sql.parameters (pageParams page) - |> Sql.executeNonQueryAsync + do! Document.upsert conn Table.Page pageParams page do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions () } /// Update a page's prior permalinks let updatePriorPermalinks pageId webLogId permalinks = backgroundTask { - match! pageExists pageId webLogId with - | true -> - let! _ = - Sql.existingConnection conn - |> Sql.query "UPDATE page SET prior_permalinks = @prior WHERE id = @id" - |> Sql.parameters - [ "@id", Sql.string (PageId.toString pageId) - "@prior", Sql.stringArray (permalinks |> List.map Permalink.toString |> Array.ofList) ] - |> Sql.executeNonQueryAsync + match! findById pageId webLogId with + | Some page -> + do! Document.update conn Table.Page pageParams { page with PriorPermalinks = permalinks } return true - | false -> return false + | None -> return false } interface IPageData with diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index aad6af6..c001442 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -14,109 +14,46 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Append revisions to a post let appendPostRevisions (post : Post) = backgroundTask { - let! revisions = - Sql.existingConnection conn - |> Sql.query "SELECT as_of, revision_text FROM post_revision WHERE post_id = @id ORDER BY as_of DESC" - |> Sql.parameters [ "@id", Sql.string (PostId.toString post.Id) ] - |> Sql.executeAsync Map.toRevision + let! revisions = Revisions.findByEntityId conn Table.PostRevision Table.Post post.Id PostId.toString return { post with Revisions = revisions } } - /// The SELECT statement for a post that will include category IDs - let selectPost = - "SELECT *, ARRAY(SELECT cat.category_id FROM post_category cat WHERE cat.post_id = p.id) AS category_ids - FROM post p" - /// Shorthand for mapping to a post - let toPost = Map.toPost ser + let toPost = Map.fromDoc ser /// Return a post with no revisions, prior permalinks, or text let postWithoutText row = { toPost row with Text = "" } - /// The INSERT statement for a post/category cross-reference - let catInsert = "INSERT INTO post_category VALUES (@postId, @categoryId)" - - /// Parameters for adding or updating a post/category cross-reference - let catParams postId cat = [ - "@postId", Sql.string (PostId.toString postId) - "categoryId", Sql.string (CategoryId.toString cat) - ] - - /// Update a post's assigned categories - let updatePostCategories postId oldCats newCats = backgroundTask { - let toDelete, toAdd = Utils.diffLists oldCats newCats CategoryId.toString - if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then - let! _ = - Sql.existingConnection conn - |> Sql.executeTransactionAsync [ - if not (List.isEmpty toDelete) then - "DELETE FROM post_category WHERE post_id = @postId AND category_id = @categoryId", - toDelete |> List.map (catParams postId) - if not (List.isEmpty toAdd) then - catInsert, toAdd |> List.map (catParams postId) - ] - () - } - - /// The INSERT statement for a post revision - let revInsert = "INSERT INTO post_revision VALUES (@postId, @asOf, @text)" - - /// The parameters for adding a post revision - let revParams postId rev = [ - typedParam "asOf" rev.AsOf - "@postId", Sql.string (PostId.toString postId) - "@text", Sql.string (MarkupText.toString rev.Text) - ] - /// Update a post's revisions - let updatePostRevisions postId oldRevs newRevs = backgroundTask { - let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs - if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then - let! _ = - Sql.existingConnection conn - |> Sql.executeTransactionAsync [ - if not (List.isEmpty toDelete) then - "DELETE FROM post_revision WHERE post_id = @postId AND as_of = @asOf", - toDelete - |> List.map (fun it -> [ - "@postId", Sql.string (PostId.toString postId) - typedParam "asOf" it.AsOf - ]) - if not (List.isEmpty toAdd) then - revInsert, toAdd |> List.map (revParams postId) - ] - () - } + let updatePostRevisions postId oldRevs newRevs = + Revisions.update conn Table.PostRevision Table.Post postId PostId.toString oldRevs newRevs /// Does the given post exist? let postExists postId webLogId = - Sql.existingConnection conn - |> Sql.query $"SELECT EXISTS (SELECT 1 FROM post WHERE id = @id AND web_log_id = @webLogId) AS {existsName}" - |> Sql.parameters [ "@id", Sql.string (PostId.toString postId); webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toExists + Document.existsByWebLog conn Table.Post postId PostId.toString webLogId + + /// Query to select posts by web log ID and status + let postsByWebLogAndStatus = + $"{docSelectForWebLogSql Table.Post} AND data ->> '{nameof Post.empty.Status}' = @status" // IMPLEMENTATION FUNCTIONS /// Count posts in a status for the given web log let countByStatus status webLogId = Sql.existingConnection conn - |> Sql.query $"SELECT COUNT(id) AS {countName} FROM post WHERE web_log_id = @webLogId AND status = @status" + |> Sql.query $"{docCountForWebLogSql Table.Post} AND data ->> '{nameof Post.empty.Status}' = @status" |> Sql.parameters [ webLogIdParam webLogId; "@status", Sql.string (PostStatus.toString status) ] |> Sql.executeRowAsync Map.toCount /// Find a post by its ID for the given web log (excluding revisions) - let findById postId webLogId = - Sql.existingConnection conn - |> Sql.query $"{selectPost} WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters [ "@id", Sql.string (PostId.toString postId); webLogIdParam webLogId ] - |> Sql.executeAsync toPost - |> tryHead + let findById postId webLogId = + Document.findByIdAndWebLog conn Table.Post postId PostId.toString webLogId toPost /// Find a post by its permalink for the given web log (excluding revisions and prior permalinks) let findByPermalink permalink webLogId = Sql.existingConnection conn - |> Sql.query $"{selectPost} WHERE web_log_id = @webLogId AND permalink = @link" + |> Sql.query $"{docSelectForWebLogSql Table.Post} AND data ->> '{nameof Post.empty.Permalink}' = @link" |> Sql.parameters [ webLogIdParam webLogId; "@link", Sql.string (Permalink.toString permalink) ] |> Sql.executeAsync toPost |> tryHead @@ -136,10 +73,9 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = | true -> let! _ = Sql.existingConnection conn - |> Sql.query - "DELETE FROM post_revision WHERE post_id = @id; - DELETE FROM post_category WHERE post_id = @id; - DELETE FROM post WHERE id = @id" + |> Sql.query $" + DELETE FROM {Table.PostComment} WHERE data ->> '{nameof Comment.empty.PostId}' = @id; + DELETE FROM {Table.Post} WHERE id = @id" |> Sql.parameters [ "@id", Sql.string (PostId.toString postId) ] |> Sql.executeNonQueryAsync return true @@ -150,10 +86,15 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let findCurrentPermalink permalinks webLogId = backgroundTask { if List.isEmpty permalinks then return None else - let linkSql, linkParams = arrayInClause "prior_permalinks" Permalink.toString permalinks + let linkSql, linkParams = + jsonArrayInClause (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks return! Sql.existingConnection conn - |> Sql.query $"SELECT permalink FROM post WHERE web_log_id = @webLogId AND ({linkSql})" + |> Sql.query $" + SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink + FROM {Table.Post} + WHERE {webLogWhere} + AND ({linkSql})" |> Sql.parameters (webLogIdParam webLogId :: linkParams) |> Sql.executeAsync Map.toPermalink |> tryHead @@ -161,21 +102,8 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Get all complete posts for the given web log let findFullByWebLog webLogId = backgroundTask { - let! posts = - Sql.existingConnection conn - |> Sql.query $"{selectPost} WHERE web_log_id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync toPost - let! revisions = - Sql.existingConnection conn - |> Sql.query - "SELECT * - FROM post_revision pr - INNER JOIN post p ON p.id = pr.post_id - WHERE p.web_log_id = @webLogId - ORDER BY as_of DESC" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync (fun row -> PostId (row.string "post_id"), Map.toRevision row) + let! posts = Document.findByWebLog conn Table.Post webLogId toPost None + let! revisions = Revisions.findByWebLog conn Table.PostRevision Table.Post PostId webLogId return posts |> List.map (fun it -> @@ -184,14 +112,11 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Get a page of categorized posts for the given web log (excludes revisions) let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = - let catSql, catParams = inClause "AND pc.category_id" "catId" CategoryId.toString categoryIds + let catSql, catParams = jsonArrayInClause (nameof Post.empty.CategoryIds) CategoryId.toString categoryIds Sql.existingConnection conn |> Sql.query $" - {selectPost} - INNER JOIN post_category pc ON pc.post_id = p.id - WHERE p.web_log_id = @webLogId - AND p.status = @status - {catSql} + {postsByWebLogAndStatus} + AND ({catSql}) ORDER BY published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters @@ -204,9 +129,9 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let findPageOfPosts webLogId pageNbr postsPerPage = Sql.existingConnection conn |> Sql.query $" - {selectPost} - WHERE web_log_id = @webLogId - ORDER BY published_on DESC NULLS FIRST, updated_on + {docSelectForWebLogSql 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 [ webLogIdParam webLogId ] |> Sql.executeAsync postWithoutText @@ -215,10 +140,8 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let findPageOfPublishedPosts webLogId pageNbr postsPerPage = Sql.existingConnection conn |> Sql.query $" - {selectPost} - WHERE web_log_id = @webLogId - AND status = @status - ORDER BY published_on DESC + {postsByWebLogAndStatus} + ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters [ webLogIdParam webLogId; "@status", Sql.string (PostStatus.toString Published) ] |> Sql.executeAsync toPost @@ -227,16 +150,14 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage = Sql.existingConnection conn |> Sql.query $" - {selectPost} - WHERE web_log_id = @webLogId - AND status = @status - AND tags && ARRAY[@tag] - ORDER BY published_on DESC + {postsByWebLogAndStatus} + AND data -> '{nameof Post.empty.Tags}' ? @tag + ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters [ webLogIdParam webLogId "@status", Sql.string (PostStatus.toString Published) - "@tag", Sql.string tag + "@tag", Sql.jsonb tag ] |> Sql.executeAsync toPost @@ -250,110 +171,59 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let! older = Sql.existingConnection conn |> Sql.query $" - {selectPost} - WHERE web_log_id = @webLogId - AND status = @status - AND published_on < @publishedOn - ORDER BY published_on DESC + {postsByWebLogAndStatus} + AND data ->> '{nameof Post.empty.PublishedOn}' < @publishedOn + ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT 1" |> queryParams () |> Sql.executeAsync toPost let! newer = Sql.existingConnection conn |> Sql.query $" - {selectPost} - WHERE web_log_id = @webLogId - AND status = @status - AND published_on > @publishedOn - ORDER BY published_on + {postsByWebLogAndStatus} + AND data ->> '{nameof Post.empty.PublishedOn}' > @publishedOn + ORDER BY data ->> '{nameof Post.empty.PublishedOn}' LIMIT 1" |> queryParams () |> Sql.executeAsync toPost return List.tryHead older, List.tryHead newer } - /// The INSERT statement for a post - let postInsert = - "INSERT INTO post ( - id, web_log_id, author_id, status, title, permalink, prior_permalinks, published_on, updated_on, - template, post_text, tags, meta_items, episode - ) VALUES ( - @id, @webLogId, @authorId, @status, @title, @permalink, @priorPermalinks, @publishedOn, @updatedOn, - @template, @text, @tags, @metaItems, @episode - )" - /// The parameters for saving a post let postParams (post : Post) = [ - webLogIdParam post.WebLogId - "@id", Sql.string (PostId.toString post.Id) - "@authorId", Sql.string (WebLogUserId.toString post.AuthorId) - "@status", Sql.string (PostStatus.toString post.Status) - "@title", Sql.string post.Title - "@permalink", Sql.string (Permalink.toString post.Permalink) - "@template", Sql.stringOrNone post.Template - "@text", Sql.string post.Text - "@priorPermalinks", Sql.stringArray (post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList) - "@episode", Sql.jsonbOrNone (post.Episode |> Option.map (Utils.serialize ser)) - "@tags", Sql.stringArrayOrNone (if List.isEmpty post.Tags then None else Some (Array.ofList post.Tags)) - "@metaItems", - if List.isEmpty post.Metadata then None else Some (Utils.serialize ser post.Metadata) - |> Sql.jsonbOrNone - optParam "publishedOn" post.PublishedOn - typedParam "updatedOn" post.UpdatedOn + "@id", Sql.string (PostId.toString post.Id) + "@data", Sql.jsonb (Utils.serialize ser post) ] /// Save a post let save (post : Post) = backgroundTask { let! oldPost = findFullById post.Id post.WebLogId - let! _ = - Sql.existingConnection conn - |> Sql.query $" - {postInsert} ON CONFLICT (id) DO UPDATE - SET author_id = EXCLUDED.author_id, - status = EXCLUDED.status, - title = EXCLUDED.title, - permalink = EXCLUDED.permalink, - prior_permalinks = EXCLUDED.prior_permalinks, - published_on = EXCLUDED.published_on, - updated_on = EXCLUDED.updated_on, - template = EXCLUDED.template, - post_text = EXCLUDED.post_text, - tags = EXCLUDED.tags, - meta_items = EXCLUDED.meta_items, - episode = EXCLUDED.episode" - |> Sql.parameters (postParams post) - |> Sql.executeNonQueryAsync - do! updatePostCategories post.Id (match oldPost with Some p -> p.CategoryIds | None -> []) post.CategoryIds - do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions + do! Document.upsert conn Table.Post postParams post + do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions } /// Restore posts from a backup let restore posts = backgroundTask { - let cats = posts |> List.collect (fun p -> p.CategoryIds |> List.map (fun c -> p.Id, c)) - let revisions = posts |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) + let revisions = posts |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = Sql.existingConnection conn |> Sql.executeTransactionAsync [ - postInsert, posts |> List.map postParams - catInsert, cats |> List.map (fun (postId, catId) -> catParams postId catId) - revInsert, revisions |> List.map (fun (postId, rev) -> revParams postId rev) + docInsertSql Table.Post, posts |> List.map postParams + Revisions.insertSql Table.PostRevision, + revisions |> List.map (fun (postId, rev) -> Revisions.revParams postId PostId.toString rev) ] () } /// Update prior permalinks for a post let updatePriorPermalinks postId webLogId permalinks = backgroundTask { - match! postExists postId webLogId with - | true -> - let! _ = - Sql.existingConnection conn - |> Sql.query "UPDATE post SET prior_permalinks = @prior WHERE id = @id" - |> Sql.parameters - [ "@id", Sql.string (PostId.toString postId) - "@prior", Sql.stringArray (permalinks |> List.map Permalink.toString |> Array.ofList) ] - |> Sql.executeNonQueryAsync + use! txn = conn.BeginTransactionAsync () + match! findById postId webLogId with + | Some post -> + do! Document.update conn Table.Post postParams { post with PriorPermalinks = permalinks } + do! txn.CommitAsync () return true - | false -> return false + | None -> return false } interface IPostData with diff --git a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs index d76bbe6..a04c7fb 100644 --- a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs @@ -2,37 +2,25 @@ namespace MyWebLog.Data.Postgres open MyWebLog open MyWebLog.Data +open Newtonsoft.Json open Npgsql open Npgsql.FSharp /// PostgreSQL myWebLog tag mapping data implementation -type PostgresTagMapData (conn : NpgsqlConnection) = - +type PostgresTagMapData (conn : NpgsqlConnection, ser : JsonSerializer) = + + /// Map a data row to a tag mapping + let toTagMap = Map.fromDoc ser + /// Find a tag mapping by its ID for the given web log let findById tagMapId webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM tag_map WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters [ "@id", Sql.string (TagMapId.toString tagMapId); webLogIdParam webLogId ] - |> Sql.executeAsync Map.toTagMap - |> tryHead + Document.findByIdAndWebLog conn Table.TagMap tagMapId TagMapId.toString webLogId toTagMap /// Delete a tag mapping for the given web log let delete tagMapId webLogId = backgroundTask { - let idParams = [ "@id", Sql.string (TagMapId.toString tagMapId) ] - let! exists = - Sql.existingConnection conn - |> Sql.query $" - SELECT EXISTS - (SELECT 1 FROM tag_map WHERE id = @id AND web_log_id = @webLogId) - AS {existsName}" - |> Sql.parameters (webLogIdParam webLogId :: idParams) - |> Sql.executeRowAsync Map.toExists + let! exists = Document.existsByWebLog conn Table.TagMap tagMapId TagMapId.toString webLogId if exists then - let! _ = - Sql.existingConnection conn - |> Sql.query "DELETE FROM tag_map WHERE id = @id" - |> Sql.parameters idParams - |> Sql.executeNonQueryAsync + do! Document.delete conn Table.TagMap (TagMapId.toString tagMapId) return true else return false } @@ -40,53 +28,32 @@ type PostgresTagMapData (conn : NpgsqlConnection) = /// Find a tag mapping by its URL value for the given web log let findByUrlValue urlValue webLogId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM tag_map WHERE web_log_id = @webLogId AND url_value = @urlValue" + |> Sql.query $"{docSelectForWebLogSql Table.TagMap} AND data ->> '{nameof TagMap.empty.UrlValue}' = @urlValue" |> Sql.parameters [ webLogIdParam webLogId; "@urlValue", Sql.string urlValue ] - |> Sql.executeAsync Map.toTagMap + |> Sql.executeAsync toTagMap |> tryHead /// Get all tag mappings for the given web log let findByWebLog webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM tag_map WHERE web_log_id = @webLogId ORDER BY tag" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync Map.toTagMap + Document.findByWebLog conn Table.TagMap webLogId toTagMap (Some "ORDER BY tag") /// Find any tag mappings in a list of tags for the given web log let findMappingForTags tags webLogId = - let tagSql, tagParams = inClause "AND tag" "tag" id tags + let tagSql, tagParams = jsonArrayInClause (nameof TagMap.empty.Tag) id tags Sql.existingConnection conn - |> Sql.query $"SELECT * FROM tag_map WHERE web_log_id = @webLogId {tagSql}" + |> Sql.query $"{docSelectForWebLogSql Table.TagMap} AND ({tagSql})" |> Sql.parameters (webLogIdParam webLogId :: tagParams) - |> Sql.executeAsync Map.toTagMap - - /// The INSERT statement for a tag mapping - let tagMapInsert = - "INSERT INTO tag_map ( - id, web_log_id, tag, url_value - ) VALUES ( - @id, @webLogId, @tag, @urlValue - )" + |> Sql.executeAsync toTagMap /// The parameters for saving a tag mapping let tagMapParams (tagMap : TagMap) = [ - webLogIdParam tagMap.WebLogId - "@id", Sql.string (TagMapId.toString tagMap.Id) - "@tag", Sql.string tagMap.Tag - "@urlValue", Sql.string tagMap.UrlValue + "@id", Sql.string (TagMapId.toString tagMap.Id) + "@data", Sql.jsonb (Utils.serialize ser tagMap) ] /// Save a tag mapping let save tagMap = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query $" - {tagMapInsert} ON CONFLICT (id) DO UPDATE - SET tag = EXCLUDED.tag, - url_value = EXCLUDED.url_value" - |> Sql.parameters (tagMapParams tagMap) - |> Sql.executeNonQueryAsync - () + do! Document.upsert conn Table.TagMap tagMapParams tagMap } /// Restore tag mappings from a backup @@ -94,7 +61,7 @@ type PostgresTagMapData (conn : NpgsqlConnection) = let! _ = Sql.existingConnection conn |> Sql.executeTransactionAsync [ - tagMapInsert, tagMaps |> List.map tagMapParams + docInsertSql Table.TagMap, tagMaps |> List.map tagMapParams ] () } diff --git a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs index be2805d..2e3bacf 100644 --- a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs @@ -2,127 +2,57 @@ open MyWebLog open MyWebLog.Data +open Newtonsoft.Json open Npgsql open Npgsql.FSharp /// PostreSQL myWebLog theme data implementation -type PostgresThemeData (conn : NpgsqlConnection) = +type PostgresThemeData (conn : NpgsqlConnection, ser : JsonSerializer) = + + /// Map a data row to a theme + let toTheme = Map.fromDoc ser + + /// Clear out the template text from a theme + let withoutTemplateText row = + let theme = toTheme row + { theme with Templates = theme.Templates |> List.map (fun template -> { template with Text = "" }) } /// Retrieve all themes (except 'admin'; excludes template text) - let all () = backgroundTask { - let! themes = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM theme WHERE id <> 'admin' ORDER BY id" - |> Sql.executeAsync Map.toTheme - let! templates = - Sql.existingConnection conn - |> Sql.query "SELECT name, theme_id FROM theme_template WHERE theme_id <> 'admin' ORDER BY name" - |> Sql.executeAsync (fun row -> ThemeId (row.string "theme_id"), Map.toThemeTemplate false row) - return - themes - |> List.map (fun t -> - { t with Templates = templates |> List.filter (fun tt -> fst tt = t.Id) |> List.map snd }) - } + let all () = + Sql.existingConnection conn + |> Sql.query $"SELECT * FROM {Table.Theme} WHERE id <> 'admin' ORDER BY id" + |> Sql.executeAsync withoutTemplateText /// Does a given theme exist? let exists themeId = - Sql.existingConnection conn - |> Sql.query "SELECT EXISTS (SELECT 1 FROM theme WHERE id = @id) AS does_exist" - |> Sql.parameters [ "@id", Sql.string (ThemeId.toString themeId) ] - |> Sql.executeRowAsync Map.toExists + Document.exists conn Table.Theme themeId ThemeId.toString /// Find a theme by its ID - let findById themeId = backgroundTask { - let themeIdParam = [ "@id", Sql.string (ThemeId.toString themeId) ] - let! theme = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM theme WHERE id = @id" - |> Sql.parameters themeIdParam - |> Sql.executeAsync Map.toTheme - |> tryHead - if Option.isSome theme then - let! templates = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM theme_template WHERE theme_id = @id" - |> Sql.parameters themeIdParam - |> Sql.executeAsync (Map.toThemeTemplate true) - return Some { theme.Value with Templates = templates } - else return None - } + let findById themeId = + Document.findById conn Table.Theme themeId ThemeId.toString toTheme /// Find a theme by its ID (excludes the text of templates) - let findByIdWithoutText themeId = backgroundTask { - match! findById themeId with - | Some theme -> - return Some { - theme with Templates = theme.Templates |> List.map (fun t -> { t with Text = "" }) - } - | None -> return None - } + let findByIdWithoutText themeId = + Document.findById conn Table.Theme themeId ThemeId.toString withoutTemplateText /// Delete a theme by its ID let delete themeId = backgroundTask { - let idParams = [ "@id", Sql.string (ThemeId.toString themeId) ] - let! exists = - Sql.existingConnection conn - |> Sql.query $"SELECT EXISTS (SELECT 1 FROM theme WHERE id = @id) AS {existsName}" - |> Sql.parameters idParams - |> Sql.executeRowAsync Map.toExists - if exists then - let! _ = - Sql.existingConnection conn - |> Sql.query - "DELETE FROM theme_asset WHERE theme_id = @id; - DELETE FROM theme_template WHERE theme_id = @id; - DELETE FROM theme WHERE id = @id" - |> Sql.parameters idParams - |> Sql.executeNonQueryAsync + match! exists themeId with + | true -> + do! Document.delete conn Table.Theme (ThemeId.toString themeId) return true - else return false + | false -> return false } + /// Create theme save parameters + let themeParams (theme : Theme) = [ + "@id", Sql.string (ThemeId.toString theme.Id) + "@data", Sql.jsonb (Utils.serialize ser theme) + ] + /// Save a theme let save (theme : Theme) = backgroundTask { - let! oldTheme = findById theme.Id - let themeIdParam = Sql.string (ThemeId.toString theme.Id) - let! _ = - Sql.existingConnection conn - |> Sql.query - "INSERT INTO theme VALUES (@id, @name, @version) - ON CONFLICT (id) DO UPDATE - SET name = EXCLUDED.name, - version = EXCLUDED.version" - |> Sql.parameters - [ "@id", themeIdParam - "@name", Sql.string theme.Name - "@version", Sql.string theme.Version ] - |> Sql.executeNonQueryAsync - - let toDelete, _ = - Utils.diffLists (oldTheme |> Option.map (fun t -> t.Templates) |> Option.defaultValue []) - theme.Templates (fun t -> t.Name) - let toAddOrUpdate = - theme.Templates - |> List.filter (fun t -> not (toDelete |> List.exists (fun d -> d.Name = t.Name))) - - if not (List.isEmpty toDelete) || not (List.isEmpty toAddOrUpdate) then - let! _ = - Sql.existingConnection conn - |> Sql.executeTransactionAsync [ - if not (List.isEmpty toDelete) then - "DELETE FROM theme_template WHERE theme_id = @themeId AND name = @name", - toDelete |> List.map (fun tmpl -> [ "@themeId", themeIdParam; "@name", Sql.string tmpl.Name ]) - if not (List.isEmpty toAddOrUpdate) then - "INSERT INTO theme_template VALUES (@themeId, @name, @template) - ON CONFLICT (theme_id, name) DO UPDATE - SET template = EXCLUDED.template", - toAddOrUpdate |> List.map (fun tmpl -> [ - "@themeId", themeIdParam - "@name", Sql.string tmpl.Name - "@template", Sql.string tmpl.Text - ]) - ] - () + do! Document.upsert conn Table.Theme themeParams theme } interface IThemeData with @@ -140,14 +70,14 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = /// Get all theme assets (excludes data) let all () = Sql.existingConnection conn - |> Sql.query "SELECT theme_id, path, updated_on FROM theme_asset" + |> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}" |> Sql.executeAsync (Map.toThemeAsset false) /// Delete all assets for the given theme let deleteByTheme themeId = backgroundTask { let! _ = Sql.existingConnection conn - |> Sql.query "DELETE FROM theme_asset WHERE theme_id = @themeId" + |> Sql.query $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @themeId" |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ] |> Sql.executeNonQueryAsync () @@ -157,7 +87,7 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = let findById assetId = let (ThemeAssetId (ThemeId themeId, path)) = assetId Sql.existingConnection conn - |> Sql.query "SELECT * FROM theme_asset WHERE theme_id = @themeId AND path = @path" + |> 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 @@ -165,14 +95,14 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = /// Get theme assets for the given theme (excludes data) let findByTheme themeId = Sql.existingConnection conn - |> Sql.query "SELECT theme_id, path, updated_on FROM theme_asset WHERE theme_id = @themeId" + |> 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) /// Get theme assets for the given theme let findByThemeWithData themeId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM theme_asset WHERE theme_id = @themeId" + |> Sql.query $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId" |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ] |> Sql.executeAsync (Map.toThemeAsset true) @@ -181,8 +111,8 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = let (ThemeAssetId (ThemeId themeId, path)) = asset.Id let! _ = Sql.existingConnection conn - |> Sql.query - "INSERT INTO theme_asset ( + |> Sql.query $" + INSERT INTO {Table.ThemeAsset} ( theme_id, path, updated_on, data ) VALUES ( @themeId, @path, @updatedOn, @data diff --git a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs index 89de2e9..65802b6 100644 --- a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs @@ -9,8 +9,8 @@ open Npgsql.FSharp type PostgresUploadData (conn : NpgsqlConnection) = /// The INSERT statement for an uploaded file - let upInsert = - "INSERT INTO upload ( + let upInsert = $" + INSERT INTO {Table.Upload} ( id, web_log_id, path, updated_on, data ) VALUES ( @id, @webLogId, @path, @updatedOn, @data @@ -37,18 +37,18 @@ type PostgresUploadData (conn : NpgsqlConnection) = /// Delete an uploaded file by its ID let delete uploadId webLogId = backgroundTask { - let theParams = [ "@id", Sql.string (UploadId.toString uploadId); webLogIdParam webLogId ] + let idParam = [ "@id", Sql.string (UploadId.toString uploadId) ] let! path = Sql.existingConnection conn - |> Sql.query "SELECT path FROM upload WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters theParams + |> 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 if Option.isSome path then let! _ = Sql.existingConnection conn - |> Sql.query "DELETE FROM upload WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters theParams + |> Sql.query (docDeleteSql Table.Upload) + |> Sql.parameters idParam |> Sql.executeNonQueryAsync return Ok path.Value else return Error $"""Upload ID {UploadId.toString uploadId} not found""" @@ -57,7 +57,7 @@ type PostgresUploadData (conn : NpgsqlConnection) = /// Find an uploaded file by its path for the given web log let findByPath path webLogId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM upload WHERE web_log_id = @webLogId AND path = @path" + |> 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 @@ -65,14 +65,14 @@ type PostgresUploadData (conn : NpgsqlConnection) = /// Find all uploaded files for the given web log (excludes data) let findByWebLog webLogId = Sql.existingConnection conn - |> Sql.query "SELECT id, web_log_id, path, updated_on FROM upload WHERE web_log_id = @webLogId" + |> 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) /// Find all uploaded files for the given web log let findByWebLogWithData webLogId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM upload WHERE web_log_id = @webLogId" + |> Sql.query $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId" |> Sql.parameters [ webLogIdParam webLogId ] |> Sql.executeAsync (Map.toUpload true) diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs index 59899ac..e3f26b9 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs @@ -11,221 +11,72 @@ type PostgresWebLogData (conn : NpgsqlConnection, ser : JsonSerializer) = // SUPPORT FUNCTIONS - /// The parameters for web log INSERT or web log/RSS options UPDATE statements - let rssParams (webLog : WebLog) = [ - "@isFeedEnabled", Sql.bool webLog.Rss.IsFeedEnabled - "@feedName", Sql.string webLog.Rss.FeedName - "@itemsInFeed", Sql.intOrNone webLog.Rss.ItemsInFeed - "@isCategoryEnabled", Sql.bool webLog.Rss.IsCategoryEnabled - "@isTagEnabled", Sql.bool webLog.Rss.IsTagEnabled - "@copyright", Sql.stringOrNone webLog.Rss.Copyright - ] + /// Map a data row to a web log + let toWebLog = Map.fromDoc ser /// The parameters for web log INSERT or UPDATE statements let webLogParams (webLog : WebLog) = [ - "@id", Sql.string (WebLogId.toString webLog.Id) - "@name", Sql.string webLog.Name - "@slug", Sql.string webLog.Slug - "@subtitle", Sql.stringOrNone webLog.Subtitle - "@defaultPage", Sql.string webLog.DefaultPage - "@postsPerPage", Sql.int webLog.PostsPerPage - "@themeId", Sql.string (ThemeId.toString webLog.ThemeId) - "@urlBase", Sql.string webLog.UrlBase - "@timeZone", Sql.string webLog.TimeZone - "@autoHtmx", Sql.bool webLog.AutoHtmx - "@uploads", Sql.string (UploadDestination.toString webLog.Uploads) - yield! rssParams webLog + "@id", Sql.string (WebLogId.toString webLog.Id) + "@data", Sql.jsonb (Utils.serialize ser webLog) ] - /// Shorthand to map a result to a custom feed - let toCustomFeed = - Map.toCustomFeed ser - - /// Get the current custom feeds for a web log - let getCustomFeeds (webLog : WebLog) = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log_feed WHERE web_log_id = @webLogId" - |> Sql.parameters [ webLogIdParam webLog.Id ] - |> Sql.executeAsync toCustomFeed - - /// Append custom feeds to a web log - let appendCustomFeeds (webLog : WebLog) = backgroundTask { - let! feeds = getCustomFeeds webLog - return { webLog with Rss = { webLog.Rss with CustomFeeds = feeds } } - } - - /// The parameters to save a custom feed - let feedParams webLogId (feed : CustomFeed) = [ - webLogIdParam webLogId - "@id", Sql.string (CustomFeedId.toString feed.Id) - "@source", Sql.string (CustomFeedSource.toString feed.Source) - "@path", Sql.string (Permalink.toString feed.Path) - "@podcast", Sql.jsonbOrNone (feed.Podcast |> Option.map (Utils.serialize ser)) - ] - - /// Update the custom feeds for a web log - let updateCustomFeeds (webLog : WebLog) = backgroundTask { - let! feeds = getCustomFeeds webLog - let toDelete, _ = Utils.diffLists feeds webLog.Rss.CustomFeeds (fun it -> $"{CustomFeedId.toString it.Id}") - let toId (feed : CustomFeed) = feed.Id - let toAddOrUpdate = - webLog.Rss.CustomFeeds |> List.filter (fun f -> not (toDelete |> List.map toId |> List.contains f.Id)) - if not (List.isEmpty toDelete) || not (List.isEmpty toAddOrUpdate) then - let! _ = - Sql.existingConnection conn - |> Sql.executeTransactionAsync [ - if not (List.isEmpty toDelete) then - "DELETE FROM web_log_feed WHERE id = @id", - toDelete |> List.map (fun it -> [ "@id", Sql.string (CustomFeedId.toString it.Id) ]) - if not (List.isEmpty toAddOrUpdate) then - "INSERT INTO web_log_feed ( - id, web_log_id, source, path, podcast - ) VALUES ( - @id, @webLogId, @source, @path, @podcast - ) ON CONFLICT (id) DO UPDATE - SET source = EXCLUDED.source, - path = EXCLUDED.path, - podcast = EXCLUDED.podcast", - toAddOrUpdate |> List.map (feedParams webLog.Id) - ] - () - } - // IMPLEMENTATION FUNCTIONS /// Add a web log let add webLog = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query - "INSERT INTO web_log ( - id, name, slug, subtitle, default_page, posts_per_page, theme_id, url_base, time_zone, auto_htmx, - uploads, is_feed_enabled, feed_name, items_in_feed, is_category_enabled, is_tag_enabled, copyright - ) VALUES ( - @id, @name, @slug, @subtitle, @defaultPage, @postsPerPage, @themeId, @urlBase, @timeZone, @autoHtmx, - @uploads, @isFeedEnabled, @feedName, @itemsInFeed, @isCategoryEnabled, @isTagEnabled, @copyright - )" - |> Sql.parameters (webLogParams webLog) - |> Sql.executeNonQueryAsync - do! updateCustomFeeds webLog + do! Document.insert conn Table.WebLog webLogParams webLog } /// Retrieve all web logs - let all () = backgroundTask { - let! webLogs = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log" - |> Sql.executeAsync Map.toWebLog - let! feeds = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log_feed" - |> Sql.executeAsync (fun row -> WebLogId (row.string "web_log_id"), toCustomFeed row) - return - webLogs - |> List.map (fun it -> - { it with - Rss = - { it.Rss with - CustomFeeds = feeds |> List.filter (fun (wlId, _) -> wlId = it.Id) |> List.map snd } }) - } + let all () = + Sql.existingConnection conn + |> Sql.query $"SELECT * FROM {Table.WebLog}" + |> Sql.executeAsync toWebLog /// Delete a web log by its ID let delete webLogId = backgroundTask { - let subQuery table = $"(SELECT id FROM {table} WHERE web_log_id = @webLogId)" - let postSubQuery = subQuery "post" - let pageSubQuery = subQuery "page" let! _ = Sql.existingConnection conn |> Sql.query $" - DELETE FROM post_comment WHERE post_id IN {postSubQuery}; - DELETE FROM post_revision WHERE post_id IN {postSubQuery}; - DELETE FROM post_category WHERE post_id IN {postSubQuery}; - DELETE FROM post WHERE web_log_id = @webLogId; - DELETE FROM page_revision WHERE page_id IN {pageSubQuery}; - DELETE FROM page WHERE web_log_id = @webLogId; - DELETE FROM category WHERE web_log_id = @webLogId; - DELETE FROM tag_map WHERE web_log_id = @webLogId; - DELETE FROM upload WHERE web_log_id = @webLogId; - DELETE FROM web_log_user WHERE web_log_id = @webLogId; - DELETE FROM web_log_feed WHERE web_log_id = @webLogId; - DELETE FROM web_log WHERE id = @webLogId" + DELETE FROM {Table.PostComment} + WHERE data ->> '{nameof Comment.empty.PostId}' IN (SELECT id FROM {Table.Post} WHERE {webLogWhere}); + DELETE FROM {Table.Post} WHERE {webLogWhere}; + DELETE FROM {Table.Page} WHERE {webLogWhere}; + DELETE FROM {Table.Category} WHERE {webLogWhere}; + DELETE FROM {Table.TagMap} WHERE {webLogWhere}; + DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId; + DELETE FROM {Table.WebLogUser} WHERE {webLogWhere}; + DELETE FROM {Table.WebLog} WHERE id = @webLogId" |> Sql.parameters [ webLogIdParam webLogId ] |> Sql.executeNonQueryAsync () } /// Find a web log by its host (URL base) - let findByHost url = backgroundTask { - let! webLog = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log WHERE url_base = @urlBase" - |> Sql.parameters [ "@urlBase", Sql.string url ] - |> Sql.executeAsync Map.toWebLog - |> tryHead - if Option.isSome webLog then - let! withFeeds = appendCustomFeeds webLog.Value - return Some withFeeds - else return None - } + let findByHost url = + Sql.existingConnection conn + |> Sql.query $"SELECT * FROM {Table.WebLog} WHERE data ->> '{nameof WebLog.empty.UrlBase}' = @urlBase" + |> Sql.parameters [ "@urlBase", Sql.string url ] + |> Sql.executeAsync toWebLog + |> tryHead /// Find a web log by its ID - let findById webLogId = backgroundTask { - let! webLog = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log WHERE id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync Map.toWebLog - |> tryHead - if Option.isSome webLog then - let! withFeeds = appendCustomFeeds webLog.Value - return Some withFeeds - else return None - } + let findById webLogId = + Document.findById conn Table.WebLog webLogId WebLogId.toString toWebLog /// Update settings for a web log let updateSettings webLog = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query - "UPDATE web_log - SET name = @name, - slug = @slug, - subtitle = @subtitle, - default_page = @defaultPage, - posts_per_page = @postsPerPage, - theme_id = @themeId, - url_base = @urlBase, - time_zone = @timeZone, - auto_htmx = @autoHtmx, - uploads = @uploads, - is_feed_enabled = @isFeedEnabled, - feed_name = @feedName, - items_in_feed = @itemsInFeed, - is_category_enabled = @isCategoryEnabled, - is_tag_enabled = @isTagEnabled, - copyright = @copyright - WHERE id = @id" - |> Sql.parameters (webLogParams webLog) - |> Sql.executeNonQueryAsync - () + do! Document.update conn Table.WebLog webLogParams webLog } /// Update RSS options for a web log let updateRssOptions (webLog : WebLog) = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query - "UPDATE web_log - SET is_feed_enabled = @isFeedEnabled, - feed_name = @feedName, - items_in_feed = @itemsInFeed, - is_category_enabled = @isCategoryEnabled, - is_tag_enabled = @isTagEnabled, - copyright = @copyright - WHERE id = @webLogId" - |> Sql.parameters (webLogIdParam webLog.Id :: rssParams webLog) - |> Sql.executeNonQueryAsync - do! updateCustomFeeds webLog + use! txn = conn.BeginTransactionAsync () + match! findById webLog.Id with + | Some blog -> + do! Document.update conn Table.WebLog webLogParams { blog with Rss = webLog.Rss } + do! txn.CommitAsync () + | None -> () } interface IWebLogData with diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index 333f5ec..787c42f 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -2,65 +2,42 @@ namespace MyWebLog.Data.Postgres open MyWebLog open MyWebLog.Data +open Newtonsoft.Json open Npgsql open Npgsql.FSharp /// PostgreSQL myWebLog user data implementation -type PostgresWebLogUserData (conn : NpgsqlConnection) = +type PostgresWebLogUserData (conn : NpgsqlConnection, ser : JsonSerializer) = - /// The INSERT statement for a user - let userInsert = - "INSERT INTO web_log_user ( - id, web_log_id, email, first_name, last_name, preferred_name, password_hash, url, access_level, - created_on, last_seen_on - ) VALUES ( - @id, @webLogId, @email, @firstName, @lastName, @preferredName, @passwordHash, @url, @accessLevel, - @createdOn, @lastSeenOn - )" + /// Map a data row to a user + let toWebLogUser = Map.fromDoc ser /// Parameters for saving web log users let userParams (user : WebLogUser) = [ - "@id", Sql.string (WebLogUserId.toString user.Id) - "@webLogId", Sql.string (WebLogId.toString user.WebLogId) - "@email", Sql.string user.Email - "@firstName", Sql.string user.FirstName - "@lastName", Sql.string user.LastName - "@preferredName", Sql.string user.PreferredName - "@passwordHash", Sql.string user.PasswordHash - "@url", Sql.stringOrNone user.Url - "@accessLevel", Sql.string (AccessLevel.toString user.AccessLevel) - typedParam "createdOn" user.CreatedOn - optParam "lastSeenOn" user.LastSeenOn + "@id", Sql.string (WebLogUserId.toString user.Id) + "@data", Sql.jsonb (Utils.serialize ser user) ] /// Find a user by their ID for the given web log let findById userId webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log_user WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters [ "@id", Sql.string (WebLogUserId.toString userId); webLogIdParam webLogId ] - |> Sql.executeAsync Map.toWebLogUser - |> tryHead + Document.findByIdAndWebLog conn Table.WebLogUser userId WebLogUserId.toString webLogId toWebLogUser /// Delete a user if they have no posts or pages let delete userId webLogId = backgroundTask { match! findById userId webLogId with | Some _ -> - let userParam = [ "@userId", Sql.string (WebLogUserId.toString userId) ] let! isAuthor = Sql.existingConnection conn - |> Sql.query - "SELECT ( EXISTS (SELECT 1 FROM page WHERE author_id = @userId - OR EXISTS (SELECT 1 FROM post WHERE author_id = @userId)) AS does_exist" - |> Sql.parameters userParam + |> Sql.query $" + SELECT ( EXISTS (SELECT 1 FROM {Table.Page} WHERE data ->> '{nameof Page.empty.AuthorId}' = @id + OR EXISTS (SELECT 1 FROM {Table.Post} WHERE data ->> '{nameof Post.empty.AuthorId}' = @id)) + AS {existsName}" + |> Sql.parameters [ "@id", Sql.string (WebLogUserId.toString userId) ] |> Sql.executeRowAsync Map.toExists if isAuthor then return Error "User has pages or posts; cannot delete" else - let! _ = - Sql.existingConnection conn - |> Sql.query "DELETE FROM web_log_user WHERE id = @userId" - |> Sql.parameters userParam - |> Sql.executeNonQueryAsync + do! Document.delete conn Table.WebLogUser (WebLogUserId.toString userId) return Ok true | None -> return Error "User does not exist" } @@ -68,26 +45,24 @@ type PostgresWebLogUserData (conn : NpgsqlConnection) = /// Find a user by their e-mail address for the given web log let findByEmail email webLogId = Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log_user WHERE web_log_id = @webLogId AND email = @email" + |> Sql.query $"{docSelectForWebLogSql Table.WebLogUser} AND data ->> '{nameof WebLogUser.empty.Email}' = @email" |> Sql.parameters [ webLogIdParam webLogId; "@email", Sql.string email ] - |> Sql.executeAsync Map.toWebLogUser + |> Sql.executeAsync toWebLogUser |> tryHead /// Get all users for the given web log let findByWebLog webLogId = - Sql.existingConnection conn - |> Sql.query "SELECT * FROM web_log_user WHERE web_log_id = @webLogId ORDER BY LOWER(preferred_name)" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync Map.toWebLogUser + Document.findByWebLog conn Table.WebLogUser webLogId toWebLogUser + (Some $"ORDER BY LOWER(data ->> '{nameof WebLogUser.empty.PreferredName}')") /// Find the names of users by their IDs for the given web log let findNames webLogId userIds = backgroundTask { let idSql, idParams = inClause "AND id" "id" WebLogUserId.toString userIds let! users = Sql.existingConnection conn - |> Sql.query $"SELECT * FROM web_log_user WHERE web_log_id = @webLogId {idSql}" + |> Sql.query $"{docSelectForWebLogSql Table.WebLogUser} {idSql}" |> Sql.parameters (webLogIdParam webLogId :: idParams) - |> Sql.executeAsync Map.toWebLogUser + |> Sql.executeAsync toWebLogUser return users |> List.map (fun u -> { Name = WebLogUserId.toString u.Id; Value = WebLogUser.displayName u }) @@ -98,42 +73,24 @@ type PostgresWebLogUserData (conn : NpgsqlConnection) = let! _ = Sql.existingConnection conn |> Sql.executeTransactionAsync [ - userInsert, users |> List.map userParams + docInsertSql Table.WebLogUser, users |> List.map userParams ] () } /// Set a user's last seen date/time to now let setLastSeen userId webLogId = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query "UPDATE web_log_user SET last_seen_on = @lastSeenOn WHERE id = @id AND web_log_id = @webLogId" - |> Sql.parameters - [ webLogIdParam webLogId - typedParam "lastSeenOn" (Noda.now ()) - "@id", Sql.string (WebLogUserId.toString userId) ] - |> Sql.executeNonQueryAsync - () + use! txn = conn.BeginTransactionAsync () + match! findById userId webLogId with + | Some user -> + do! Document.update conn Table.WebLogUser userParams { user with LastSeenOn = Some (Noda.now ()) } + do! txn.CommitAsync () + | None -> () } /// Save a user let save user = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query $" - {userInsert} ON CONFLICT (id) DO UPDATE - SET email = @email, - first_name = @firstName, - last_name = @lastName, - preferred_name = @preferredName, - password_hash = @passwordHash, - url = @url, - access_level = @accessLevel, - created_on = @createdOn, - last_seen_on = @lastSeenOn" - |> Sql.parameters (userParams user) - |> Sql.executeNonQueryAsync - () + do! Document.upsert conn Table.WebLogUser userParams user } interface IWebLogUserData with diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index 223efc5..ef41747 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -1,6 +1,7 @@ namespace MyWebLog.Data open Microsoft.Extensions.Logging +open MyWebLog open MyWebLog.Data.Postgres open Newtonsoft.Json open Npgsql @@ -18,183 +19,98 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J |> Sql.query "SELECT tablename FROM pg_tables WHERE schemaname = 'public'" |> Sql.executeAsync (fun row -> row.string "tablename") let needsTable table = not (List.contains table tables) + // Create a document table + let docTable table = $"CREATE TABLE %s{table} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)" let mutable isNew = false let sql = seq { // Theme tables - if needsTable "theme" then + if needsTable Table.Theme then isNew <- true - "CREATE TABLE theme ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - version TEXT NOT NULL)" - if needsTable "theme_template" then - "CREATE TABLE theme_template ( - theme_id TEXT NOT NULL REFERENCES theme (id), - name TEXT NOT NULL, - template TEXT NOT NULL, - PRIMARY KEY (theme_id, name))" - if needsTable "theme_asset" then - "CREATE TABLE theme_asset ( - theme_id TEXT NOT NULL REFERENCES theme (id), + docTable Table.Theme + if needsTable Table.ThemeAsset then + $"CREATE TABLE {Table.ThemeAsset} ( + theme_id TEXT NOT NULL REFERENCES {Table.Theme} (id) ON DELETE CASCADE, path TEXT NOT NULL, updated_on TIMESTAMPTZ NOT NULL, data BYTEA NOT NULL, PRIMARY KEY (theme_id, path))" - // Web log tables - if needsTable "web_log" then - "CREATE TABLE web_log ( - id TEXT NOT NULL PRIMARY KEY, - name TEXT NOT NULL, - slug TEXT NOT NULL, - subtitle TEXT, - default_page TEXT NOT NULL, - posts_per_page INTEGER NOT NULL, - theme_id TEXT NOT NULL REFERENCES theme (id), - url_base TEXT NOT NULL, - time_zone TEXT NOT NULL, - auto_htmx BOOLEAN NOT NULL DEFAULT FALSE, - uploads TEXT NOT NULL, - is_feed_enabled BOOLEAN NOT NULL DEFAULT FALSE, - feed_name TEXT NOT NULL, - items_in_feed INTEGER, - is_category_enabled BOOLEAN NOT NULL DEFAULT FALSE, - is_tag_enabled BOOLEAN NOT NULL DEFAULT FALSE, - copyright TEXT)" - "CREATE INDEX web_log_theme_idx ON web_log (theme_id)" - if needsTable "web_log_feed" then - "CREATE TABLE web_log_feed ( - id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), - source TEXT NOT NULL, - path TEXT NOT NULL, - podcast JSONB)" - "CREATE INDEX web_log_feed_web_log_idx ON web_log_feed (web_log_id)" + // Web log table + if needsTable Table.WebLog then + docTable Table.WebLog + $"CREATE INDEX web_log_theme_idx ON {Table.WebLog} (data ->> '{nameof WebLog.empty.ThemeId}')" // Category table - if needsTable "category" then - "CREATE TABLE category ( - id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), - name TEXT NOT NULL, - slug TEXT NOT NULL, - description TEXT, - parent_id TEXT)" - "CREATE INDEX category_web_log_idx ON category (web_log_id)" + if needsTable Table.Category then + docTable Table.Category + $"CREATE INDEX category_web_log_idx ON {Table.Category} (data ->> '{nameof Category.empty.WebLogId}')" // Web log user table - if needsTable "web_log_user" then - "CREATE TABLE web_log_user ( - id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), - email TEXT NOT NULL, - first_name TEXT NOT NULL, - last_name TEXT NOT NULL, - preferred_name TEXT NOT NULL, - password_hash TEXT NOT NULL, - url TEXT, - access_level TEXT NOT NULL, - created_on TIMESTAMPTZ NOT NULL, - last_seen_on TIMESTAMPTZ)" - "CREATE INDEX web_log_user_web_log_idx ON web_log_user (web_log_id)" - "CREATE INDEX web_log_user_email_idx ON web_log_user (web_log_id, email)" + if needsTable Table.WebLogUser then + docTable Table.WebLogUser + $"CREATE INDEX web_log_user_web_log_idx ON {Table.WebLogUser} + (data ->> '{nameof WebLogUser.empty.WebLogId}')" + $"CREATE INDEX web_log_user_email_idx ON {Table.WebLogUser} + (data ->> '{nameof WebLogUser.empty.WebLogId}', data ->> '{nameof WebLogUser.empty.Email}')" // Page tables - if needsTable "page" then - "CREATE TABLE page ( - id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), - author_id TEXT NOT NULL REFERENCES web_log_user (id), - title TEXT NOT NULL, - permalink TEXT NOT NULL, - prior_permalinks TEXT[] NOT NULL DEFAULT '{}', - published_on TIMESTAMPTZ NOT NULL, - updated_on TIMESTAMPTZ NOT NULL, - is_in_page_list BOOLEAN NOT NULL DEFAULT FALSE, - template TEXT, - page_text TEXT NOT NULL, - meta_items JSONB)" - "CREATE INDEX page_web_log_idx ON page (web_log_id)" - "CREATE INDEX page_author_idx ON page (author_id)" - "CREATE INDEX page_permalink_idx ON page (web_log_id, permalink)" - if needsTable "page_revision" then - "CREATE TABLE page_revision ( - page_id TEXT NOT NULL REFERENCES page (id), + if needsTable Table.Page then + docTable Table.Page + $"CREATE INDEX page_web_log_idx ON {Table.Page} (data ->> '{nameof Page.empty.WebLogId}')" + $"CREATE INDEX page_author_idx ON {Table.Page} (data ->> '{nameof Page.empty.AuthorId}')" + $"CREATE INDEX page_permalink_idx ON {Table.Page} + (data ->> '{nameof Page.empty.WebLogId}', data ->> '{nameof Page.empty.Permalink}')" + if needsTable Table.PageRevision then + $"CREATE TABLE {Table.PageRevision} ( + page_id TEXT NOT NULL REFERENCES {Table.Page} (id) ON DELETE CASCADE, as_of TIMESTAMPTZ NOT NULL, revision_text TEXT NOT NULL, PRIMARY KEY (page_id, as_of))" // Post tables - if needsTable "post" then - "CREATE TABLE post ( - id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), - author_id TEXT NOT NULL REFERENCES web_log_user (id), - status TEXT NOT NULL, - title TEXT NOT NULL, - permalink TEXT NOT NULL, - prior_permalinks TEXT[] NOT NULL DEFAULT '{}', - published_on TIMESTAMPTZ, - updated_on TIMESTAMPTZ NOT NULL, - template TEXT, - post_text TEXT NOT NULL, - tags TEXT[], - meta_items JSONB, - episode JSONB)" - "CREATE INDEX post_web_log_idx ON post (web_log_id)" - "CREATE INDEX post_author_idx ON post (author_id)" - "CREATE INDEX post_status_idx ON post (web_log_id, status, updated_on)" - "CREATE INDEX post_permalink_idx ON post (web_log_id, permalink)" - if needsTable "post_category" then - "CREATE TABLE post_category ( - post_id TEXT NOT NULL REFERENCES post (id), - category_id TEXT NOT NULL REFERENCES category (id), - PRIMARY KEY (post_id, category_id))" - "CREATE INDEX post_category_category_idx ON post_category (category_id)" - if needsTable "post_revision" then - "CREATE TABLE post_revision ( - post_id TEXT NOT NULL REFERENCES post (id), + if needsTable Table.Post then + docTable Table.Post + $"CREATE INDEX post_web_log_idx ON {Table.Post} (data ->> '{nameof Post.empty.WebLogId}')" + $"CREATE INDEX post_author_idx ON {Table.Post} (data ->> '{nameof Post.empty.AuthorId}')" + $"CREATE INDEX post_status_idx ON {Table.Post} + (data ->> '{nameof Post.empty.WebLogId}', data ->> '{nameof Post.empty.Status}', + data ->> '{nameof Post.empty.UpdatedOn}')" + $"CREATE INDEX post_permalink_idx ON {Table.Post} + (data ->> '{nameof Post.empty.WebLogId}', data ->> '{nameof Post.empty.Permalink}')" + $"CREATE INDEX post_category_idx ON {Table.Post} USING GIN + (data ->> '{nameof Post.empty.CategoryIds}')" + $"CREATE INDEX post_tag_idx ON {Table.Post} USING GIN (data ->> '{nameof Post.empty.Tags}')" + if needsTable Table.PostRevision then + $"CREATE TABLE {Table.PostRevision} ( + post_id TEXT NOT NULL REFERENCES {Table.Post} (id) ON DELETE CASCADE, as_of TIMESTAMPTZ NOT NULL, revision_text TEXT NOT NULL, PRIMARY KEY (post_id, as_of))" - if needsTable "post_comment" then - "CREATE TABLE post_comment ( - id TEXT NOT NULL PRIMARY KEY, - post_id TEXT NOT NULL REFERENCES post(id), - in_reply_to_id TEXT, - name TEXT NOT NULL, - email TEXT NOT NULL, - url TEXT, - status TEXT NOT NULL, - posted_on TIMESTAMPTZ NOT NULL, - comment_text TEXT NOT NULL)" - "CREATE INDEX post_comment_post_idx ON post_comment (post_id)" + if needsTable Table.PostComment then + docTable Table.PostComment + $"CREATE INDEX post_comment_post_idx ON {Table.PostComment} (data ->> '{nameof Comment.empty.PostId}')" // Tag map table - if needsTable "tag_map" then - "CREATE TABLE tag_map ( - id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), - tag TEXT NOT NULL, - url_value TEXT NOT NULL)" - "CREATE INDEX tag_map_web_log_idx ON tag_map (web_log_id)" + if needsTable Table.TagMap then + docTable Table.TagMap + $"CREATE INDEX tag_map_web_log_idx ON {Table.TagMap} (data ->> '{nameof TagMap.empty.WebLogId}')" // Uploaded file table - if needsTable "upload" then - "CREATE TABLE upload ( + if needsTable Table.Upload then + $"CREATE TABLE {Table.Upload} ( id TEXT NOT NULL PRIMARY KEY, - web_log_id TEXT NOT NULL REFERENCES web_log (id), + web_log_id TEXT NOT NULL REFERENCES {Table.WebLog} (id), path TEXT NOT NULL, updated_on TIMESTAMPTZ NOT NULL, data BYTEA NOT NULL)" - "CREATE INDEX upload_web_log_idx ON upload (web_log_id)" - "CREATE INDEX upload_path_idx ON upload (web_log_id, path)" + $"CREATE INDEX upload_web_log_idx ON {Table.Upload} (web_log_id)" + $"CREATE INDEX upload_path_idx ON {Table.Upload} (web_log_id, path)" // Database version table - if needsTable "db_version" then - "CREATE TABLE db_version (id TEXT NOT NULL PRIMARY KEY)" - $"INSERT INTO db_version VALUES ('{Utils.currentDbVersion}')" + if needsTable Table.DbVersion then + $"CREATE TABLE {Table.DbVersion} (id TEXT NOT NULL PRIMARY KEY)" + $"INSERT INTO {Table.DbVersion} VALUES ('{Utils.currentDbVersion}')" } Sql.existingConnection conn @@ -233,15 +149,15 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J interface IData with - member _.Category = PostgresCategoryData conn + member _.Category = PostgresCategoryData (conn, ser) member _.Page = PostgresPageData (conn, ser) member _.Post = PostgresPostData (conn, ser) - member _.TagMap = PostgresTagMapData conn - member _.Theme = PostgresThemeData conn + member _.TagMap = PostgresTagMapData (conn, ser) + member _.Theme = PostgresThemeData (conn, ser) member _.ThemeAsset = PostgresThemeAssetData conn member _.Upload = PostgresUploadData conn member _.WebLog = PostgresWebLogData (conn, ser) - member _.WebLogUser = PostgresWebLogUserData conn + member _.WebLogUser = PostgresWebLogUserData (conn, ser) member _.Serializer = ser diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index 475923d..c3968d2 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -5,7 +5,6 @@ open MyWebLog open RethinkDb.Driver /// Functions to assist with retrieving data -[] module private RethinkHelpers = /// Table names @@ -90,6 +89,7 @@ open System open Microsoft.Extensions.Logging open MyWebLog.ViewModels open RethinkDb.Driver.FSharp +open RethinkHelpers /// RethinkDB implementation of data functions for myWebLog type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger) = -- 2.45.1 From 13dbecfe1e0754c84cc363fcf7dfa45d5c9cf979 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 5 Feb 2023 23:20:07 -0500 Subject: [PATCH 02/17] Update to .NET 7 WIP on document utility library --- src/Directory.Build.props | 2 +- src/MyWebLog.Data/MyWebLog.Data.fsproj | 4 ++-- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 4 ++++ src/MyWebLog.Domain/MyWebLog.Domain.fsproj | 1 - src/MyWebLog.sln | 6 ++++++ src/MyWebLog/MyWebLog.fsproj | 1 - src/Npgsql.FSharp.Documents/Library.fs | 10 ++++++++++ .../Npgsql.FSharp.Documents.fsproj | 11 +++++++++++ 8 files changed, 34 insertions(+), 5 deletions(-) create mode 100644 src/Npgsql.FSharp.Documents/Library.fs create mode 100644 src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b9690f2..a53054e 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - net6.0 + net7.0 embedded 2.0.0.0 2.0.0.0 diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index 4f2b61b..eed7751 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -2,6 +2,7 @@ + @@ -17,7 +18,6 @@ - @@ -45,7 +45,7 @@ - + diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 2280095..3d3ba63 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -206,6 +206,10 @@ module Document = /// Convert extra SQL to a for that can be appended to a query let private moreSql sql = sql |> Option.map (fun it -> $" %s{it}") |> Option.defaultValue "" + /// Create a parameter for a @> (contains) query + let contains<'T> (name : string) ser (value : 'T) = + name, Sql.jsonb (Utils.serialize ser value) + /// Count documents for a web log let countByWebLog conn table webLogId extraSql = Sql.existingConnection conn diff --git a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj index 49fa066..ad1ed87 100644 --- a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj +++ b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj @@ -8,7 +8,6 @@ - diff --git a/src/MyWebLog.sln b/src/MyWebLog.sln index a594b6e..f12f40f 100644 --- a/src/MyWebLog.sln +++ b/src/MyWebLog.sln @@ -9,6 +9,8 @@ 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 @@ -27,6 +29,10 @@ 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/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj index 1473d53..646f1b1 100644 --- a/src/MyWebLog/MyWebLog.fsproj +++ b/src/MyWebLog/MyWebLog.fsproj @@ -29,7 +29,6 @@ - diff --git a/src/Npgsql.FSharp.Documents/Library.fs b/src/Npgsql.FSharp.Documents/Library.fs new file mode 100644 index 0000000..cbbbe00 --- /dev/null +++ b/src/Npgsql.FSharp.Documents/Library.fs @@ -0,0 +1,10 @@ +module Npgsql.FSharp.Documents + + +/// Query construction functions +module Query = + + /// Create a parameter for a @> (contains) query + let contains<'T> (name : string) (value : 'T) = + name, Sql.jsonb (string value) // FIXME: need a serializer + diff --git a/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj b/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj new file mode 100644 index 0000000..9f4040e --- /dev/null +++ b/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj @@ -0,0 +1,11 @@ + + + + + + + + + + + -- 2.45.1 From cc6f444b5f82ef7cc35719ec9751c17a00488036 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 6 Feb 2023 23:20:43 -0500 Subject: [PATCH 03/17] Convert from conn to data source - First cut of doc library implementation --- src/MyWebLog.Data/MyWebLog.Data.fsproj | 2 - .../Postgres/PostgresCategoryData.fs | 90 +++---- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 152 ++--------- .../Postgres/PostgresPageData.fs | 106 ++++---- .../Postgres/PostgresPostData.fs | 159 ++++++------ .../Postgres/PostgresTagMapData.fs | 55 ++-- .../Postgres/PostgresThemeData.fs | 51 ++-- .../Postgres/PostgresUploadData.fs | 18 +- .../Postgres/PostgresWebLogData.fs | 67 ++--- .../Postgres/PostgresWebLogUserData.fs | 72 +++--- src/MyWebLog.Data/PostgresData.fs | 65 ++--- src/MyWebLog/Program.fs | 7 +- src/Npgsql.FSharp.Documents/Library.fs | 238 +++++++++++++++++- .../Npgsql.FSharp.Documents.fsproj | 1 + 14 files changed, 612 insertions(+), 471 deletions(-) diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index eed7751..a788fee 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -13,8 +13,6 @@ - - diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index c84ac76..16bc955 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -2,30 +2,36 @@ open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostgreSQL myWebLog category data implementation -type PostgresCategoryData (conn : NpgsqlConnection, ser : JsonSerializer) = - - /// Convert a data row to a category - let toCategory = Map.fromDoc ser +type PostgresCategoryData (source : NpgsqlDataSource) = + /// Shorthand for turning a web log ID into a string + let wls = WebLogId.toString + /// Count all categories for the given web log let countAll webLogId = - Document.countByWebLog conn Table.Category webLogId None + Sql.fromDataSource source + |> Query.countByContains Table.Category {| WebLogId = wls webLogId |} /// Count all top-level categories for the given web log let countTopLevel webLogId = - Document.countByWebLog conn Table.Category webLogId - (Some $"AND data -> '{nameof Category.empty.ParentId}' IS NULL") + Sql.fromDataSource source + |> Query.countByContains Table.Category {| WebLogId = wls webLogId; ParentId = None |} /// Retrieve all categories for the given web log in a DotLiquid-friendly format let findAllForView webLogId = backgroundTask { let! cats = - Document.findByWebLog conn Table.Category webLogId toCategory - (Some $"ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')") + Sql.fromDataSource source + |> Sql.query $""" + {Query.selectFromTable Table.Category} + WHERE {Query.whereDataContains "@criteria"} + ORDER BY LOWER(data->>'{nameof Category.empty.Name}')""" + |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.executeAsync fromData let ordered = Utils.orderByHierarchy cats None None [] let counts = ordered @@ -39,14 +45,16 @@ type PostgresCategoryData (conn : NpgsqlConnection, ser : JsonSerializer) = |> List.ofSeq |> jsonArrayInClause (nameof Post.empty.CategoryIds) id let postCount = - Sql.existingConnection conn - |> Sql.query $" + Sql.fromDataSource source + |> Sql.query $""" SELECT COUNT(DISTINCT id) AS {countName} FROM {Table.Post} - WHERE {webLogWhere} - AND data ->> '{nameof Post.empty.Status}' = '{PostStatus.toString Published}' - AND ({catIdSql})" - |> Sql.parameters (webLogIdParam webLogId :: catIdParams) + WHERE {Query.whereDataContains "@criteria"} + AND ({catIdSql})""" + |> Sql.parameters ( + ("@criteria", + Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |}) + :: catIdParams) |> Sql.executeRowAsync Map.toCount |> Async.AwaitTask |> Async.RunSynchronously @@ -65,76 +73,70 @@ type PostgresCategoryData (conn : NpgsqlConnection, ser : JsonSerializer) = } /// Find a category by its ID for the given web log let findById catId webLogId = - Document.findByIdAndWebLog conn Table.Category catId CategoryId.toString webLogId toCategory + Document.findByIdAndWebLog source Table.Category catId CategoryId.toString webLogId /// Find all categories for the given web log let findByWebLog webLogId = - Document.findByWebLog conn Table.Category webLogId toCategory None + Document.findByWebLog source Table.Category webLogId /// Create parameters for a category insert / update - let catParameters (cat : Category) = [ - "@id", Sql.string (CategoryId.toString cat.Id) - "@data", Sql.jsonb (Utils.serialize ser cat) - ] + let catParameters (cat : Category) = + Query.docParameters (CategoryId.toString cat.Id) cat /// Delete a category let delete catId webLogId = backgroundTask { match! findById catId webLogId with | Some cat -> // Reassign any children to the category's parent category - let parentParam = "@parentId", Sql.string (CategoryId.toString catId) let! children = - Sql.existingConnection conn - |> Sql.query - $"SELECT * FROM {Table.Category} WHERE data ->> '{nameof Category.empty.ParentId}' = @parentId" - |> Sql.parameters [ parentParam ] - |> Sql.executeAsync toCategory + Sql.fromDataSource source + |> Query.findByContains Table.Category {| ParentId = CategoryId.toString catId |} let hasChildren = not (List.isEmpty children) if hasChildren then let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docUpdateSql Table.Category, + Query.updateQuery Table.Category, children |> List.map (fun child -> catParameters { child with ParentId = cat.ParentId }) ] () // Delete the category off all posts where it is assigned let! posts = - Sql.existingConnection conn - |> Sql.query $"SELECT * FROM {Table.Post} WHERE data -> '{nameof Post.empty.CategoryIds}' ? @id" + Sql.fromDataSource source + |> Sql.query $"SELECT data FROM {Table.Post} WHERE data->'{nameof Post.empty.CategoryIds}' ? @id" |> Sql.parameters [ "@id", Sql.jsonb (CategoryId.toString catId) ] - |> Sql.executeAsync (Map.fromDoc ser) + |> Sql.executeAsync fromData if not (List.isEmpty posts) then let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docUpdateSql Table.Post, + Query.updateQuery Table.Post, posts |> List.map (fun post -> [ "@id", Sql.string (PostId.toString post.Id) - "@data", Sql.jsonb (Utils.serialize ser { - post with - CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) - }) + "@data", Query.jsonbDocParam + { post with + CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) + } ]) ] () // Delete the category itself - do! Document.delete conn Table.Category (CategoryId.toString catId) + do! Sql.fromDataSource source |> Query.deleteById Table.Category (CategoryId.toString catId) return if hasChildren then ReassignedChildCategories else CategoryDeleted | None -> return CategoryNotFound } /// Save a category - let save cat = backgroundTask { - do! Document.upsert conn Table.Category catParameters cat + let save (cat : Category) = backgroundTask { + do! Sql.fromDataSource source |> Query.save Table.Category (CategoryId.toString cat.Id) cat } /// Restore categories from a backup let restore cats = backgroundTask { let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docInsertSql Table.Category, cats |> List.map catParameters + Query.insertQuery Table.Category, cats |> List.map catParameters ] () } diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 3d3ba63..5f06f5f 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -66,6 +66,7 @@ open MyWebLog.Data open NodaTime open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// Create a WHERE clause fragment for the web log ID let webLogWhere = "data ->> 'WebLogId' = @webLogId" @@ -74,6 +75,10 @@ let webLogWhere = "data ->> 'WebLogId' = @webLogId" let webLogIdParam webLogId = "@webLogId", Sql.string (WebLogId.toString webLogId) +/// Create a parameter for a web log document-contains query +let webLogContains webLogId = + Query.jsonbDocParam {| WebLogId = WebLogId.toString webLogId |} + /// The name of the field to select to be able to use Map.toCount let countName = "the_count" @@ -127,45 +132,9 @@ let optParam<'T> name (it : 'T option) = let p = NpgsqlParameter ($"@%s{name}", if Option.isSome it then box it.Value else DBNull.Value) p.ParameterName, Sql.parameter p -/// SQL statement to insert into a document table -let docInsertSql table = - $"INSERT INTO %s{table} VALUES (@id, @data)" - -/// SQL statement to select a document by its ID -let docSelectSql table = - $"SELECT * FROM %s{table} WHERE id = @id" - -/// SQL statement to select documents by their web log IDs -let docSelectForWebLogSql table = - $"SELECT * FROM %s{table} WHERE {webLogWhere}" - -/// SQL statement to update a document in a document table -let docUpdateSql table = - $"UPDATE %s{table} SET data = @data WHERE id = @id" - -/// SQL statement to insert or update a document in a document table -let docUpsertSql table = - $"{docInsertSql table} ON CONFLICT (id) DO UPDATE SET data = EXCLUDED.data" - -/// SQL statement to delete a document from a document table by its ID -let docDeleteSql table = - $"DELETE FROM %s{table} WHERE id = @id" - -/// SQL statement to count documents for a web log -let docCountForWebLogSql table = - $"SELECT COUNT(id) AS {countName} FROM %s{table} WHERE {webLogWhere}" - -/// SQL statement to determine if a document exists for a web log -let docExistsForWebLogSql table = - $"SELECT EXISTS (SELECT 1 FROM %s{table} WHERE id = @id AND {webLogWhere}) AS {existsName}" - /// Mapping functions for SQL queries module Map = - /// Map an item by deserializing the document - let fromDoc<'T> ser (row : RowReader) = - Utils.deserialize<'T> ser (row.string "data") - /// Get a count from a row let toCount (row : RowReader) = row.int countName @@ -203,112 +172,43 @@ module Map = /// Document manipulation functions module Document = - /// Convert extra SQL to a for that can be appended to a query - let private moreSql sql = sql |> Option.map (fun it -> $" %s{it}") |> Option.defaultValue "" - - /// Create a parameter for a @> (contains) query - let contains<'T> (name : string) ser (value : 'T) = - name, Sql.jsonb (Utils.serialize ser value) - - /// Count documents for a web log - let countByWebLog conn table webLogId extraSql = - Sql.existingConnection conn - |> Sql.query $"{docCountForWebLogSql table}{moreSql extraSql}" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeRowAsync Map.toCount - - /// Delete a document - let delete conn table idParam = backgroundTask { - let! _ = - Sql.existingConnection conn - |> Sql.query (docDeleteSql table) - |> Sql.parameters [ "@id", Sql.string idParam ] - |> Sql.executeNonQueryAsync - () - } - - /// Determine if a document with the given ID exists - let exists<'TKey> conn table (key : 'TKey) (keyFunc : 'TKey -> string) = - Sql.existingConnection conn - |> Sql.query $"SELECT EXISTS (SELECT 1 FROM %s{table} WHERE id = @id) AS {existsName}" - |> Sql.parameters [ "@id", Sql.string (keyFunc key) ] - |> Sql.executeRowAsync Map.toExists - /// Determine whether a document exists with the given key for the given web log - let existsByWebLog<'TKey> conn table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId = - Sql.existingConnection conn - |> Sql.query (docExistsForWebLogSql table) + let existsByWebLog<'TKey> source table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId = + Sql.fromDataSource source + |> Sql.query $""" + SELECT EXISTS ( + SELECT 1 FROM %s{table} WHERE id = @id AND {Query.whereDataContains "@criteria"} + ) AS {existsName}""" |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogIdParam webLogId ] |> Sql.executeRowAsync Map.toExists - /// Find a document by its ID - let findById<'TKey, 'TDoc> conn table (key : 'TKey) (keyFunc : 'TKey -> string) (docFunc : RowReader -> 'TDoc) = - Sql.existingConnection conn - |> Sql.query (docSelectSql table) - |> Sql.parameters [ "@id", Sql.string (keyFunc key) ] - |> Sql.executeAsync docFunc + /// 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); "@criteria", webLogContains webLogId ] + |> Sql.executeAsync fromData<'TDoc> |> tryHead /// Find a document by its ID for the given web log - let findByIdAndWebLog<'TKey, 'TDoc> conn table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId - (docFunc : RowReader -> 'TDoc) = - Sql.existingConnection conn - |> Sql.query $"{docSelectSql table} AND {webLogWhere}" - |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogIdParam webLogId ] - |> Sql.executeAsync docFunc - |> tryHead - - /// Find all documents for the given web log - let findByWebLog<'TDoc> conn table webLogId (docFunc : RowReader -> 'TDoc) extraSql = - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql table}{moreSql extraSql}" - |> Sql.parameters [ webLogIdParam webLogId ] - |> Sql.executeAsync docFunc - - /// Insert a new document - let insert<'T> conn table (paramFunc : 'T -> (string * SqlValue) list) (doc : 'T) = task { - let! _ = - Sql.existingConnection conn - |> Sql.query (docInsertSql table) - |> Sql.parameters (paramFunc doc) - |> Sql.executeNonQueryAsync - () - } + let findByWebLog<'TDoc> source table webLogId : Task<'TDoc list> = + Sql.fromDataSource source + |> Query.findByContains table {| WebLogId = WebLogId.toString webLogId |} - /// Update an existing document - let update<'T> conn table (paramFunc : 'T -> (string * SqlValue) list) (doc : 'T) = task { - let! _ = - Sql.existingConnection conn - |> Sql.query (docUpdateSql table) - |> Sql.parameters (paramFunc doc) - |> Sql.executeNonQueryAsync - () - } - - /// Insert or update a document - let upsert<'T> conn table (paramFunc : 'T -> (string * SqlValue) list) (doc : 'T) = task { - let! _ = - Sql.existingConnection conn - |> Sql.query (docUpsertSql table) - |> Sql.parameters (paramFunc doc) - |> Sql.executeNonQueryAsync - () - } - /// Functions to support revisions module Revisions = /// Find all revisions for the given entity - let findByEntityId<'TKey> conn revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) = - Sql.existingConnection conn + 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 /// Find all revisions for all posts for the given web log - let findByWebLog<'TKey> conn revTable entityTable (keyFunc : string -> 'TKey) webLogId = - Sql.existingConnection conn + let findByWebLog<'TKey> source revTable entityTable (keyFunc : string -> 'TKey) webLogId = + Sql.fromDataSource source |> Sql.query $" SELECT pr.* FROM %s{revTable} pr @@ -331,11 +231,11 @@ module Revisions = /// Update a page's revisions let update<'TKey> - conn revTable entityTable (key : 'TKey) (keyFunc : 'TKey -> string) oldRevs newRevs = backgroundTask { + source 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.existingConnection conn + Sql.fromDataSource source |> 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 3a82203..29e1bf7 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -2,54 +2,61 @@ namespace MyWebLog.Data.Postgres open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostgreSQL myWebLog page data implementation -type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = +type PostgresPageData (source : NpgsqlDataSource) = // SUPPORT FUNCTIONS + /// Shorthand for turning a web log ID into a string + let wls = WebLogId.toString + /// Append revisions to a page let appendPageRevisions (page : Page) = backgroundTask { - let! revisions = Revisions.findByEntityId conn Table.PageRevision Table.Page page.Id PageId.toString + let! revisions = Revisions.findByEntityId source Table.PageRevision Table.Page page.Id PageId.toString return { page with Revisions = revisions } } - /// Shorthand to map to a page - let toPage = Map.fromDoc ser - /// Return a page with no text or revisions let pageWithoutText row = - { toPage row with Text = "" } + { fromData row with Text = "" } /// Update a page's revisions let updatePageRevisions pageId oldRevs newRevs = - Revisions.update conn Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs + Revisions.update source Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs /// Does the given page exist? let pageExists pageId webLogId = - Document.existsByWebLog conn Table.Page pageId PageId.toString webLogId + Document.existsByWebLog source Table.Page pageId PageId.toString webLogId // IMPLEMENTATION FUNCTIONS /// Get all pages for a web log (without text or revisions) let all webLogId = - Document.findByWebLog conn Table.Page webLogId pageWithoutText - (Some $"ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')") + Sql.fromDataSource source + |> Sql.query $""" + {Query.selectFromTable Table.Page} + WHERE {Query.whereDataContains "@criteria"} + ORDER BY LOWER(data->>'{nameof Page.empty.Title}')""" + |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.executeAsync fromData /// Count all pages for the given web log let countAll webLogId = - Document.countByWebLog conn Table.Page webLogId None + Sql.fromDataSource source + |> Query.countByContains Table.Page {| WebLogId = wls webLogId |} /// Count all pages shown in the page list for the given web log let countListed webLogId = - Document.countByWebLog conn Table.Page webLogId (Some $"AND data -> '{nameof Page.empty.IsInPageList}' = TRUE") + Sql.fromDataSource source + |> Query.countByContains Table.Page {| WebLogId = wls webLogId; IsInPageList = true |} /// Find a page by its ID (without revisions) let findById pageId webLogId = - Document.findByIdAndWebLog conn Table.Page pageId PageId.toString webLogId toPage + Document.findByIdAndWebLog source Table.Page pageId PageId.toString webLogId /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { @@ -64,17 +71,15 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = let delete pageId webLogId = backgroundTask { match! pageExists pageId webLogId with | true -> - do! Document.delete conn Table.Page (PageId.toString pageId) + do! Sql.fromDataSource source |> Query.deleteById Table.Page (PageId.toString pageId) return true | false -> return false } /// Find a page by its permalink for the given web log let findByPermalink permalink webLogId = - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql Table.Page} AND data ->> '{nameof Page.empty.Permalink}' = @link" - |> Sql.parameters [ webLogIdParam webLogId; "@link", Sql.string (Permalink.toString permalink) ] - |> Sql.executeAsync toPage + Sql.fromDataSource source + |> Query.findByContains Table.Page {| WebLogId = wls webLogId; Permalink = Permalink.toString permalink |} |> tryHead /// Find the current permalink within a set of potential prior permalinks for the given web log @@ -84,21 +89,22 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = let linkSql, linkParams = jsonArrayInClause (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks return! - Sql.existingConnection conn - |> Sql.query $" - SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink + // TODO: stopped here + Sql.fromDataSource source + |> Sql.query $""" + SELECT data->>'{nameof Page.empty.Permalink}' AS permalink FROM page - WHERE {webLogWhere} - AND ({linkSql})" - |> Sql.parameters (webLogIdParam webLogId :: linkParams) + WHERE {Query.whereDataContains "@criteria"} + AND ({linkSql})""" + |> Sql.parameters (("@criteria", webLogContains webLogId) :: linkParams) |> Sql.executeAsync Map.toPermalink |> tryHead } /// Get all complete pages for the given web log let findFullByWebLog webLogId = backgroundTask { - let! pages = Document.findByWebLog conn Table.Page webLogId toPage None - let! revisions = Revisions.findByWebLog conn Table.PageRevision Table.Page PageId webLogId + let! pages = Document.findByWebLog source Table.Page webLogId + let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId return pages |> List.map (fun it -> @@ -107,37 +113,40 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Get all listed pages for the given web log (without revisions or text) let findListed webLogId = - Sql.existingConnection conn - |> Sql.query $" - {docSelectForWebLogSql Table.Page} - AND data -> '{nameof Page.empty.IsInPageList}' = TRUE - ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')" - |> Sql.parameters [ webLogIdParam webLogId ] + Sql.fromDataSource source + |> Sql.query $""" + {Query.selectFromTable Table.Page} + WHERE {Query.whereDataContains "@criteria"} + ORDER BY LOWER(data->>'{nameof Page.empty.Title}')""" + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; IsInPageList = true |} ] |> Sql.executeAsync pageWithoutText /// Get a page of pages for the given web log (without revisions) let findPageOfPages webLogId pageNbr = - Sql.existingConnection conn - |> Sql.query $" - {docSelectForWebLogSql Table.Page} - ORDER BY LOWER(data ->> '{nameof Page.empty.Title}') - LIMIT @pageSize OFFSET @toSkip" - |> Sql.parameters [ webLogIdParam webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ] - |> Sql.executeAsync toPage + Sql.fromDataSource source + |> Sql.query $""" + {Query.selectFromTable Table.Page} + WHERE {Query.whereDataContains "@criteria"} + ORDER BY LOWER(data->>'{nameof Page.empty.Title}') + LIMIT @pageSize OFFSET @toSkip""" + |> Sql.parameters + [ "@criteria", webLogContains webLogId + "@pageSize", Sql.int 26 + "@toSkip", Sql.int ((pageNbr - 1) * 25) + ] + |> Sql.executeAsync fromData /// The parameters for saving a page - let pageParams (page : Page) = [ - "@id", Sql.string (PageId.toString page.Id) - "@data", Sql.jsonb (Utils.serialize ser page) - ] + let pageParams (page : Page) = + Query.docParameters (PageId.toString page.Id) page /// Restore pages from a backup let restore (pages : Page list) = backgroundTask { let revisions = pages |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docInsertSql Table.Page, pages |> List.map pageParams + Query.insertQuery Table.Page, pages |> List.map pageParams Revisions.insertSql Table.PageRevision, revisions |> List.map (fun (pageId, rev) -> Revisions.revParams pageId PageId.toString rev) ] @@ -147,7 +156,7 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Save a page let save (page : Page) = backgroundTask { let! oldPage = findFullById page.Id page.WebLogId - do! Document.upsert conn Table.Page pageParams page + do! Sql.fromDataSource source |> Query.save Table.Page (PageId.toString page.Id) page do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions () } @@ -156,7 +165,8 @@ type PostgresPageData (conn : NpgsqlConnection, ser : JsonSerializer) = let updatePriorPermalinks pageId webLogId permalinks = backgroundTask { match! findById pageId webLogId with | Some page -> - do! Document.update conn Table.Page pageParams { page with PriorPermalinks = permalinks } + do! Sql.fromDataSource source + |> Query.update Table.Page (PageId.toString page.Id) { page with PriorPermalinks = permalinks } return true | None -> return false } diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index c001442..f3cca98 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -2,60 +2,63 @@ namespace MyWebLog.Data.Postgres open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open NodaTime open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostgreSQL myWebLog post data implementation -type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = +type PostgresPostData (source : NpgsqlDataSource) = // SUPPORT FUNCTIONS + /// Shorthand for turning a web log ID into a string + let wls = WebLogId.toString + /// Append revisions to a post let appendPostRevisions (post : Post) = backgroundTask { - let! revisions = Revisions.findByEntityId conn Table.PostRevision Table.Post post.Id PostId.toString + let! revisions = Revisions.findByEntityId source Table.PostRevision Table.Post post.Id PostId.toString return { post with Revisions = revisions } } - /// Shorthand for mapping to a post - let toPost = Map.fromDoc ser - /// Return a post with no revisions, prior permalinks, or text let postWithoutText row = - { toPost row with Text = "" } + { fromData row with Text = "" } /// Update a post's revisions let updatePostRevisions postId oldRevs newRevs = - Revisions.update conn Table.PostRevision Table.Post postId PostId.toString oldRevs newRevs + Revisions.update source Table.PostRevision Table.Post postId PostId.toString oldRevs newRevs /// Does the given post exist? let postExists postId webLogId = - Document.existsByWebLog conn Table.Post postId PostId.toString webLogId + Document.existsByWebLog source Table.Post postId PostId.toString webLogId - /// Query to select posts by web log ID and status - let postsByWebLogAndStatus = - $"{docSelectForWebLogSql Table.Post} AND data ->> '{nameof Post.empty.Status}' = @status" + /// Query to select posts by JSON document containment criteria + let postsByCriteria = + $"""{Query.selectFromTable Table.Post} WHERE {Query.whereDataContains "@criteria"}""" // IMPLEMENTATION FUNCTIONS /// Count posts in a status for the given web log let countByStatus status webLogId = - Sql.existingConnection conn - |> Sql.query $"{docCountForWebLogSql Table.Post} AND data ->> '{nameof Post.empty.Status}' = @status" - |> Sql.parameters [ webLogIdParam webLogId; "@status", Sql.string (PostStatus.toString status) ] + Sql.fromDataSource source + |> Sql.query + $"""SELECT COUNT(id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}""" + |> Sql.parameters + [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString status |} ] |> Sql.executeRowAsync Map.toCount /// Find a post by its ID for the given web log (excluding revisions) let findById postId webLogId = - Document.findByIdAndWebLog conn Table.Post postId PostId.toString webLogId toPost + Document.findByIdAndWebLog source 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 = - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql Table.Post} AND data ->> '{nameof Post.empty.Permalink}' = @link" - |> Sql.parameters [ webLogIdParam webLogId; "@link", Sql.string (Permalink.toString permalink) ] - |> Sql.executeAsync toPost + Sql.fromDataSource source + |> Sql.query postsByCriteria + |> Sql.parameters + [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Permalink = Permalink.toString permalink |} ] + |> Sql.executeAsync fromData |> tryHead /// Find a complete post by its ID for the given web log @@ -71,12 +74,13 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let delete postId webLogId = backgroundTask { match! postExists postId webLogId with | true -> + let theId = PostId.toString postId let! _ = - Sql.existingConnection conn - |> Sql.query $" - DELETE FROM {Table.PostComment} WHERE data ->> '{nameof Comment.empty.PostId}' = @id; - DELETE FROM {Table.Post} WHERE id = @id" - |> Sql.parameters [ "@id", Sql.string (PostId.toString postId) ] + 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 return true | false -> return false @@ -89,21 +93,21 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let linkSql, linkParams = jsonArrayInClause (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks return! - Sql.existingConnection conn - |> Sql.query $" - SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink + Sql.fromDataSource source + |> Sql.query $""" + SELECT data->>'{nameof Post.empty.Permalink}' AS permalink FROM {Table.Post} - WHERE {webLogWhere} - AND ({linkSql})" - |> Sql.parameters (webLogIdParam webLogId :: linkParams) + WHERE {Query.whereDataContains "@criteria"} + AND ({linkSql})""" + |> Sql.parameters (("@criteria", webLogContains webLogId) :: linkParams) |> Sql.executeAsync Map.toPermalink |> tryHead } /// Get all complete posts for the given web log let findFullByWebLog webLogId = backgroundTask { - let! posts = Document.findByWebLog conn Table.Post webLogId toPost None - let! revisions = Revisions.findByWebLog conn Table.PostRevision Table.Post PostId webLogId + let! posts = Document.findByWebLog source Table.Post webLogId + let! revisions = Revisions.findByWebLog source Table.PostRevision Table.Post PostId webLogId return posts |> List.map (fun it -> @@ -113,92 +117,88 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Get a page of categorized posts for the given web log (excludes revisions) let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = let catSql, catParams = jsonArrayInClause (nameof Post.empty.CategoryIds) CategoryId.toString categoryIds - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - {postsByWebLogAndStatus} + {postsByCriteria} AND ({catSql}) ORDER BY published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - |> Sql.parameters - [ webLogIdParam webLogId - "@status", Sql.string (PostStatus.toString Published) - yield! catParams ] - |> Sql.executeAsync toPost + |> Sql.parameters ( + ("@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |}) + :: catParams) + |> Sql.executeAsync fromData /// Get a page of posts for the given web log (excludes text and revisions) let findPageOfPosts webLogId pageNbr postsPerPage = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - {docSelectForWebLogSql Table.Post} - ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC NULLS FIRST, - data ->> '{nameof Post.empty.UpdatedOn}' + {postsByCriteria} + ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC NULLS FIRST, + data->>'{nameof Post.empty.UpdatedOn}' LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - |> Sql.parameters [ webLogIdParam webLogId ] + |> Sql.parameters [ "@criteria", webLogContains webLogId ] |> Sql.executeAsync postWithoutText /// Get a page of published posts for the given web log (excludes revisions) let findPageOfPublishedPosts webLogId pageNbr postsPerPage = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - {postsByWebLogAndStatus} - ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC + {postsByCriteria} + ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - |> Sql.parameters [ webLogIdParam webLogId; "@status", Sql.string (PostStatus.toString Published) ] - |> Sql.executeAsync toPost + |> Sql.parameters + [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |} ] + |> Sql.executeAsync fromData /// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks) let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - {postsByWebLogAndStatus} - AND data -> '{nameof Post.empty.Tags}' ? @tag - ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC + {postsByCriteria} + AND data->'{nameof Post.empty.Tags}' ? @tag + ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters - [ webLogIdParam webLogId - "@status", Sql.string (PostStatus.toString Published) - "@tag", Sql.jsonb tag + [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |} + "@tag", Sql.jsonb tag ] - |> Sql.executeAsync toPost + |> Sql.executeAsync fromData /// Find the next newest and oldest post from a publish date for the given web log let findSurroundingPosts webLogId (publishedOn : Instant) = backgroundTask { let queryParams () = Sql.parameters [ - webLogIdParam webLogId + "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |} typedParam "publishedOn" publishedOn - "@status", Sql.string (PostStatus.toString Published) ] let! older = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - {postsByWebLogAndStatus} - AND data ->> '{nameof Post.empty.PublishedOn}' < @publishedOn - ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC + {postsByCriteria} + AND data->>'{nameof Post.empty.PublishedOn}' < @publishedOn + ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC LIMIT 1" |> queryParams () - |> Sql.executeAsync toPost + |> Sql.executeAsync fromData let! newer = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - {postsByWebLogAndStatus} - AND data ->> '{nameof Post.empty.PublishedOn}' > @publishedOn - ORDER BY data ->> '{nameof Post.empty.PublishedOn}' + {postsByCriteria} + AND data->>'{nameof Post.empty.PublishedOn}' > @publishedOn + ORDER BY data->>'{nameof Post.empty.PublishedOn}' LIMIT 1" |> queryParams () - |> Sql.executeAsync toPost + |> Sql.executeAsync fromData return List.tryHead older, List.tryHead newer } /// The parameters for saving a post - let postParams (post : Post) = [ - "@id", Sql.string (PostId.toString post.Id) - "@data", Sql.jsonb (Utils.serialize ser post) - ] + let postParams (post : Post) = + Query.docParameters (PostId.toString post.Id) post /// Save a post let save (post : Post) = backgroundTask { let! oldPost = findFullById post.Id post.WebLogId - do! Document.upsert conn Table.Post postParams post + do! Sql.fromDataSource source |> Query.save Table.Post (PostId.toString post.Id) post do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions } @@ -206,9 +206,9 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = let restore posts = backgroundTask { let revisions = posts |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docInsertSql Table.Post, posts |> List.map postParams + Query.insertQuery Table.Post, posts |> List.map postParams Revisions.insertSql Table.PostRevision, revisions |> List.map (fun (postId, rev) -> Revisions.revParams postId PostId.toString rev) ] @@ -217,11 +217,10 @@ type PostgresPostData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Update prior permalinks for a post let updatePriorPermalinks postId webLogId permalinks = backgroundTask { - use! txn = conn.BeginTransactionAsync () match! findById postId webLogId with | Some post -> - do! Document.update conn Table.Post postParams { post with PriorPermalinks = permalinks } - do! txn.CommitAsync () + do! Sql.fromDataSource source + |> Query.update Table.Post (PostId.toString post.Id) { post with PriorPermalinks = permalinks } return true | None -> return false } diff --git a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs index a04c7fb..a576924 100644 --- a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs @@ -2,66 +2,71 @@ namespace MyWebLog.Data.Postgres open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostgreSQL myWebLog tag mapping data implementation -type PostgresTagMapData (conn : NpgsqlConnection, ser : JsonSerializer) = +type PostgresTagMapData (source : NpgsqlDataSource) = - /// Map a data row to a tag mapping - let toTagMap = Map.fromDoc ser + /// Shorthand for turning a web log ID into a string + let wls = WebLogId.toString + + /// A query to select tag map(s) by JSON document containment criteria + let tagMapByCriteria = + $"""{Query.selectFromTable Table.TagMap} WHERE {Query.whereDataContains "@criteria"}""" /// Find a tag mapping by its ID for the given web log let findById tagMapId webLogId = - Document.findByIdAndWebLog conn Table.TagMap tagMapId TagMapId.toString webLogId toTagMap + Document.findByIdAndWebLog source Table.TagMap tagMapId TagMapId.toString webLogId /// Delete a tag mapping for the given web log let delete tagMapId webLogId = backgroundTask { - let! exists = Document.existsByWebLog conn Table.TagMap tagMapId TagMapId.toString webLogId + let! exists = Document.existsByWebLog source Table.TagMap tagMapId TagMapId.toString webLogId if exists then - do! Document.delete conn Table.TagMap (TagMapId.toString tagMapId) + do! Sql.fromDataSource source |> Query.deleteById Table.TagMap (TagMapId.toString tagMapId) return true else return false } /// Find a tag mapping by its URL value for the given web log - let findByUrlValue urlValue webLogId = - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql Table.TagMap} AND data ->> '{nameof TagMap.empty.UrlValue}' = @urlValue" - |> Sql.parameters [ webLogIdParam webLogId; "@urlValue", Sql.string urlValue ] - |> Sql.executeAsync toTagMap + let findByUrlValue (urlValue : string) webLogId = + Sql.fromDataSource source + |> Sql.query tagMapByCriteria + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; UrlValue = urlValue |} ] + |> Sql.executeAsync fromData |> tryHead /// Get all tag mappings for the given web log let findByWebLog webLogId = - Document.findByWebLog conn Table.TagMap webLogId toTagMap (Some "ORDER BY tag") + Sql.fromDataSource source + |> Sql.query $"{tagMapByCriteria} ORDER BY data->>'tag'" + |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.executeAsync fromData /// Find any tag mappings in a list of tags for the given web log let findMappingForTags tags webLogId = let tagSql, tagParams = jsonArrayInClause (nameof TagMap.empty.Tag) id tags - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql Table.TagMap} AND ({tagSql})" - |> Sql.parameters (webLogIdParam webLogId :: tagParams) - |> Sql.executeAsync toTagMap + Sql.fromDataSource source + |> Sql.query $"{tagMapByCriteria} AND ({tagSql})" + |> Sql.parameters (("@criteria", webLogContains webLogId) :: tagParams) + |> Sql.executeAsync fromData /// The parameters for saving a tag mapping - let tagMapParams (tagMap : TagMap) = [ - "@id", Sql.string (TagMapId.toString tagMap.Id) - "@data", Sql.jsonb (Utils.serialize ser tagMap) - ] + let tagMapParams (tagMap : TagMap) = + Query.docParameters (TagMapId.toString tagMap.Id) tagMap /// Save a tag mapping - let save tagMap = backgroundTask { - do! Document.upsert conn Table.TagMap tagMapParams tagMap + let save (tagMap : TagMap) = backgroundTask { + do! Sql.fromDataSource source |> Query.save Table.TagMap (TagMapId.toString tagMap.Id) tagMap } /// Restore tag mappings from a backup let restore tagMaps = backgroundTask { let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docInsertSql Table.TagMap, tagMaps |> List.map tagMapParams + Query.insertQuery Table.TagMap, tagMaps |> List.map tagMapParams ] () } diff --git a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs index 2e3bacf..1c56fca 100644 --- a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs @@ -5,55 +5,52 @@ open MyWebLog.Data open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostreSQL myWebLog theme data implementation -type PostgresThemeData (conn : NpgsqlConnection, ser : JsonSerializer) = - - /// Map a data row to a theme - let toTheme = Map.fromDoc ser +type PostgresThemeData (source : NpgsqlDataSource) = /// Clear out the template text from a theme let withoutTemplateText row = - let theme = toTheme row + let theme = fromData row { theme with Templates = theme.Templates |> List.map (fun template -> { template with Text = "" }) } /// Retrieve all themes (except 'admin'; excludes template text) let all () = - Sql.existingConnection conn - |> Sql.query $"SELECT * FROM {Table.Theme} WHERE id <> 'admin' ORDER BY id" + Sql.fromDataSource source + |> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id <> 'admin' ORDER BY id" |> Sql.executeAsync withoutTemplateText /// Does a given theme exist? let exists themeId = - Document.exists conn Table.Theme themeId ThemeId.toString + Sql.fromDataSource source + |> Query.existsById Table.Theme (ThemeId.toString themeId) /// Find a theme by its ID let findById themeId = - Document.findById conn Table.Theme themeId ThemeId.toString toTheme + Sql.fromDataSource source + |> Query.tryById Table.Theme (ThemeId.toString themeId) /// Find a theme by its ID (excludes the text of templates) let findByIdWithoutText themeId = - Document.findById conn Table.Theme themeId ThemeId.toString withoutTemplateText + Sql.fromDataSource source + |> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id = @id" + |> Sql.parameters [ "@id", Sql.string (ThemeId.toString themeId) ] + |> Sql.executeAsync withoutTemplateText + |> tryHead /// Delete a theme by its ID let delete themeId = backgroundTask { match! exists themeId with | true -> - do! Document.delete conn Table.Theme (ThemeId.toString themeId) + do! Sql.fromDataSource source |> Query.deleteById Table.Theme (ThemeId.toString themeId) return true | false -> return false } - /// Create theme save parameters - let themeParams (theme : Theme) = [ - "@id", Sql.string (ThemeId.toString theme.Id) - "@data", Sql.jsonb (Utils.serialize ser theme) - ] - /// Save a theme - let save (theme : Theme) = backgroundTask { - do! Document.upsert conn Table.Theme themeParams theme - } + let save (theme : Theme) = + Sql.fromDataSource source |> Query.save Table.Theme (ThemeId.toString theme.Id) theme interface IThemeData with member _.All () = all () @@ -65,18 +62,18 @@ type PostgresThemeData (conn : NpgsqlConnection, ser : JsonSerializer) = /// PostreSQL myWebLog theme data implementation -type PostgresThemeAssetData (conn : NpgsqlConnection) = +type PostgresThemeAssetData (source : NpgsqlDataSource) = /// Get all theme assets (excludes data) let all () = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}" |> Sql.executeAsync (Map.toThemeAsset false) /// Delete all assets for the given theme let deleteByTheme themeId = backgroundTask { let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @themeId" |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ] |> Sql.executeNonQueryAsync @@ -86,7 +83,7 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = /// Find a theme asset by its ID let findById assetId = let (ThemeAssetId (ThemeId themeId, path)) = assetId - Sql.existingConnection conn + 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) @@ -94,14 +91,14 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = /// Get theme assets for the given theme (excludes data) let findByTheme themeId = - Sql.existingConnection conn + 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) /// Get theme assets for the given theme let findByThemeWithData themeId = - Sql.existingConnection conn + 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) @@ -110,7 +107,7 @@ type PostgresThemeAssetData (conn : NpgsqlConnection) = let save (asset : ThemeAsset) = backgroundTask { let (ThemeAssetId (ThemeId themeId, path)) = asset.Id let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" INSERT INTO {Table.ThemeAsset} ( theme_id, path, updated_on, data diff --git a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs index 65802b6..08f2f2f 100644 --- a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs @@ -6,7 +6,7 @@ open Npgsql open Npgsql.FSharp /// PostgreSQL myWebLog uploaded file data implementation -type PostgresUploadData (conn : NpgsqlConnection) = +type PostgresUploadData (source : NpgsqlDataSource) = /// The INSERT statement for an uploaded file let upInsert = $" @@ -28,7 +28,7 @@ type PostgresUploadData (conn : NpgsqlConnection) = /// Save an uploaded file let add upload = backgroundTask { let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query upInsert |> Sql.parameters (upParams upload) |> Sql.executeNonQueryAsync @@ -39,15 +39,15 @@ type PostgresUploadData (conn : NpgsqlConnection) = let delete uploadId webLogId = backgroundTask { let idParam = [ "@id", Sql.string (UploadId.toString uploadId) ] let! path = - Sql.existingConnection conn + 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 if Option.isSome path then let! _ = - Sql.existingConnection conn - |> Sql.query (docDeleteSql Table.Upload) + Sql.fromDataSource source + |> Sql.query $"DELETE FROM {Table.Upload} WHERE id = @id" |> Sql.parameters idParam |> Sql.executeNonQueryAsync return Ok path.Value @@ -56,7 +56,7 @@ type PostgresUploadData (conn : NpgsqlConnection) = /// Find an uploaded file by its path for the given web log let findByPath path webLogId = - Sql.existingConnection conn + 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) @@ -64,14 +64,14 @@ type PostgresUploadData (conn : NpgsqlConnection) = /// Find all uploaded files for the given web log (excludes data) let findByWebLog webLogId = - Sql.existingConnection conn + 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) /// Find all uploaded files for the given web log let findByWebLogWithData webLogId = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId" |> Sql.parameters [ webLogIdParam webLogId ] |> Sql.executeAsync (Map.toUpload true) @@ -80,7 +80,7 @@ type PostgresUploadData (conn : NpgsqlConnection) = let restore uploads = backgroundTask { for batch in uploads |> List.chunkBySize 5 do let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ upInsert, batch |> List.map upParams ] diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs index e3f26b9..25a14ed 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs @@ -2,80 +2,65 @@ open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostgreSQL myWebLog web log data implementation -type PostgresWebLogData (conn : NpgsqlConnection, ser : JsonSerializer) = - - // SUPPORT FUNCTIONS - - /// Map a data row to a web log - let toWebLog = Map.fromDoc ser - - /// The parameters for web log INSERT or UPDATE statements - let webLogParams (webLog : WebLog) = [ - "@id", Sql.string (WebLogId.toString webLog.Id) - "@data", Sql.jsonb (Utils.serialize ser webLog) - ] - - // IMPLEMENTATION FUNCTIONS +type PostgresWebLogData (source : NpgsqlDataSource) = /// Add a web log - let add webLog = backgroundTask { - do! Document.insert conn Table.WebLog webLogParams webLog - } + let add (webLog : WebLog) = + Sql.fromDataSource source |> Query.insert Table.WebLog (WebLogId.toString webLog.Id) webLog /// Retrieve all web logs let all () = - Sql.existingConnection conn - |> Sql.query $"SELECT * FROM {Table.WebLog}" - |> Sql.executeAsync toWebLog + Sql.fromDataSource source + |> Query.all Table.WebLog /// Delete a web log by its ID let delete webLogId = backgroundTask { + let criteria = Query.whereDataContains "@criteria" let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" DELETE FROM {Table.PostComment} - WHERE data ->> '{nameof Comment.empty.PostId}' IN (SELECT id FROM {Table.Post} WHERE {webLogWhere}); - DELETE FROM {Table.Post} WHERE {webLogWhere}; - DELETE FROM {Table.Page} WHERE {webLogWhere}; - DELETE FROM {Table.Category} WHERE {webLogWhere}; - DELETE FROM {Table.TagMap} WHERE {webLogWhere}; + WHERE data->>'{nameof Comment.empty.PostId}' IN (SELECT id FROM {Table.Post} WHERE {criteria}); + DELETE FROM {Table.Post} WHERE {criteria}; + DELETE FROM {Table.Page} WHERE {criteria}; + DELETE FROM {Table.Category} WHERE {criteria}; + DELETE FROM {Table.TagMap} WHERE {criteria}; DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId; - DELETE FROM {Table.WebLogUser} WHERE {webLogWhere}; + DELETE FROM {Table.WebLogUser} WHERE {criteria}; DELETE FROM {Table.WebLog} WHERE id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId ] + |> Sql.parameters [ webLogIdParam webLogId; "@criteria", webLogContains webLogId ] |> Sql.executeNonQueryAsync () } /// Find a web log by its host (URL base) - let findByHost url = - Sql.existingConnection conn - |> Sql.query $"SELECT * FROM {Table.WebLog} WHERE data ->> '{nameof WebLog.empty.UrlBase}' = @urlBase" - |> Sql.parameters [ "@urlBase", Sql.string url ] - |> Sql.executeAsync toWebLog + let findByHost (url : string) = + Sql.fromDataSource source + |> Sql.query $"""{Query.selectFromTable Table.WebLog} WHERE {Query.whereDataContains "@criteria"}""" + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| UrlBase = url |} ] + |> Sql.executeAsync fromData |> tryHead /// Find a web log by its ID let findById webLogId = - Document.findById conn Table.WebLog webLogId WebLogId.toString toWebLog + Sql.fromDataSource source + |> Query.tryById Table.WebLog (WebLogId.toString webLogId) /// Update settings for a web log - let updateSettings webLog = backgroundTask { - do! Document.update conn Table.WebLog webLogParams webLog - } + let updateSettings (webLog : WebLog) = + Sql.fromDataSource source |> Query.update Table.WebLog (WebLogId.toString webLog.Id) webLog /// Update RSS options for a web log let updateRssOptions (webLog : WebLog) = backgroundTask { - use! txn = conn.BeginTransactionAsync () match! findById webLog.Id with | Some blog -> - do! Document.update conn Table.WebLog webLogParams { blog with Rss = webLog.Rss } - do! txn.CommitAsync () + do! Sql.fromDataSource source + |> Query.update Table.WebLog (WebLogId.toString webLog.Id) { blog with Rss = webLog.Rss } | None -> () } diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index 787c42f..68a29d9 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -2,67 +2,74 @@ namespace MyWebLog.Data.Postgres open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// PostgreSQL myWebLog user data implementation -type PostgresWebLogUserData (conn : NpgsqlConnection, ser : JsonSerializer) = +type PostgresWebLogUserData (source : NpgsqlDataSource) = - /// Map a data row to a user - let toWebLogUser = Map.fromDoc ser + /// Shorthand for making a web log ID into a string + let wls = WebLogId.toString + + /// Query to get users by JSON document containment criteria + let userByCriteria = + $"""{Query.selectFromTable Table.WebLogUser} WHERE {Query.whereDataContains "@criteria"}""" /// Parameters for saving web log users - let userParams (user : WebLogUser) = [ - "@id", Sql.string (WebLogUserId.toString user.Id) - "@data", Sql.jsonb (Utils.serialize ser user) - ] + let userParams (user : WebLogUser) = + Query.docParameters (WebLogUserId.toString user.Id) user /// Find a user by their ID for the given web log let findById userId webLogId = - Document.findByIdAndWebLog conn Table.WebLogUser userId WebLogUserId.toString webLogId toWebLogUser + Document.findByIdAndWebLog + source Table.WebLogUser userId WebLogUserId.toString webLogId /// Delete a user if they have no posts or pages let delete userId webLogId = backgroundTask { match! findById userId webLogId with | Some _ -> + let criteria = Query.whereDataContains "@criteria" + let usrId = WebLogUserId.toString userId let! isAuthor = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $" - SELECT ( EXISTS (SELECT 1 FROM {Table.Page} WHERE data ->> '{nameof Page.empty.AuthorId}' = @id - OR EXISTS (SELECT 1 FROM {Table.Post} WHERE data ->> '{nameof Post.empty.AuthorId}' = @id)) + SELECT ( EXISTS (SELECT 1 FROM {Table.Page} WHERE {criteria} + OR EXISTS (SELECT 1 FROM {Table.Post} WHERE {criteria})) AS {existsName}" - |> Sql.parameters [ "@id", Sql.string (WebLogUserId.toString userId) ] + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| AuthorId = usrId |} ] |> Sql.executeRowAsync Map.toExists if isAuthor then return Error "User has pages or posts; cannot delete" else - do! Document.delete conn Table.WebLogUser (WebLogUserId.toString userId) + do! Sql.fromDataSource source |> Query.deleteById Table.WebLogUser usrId return Ok true | None -> return Error "User does not exist" } /// Find a user by their e-mail address for the given web log - let findByEmail email webLogId = - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql Table.WebLogUser} AND data ->> '{nameof WebLogUser.empty.Email}' = @email" - |> Sql.parameters [ webLogIdParam webLogId; "@email", Sql.string email ] - |> Sql.executeAsync toWebLogUser + let findByEmail (email : string) webLogId = + Sql.fromDataSource source + |> Sql.query userByCriteria + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Email = email |} ] + |> Sql.executeAsync fromData |> tryHead /// Get all users for the given web log let findByWebLog webLogId = - Document.findByWebLog conn Table.WebLogUser webLogId toWebLogUser - (Some $"ORDER BY LOWER(data ->> '{nameof WebLogUser.empty.PreferredName}')") + Sql.fromDataSource source + |> Sql.query $"{userByCriteria} ORDER BY LOWER(data->>'{nameof WebLogUser.empty.PreferredName}')" + |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.executeAsync fromData /// Find the names of users by their IDs for the given web log let findNames webLogId userIds = backgroundTask { let idSql, idParams = inClause "AND id" "id" WebLogUserId.toString userIds let! users = - Sql.existingConnection conn - |> Sql.query $"{docSelectForWebLogSql Table.WebLogUser} {idSql}" - |> Sql.parameters (webLogIdParam webLogId :: idParams) - |> Sql.executeAsync toWebLogUser + Sql.fromDataSource source + |> Sql.query $"{userByCriteria} {idSql}" + |> Sql.parameters (("@criteria", webLogContains webLogId) :: idParams) + |> Sql.executeAsync fromData return users |> List.map (fun u -> { Name = WebLogUserId.toString u.Id; Value = WebLogUser.displayName u }) @@ -71,27 +78,26 @@ type PostgresWebLogUserData (conn : NpgsqlConnection, ser : JsonSerializer) = /// Restore users from a backup let restore users = backgroundTask { let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync [ - docInsertSql Table.WebLogUser, users |> List.map userParams + Query.insertQuery Table.WebLogUser, users |> List.map userParams ] () } /// Set a user's last seen date/time to now let setLastSeen userId webLogId = backgroundTask { - use! txn = conn.BeginTransactionAsync () match! findById userId webLogId with | Some user -> - do! Document.update conn Table.WebLogUser userParams { user with LastSeenOn = Some (Noda.now ()) } - do! txn.CommitAsync () + do! Sql.fromDataSource source + |> Query.update Table.WebLogUser (WebLogUserId.toString userId) + { user with LastSeenOn = Some (Noda.now ()) } | None -> () } /// Save a user - let save user = backgroundTask { - do! Document.upsert conn Table.WebLogUser userParams user - } + let save (user : WebLogUser) = + Sql.fromDataSource source |> Query.save Table.WebLogUser (WebLogUserId.toString user.Id) user interface IWebLogUserData with member _.Add user = save user diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index ef41747..932a80a 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -6,28 +6,34 @@ open MyWebLog.Data.Postgres open Newtonsoft.Json open Npgsql open Npgsql.FSharp +open Npgsql.FSharp.Documents /// Data implementation for PostgreSQL -type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : JsonSerializer) = +type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : JsonSerializer) = /// Create any needed tables let ensureTables () = backgroundTask { - let _ = NpgsqlConnection.GlobalTypeMapper.UseNodaTime () + // Set up the PostgreSQL document store + Configuration.useDataSource source + Configuration.useSerializer + { new IDocumentSerializer with + member _.Serialize<'T> (it : 'T) : string = Utils.serialize ser it + member _.Deserialize<'T> (it : string) : 'T = Utils.deserialize ser it + } let! tables = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query "SELECT tablename FROM pg_tables WHERE schemaname = 'public'" |> Sql.executeAsync (fun row -> row.string "tablename") let needsTable table = not (List.contains table tables) // Create a document table - let docTable table = $"CREATE TABLE %s{table} (id TEXT NOT NULL PRIMARY KEY, data JSONB NOT NULL)" let mutable isNew = false let sql = seq { // Theme tables if needsTable Table.Theme then isNew <- true - docTable Table.Theme + Definition.createTable Table.Theme if needsTable Table.ThemeAsset then $"CREATE TABLE {Table.ThemeAsset} ( theme_id TEXT NOT NULL REFERENCES {Table.Theme} (id) ON DELETE CASCADE, @@ -38,25 +44,22 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J // Web log table if needsTable Table.WebLog then - docTable Table.WebLog - $"CREATE INDEX web_log_theme_idx ON {Table.WebLog} (data ->> '{nameof WebLog.empty.ThemeId}')" + Definition.createTable Table.WebLog + Definition.createIndex Table.WebLog Optimized // Category table if needsTable Table.Category then - docTable Table.Category - $"CREATE INDEX category_web_log_idx ON {Table.Category} (data ->> '{nameof Category.empty.WebLogId}')" + Definition.createTable Table.Category + Definition.createIndex Table.Category Optimized // Web log user table if needsTable Table.WebLogUser then - docTable Table.WebLogUser - $"CREATE INDEX web_log_user_web_log_idx ON {Table.WebLogUser} - (data ->> '{nameof WebLogUser.empty.WebLogId}')" - $"CREATE INDEX web_log_user_email_idx ON {Table.WebLogUser} - (data ->> '{nameof WebLogUser.empty.WebLogId}', data ->> '{nameof WebLogUser.empty.Email}')" + Definition.createTable Table.WebLogUser + Definition.createIndex Table.WebLogUser Optimized // Page tables if needsTable Table.Page then - docTable Table.Page + Definition.createTable Table.Page $"CREATE INDEX page_web_log_idx ON {Table.Page} (data ->> '{nameof Page.empty.WebLogId}')" $"CREATE INDEX page_author_idx ON {Table.Page} (data ->> '{nameof Page.empty.AuthorId}')" $"CREATE INDEX page_permalink_idx ON {Table.Page} @@ -70,7 +73,7 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J // Post tables if needsTable Table.Post then - docTable Table.Post + Definition.createTable Table.Post $"CREATE INDEX post_web_log_idx ON {Table.Post} (data ->> '{nameof Post.empty.WebLogId}')" $"CREATE INDEX post_author_idx ON {Table.Post} (data ->> '{nameof Post.empty.AuthorId}')" $"CREATE INDEX post_status_idx ON {Table.Post} @@ -88,13 +91,13 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J revision_text TEXT NOT NULL, PRIMARY KEY (post_id, as_of))" if needsTable Table.PostComment then - docTable Table.PostComment + Definition.createTable Table.PostComment $"CREATE INDEX post_comment_post_idx ON {Table.PostComment} (data ->> '{nameof Comment.empty.PostId}')" // Tag map table if needsTable Table.TagMap then - docTable Table.TagMap - $"CREATE INDEX tag_map_web_log_idx ON {Table.TagMap} (data ->> '{nameof TagMap.empty.WebLogId}')" + Definition.createTable Table.TagMap + Definition.createIndex Table.TagMap Optimized // Uploaded file table if needsTable Table.Upload then @@ -113,7 +116,7 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J $"INSERT INTO {Table.DbVersion} VALUES ('{Utils.currentDbVersion}')" } - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.executeTransactionAsync (sql |> Seq.map (fun s -> @@ -130,7 +133,7 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J /// Set a specific database version let setDbVersion version = backgroundTask { let! _ = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query $"DELETE FROM db_version; INSERT INTO db_version VALUES ('%s{version}')" |> Sql.executeNonQueryAsync () @@ -149,15 +152,15 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J interface IData with - member _.Category = PostgresCategoryData (conn, ser) - member _.Page = PostgresPageData (conn, ser) - member _.Post = PostgresPostData (conn, ser) - member _.TagMap = PostgresTagMapData (conn, ser) - member _.Theme = PostgresThemeData (conn, ser) - member _.ThemeAsset = PostgresThemeAssetData conn - member _.Upload = PostgresUploadData conn - member _.WebLog = PostgresWebLogData (conn, ser) - member _.WebLogUser = PostgresWebLogUserData (conn, ser) + member _.Category = PostgresCategoryData source + member _.Page = PostgresPageData source + member _.Post = PostgresPostData source + member _.TagMap = PostgresTagMapData source + member _.Theme = PostgresThemeData source + member _.ThemeAsset = PostgresThemeAssetData source + member _.Upload = PostgresUploadData source + member _.WebLog = PostgresWebLogData source + member _.WebLogUser = PostgresWebLogUserData source member _.Serializer = ser @@ -165,7 +168,7 @@ type PostgresData (conn : NpgsqlConnection, log : ILogger, ser : J do! ensureTables () let! version = - Sql.existingConnection conn + Sql.fromDataSource source |> Sql.query "SELECT id FROM db_version" |> Sql.executeAsync (fun row -> row.string "id") |> tryHead diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index a9fecf4..7ae623d 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -64,9 +64,12 @@ module DataImplementation = elif hasConnStr "PostgreSQL" then let log = sp.GetRequiredService> () // NpgsqlLogManager.Provider <- ConsoleLoggingProvider NpgsqlLogLevel.Debug - let conn = new NpgsqlConnection (connStr "PostgreSQL") + let builder = NpgsqlDataSourceBuilder (connStr "PostgreSQL") + let _ = builder.UseNodaTime () + let source = builder.Build () + use conn = source.CreateConnection () log.LogInformation $"Using PostgreSQL database {conn.Host}:{conn.Port}/{conn.Database}" - PostgresData (conn, log, Json.configure (JsonSerializer.CreateDefault ())) + PostgresData (source, log, Json.configure (JsonSerializer.CreateDefault ())) else createSQLite "Data Source=./myweblog.db;Cache=Shared" diff --git a/src/Npgsql.FSharp.Documents/Library.fs b/src/Npgsql.FSharp.Documents/Library.fs index cbbbe00..db3c624 100644 --- a/src/Npgsql.FSharp.Documents/Library.fs +++ b/src/Npgsql.FSharp.Documents/Library.fs @@ -1,10 +1,242 @@ 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 = - /// Create a parameter for a @> (contains) query - let contains<'T> (name : string) (value : 'T) = - name, Sql.jsonb (string value) // FIXME: need a serializer + 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 index 9f4040e..795d55c 100644 --- a/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj +++ b/src/Npgsql.FSharp.Documents/Npgsql.FSharp.Documents.fsproj @@ -5,6 +5,7 @@ + -- 2.45.1 From bb12d2525efe51a5b62212dd78f5f9e9f5f22006 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 7 Feb 2023 20:52:12 -0500 Subject: [PATCH 04/17] Refine document queries --- src/MyWebLog.Data/Postgres/PostgresCache.fs | 16 +++---- .../Postgres/PostgresCategoryData.fs | 11 ++--- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 27 ++++++------ .../Postgres/PostgresPageData.fs | 42 +++++++------------ .../Postgres/PostgresPostData.fs | 19 ++++----- .../Postgres/PostgresTagMapData.fs | 9 ++-- .../Postgres/PostgresWebLogData.fs | 2 +- .../Postgres/PostgresWebLogUserData.fs | 9 ++-- 8 files changed, 57 insertions(+), 78 deletions(-) diff --git a/src/MyWebLog.Data/Postgres/PostgresCache.fs b/src/MyWebLog.Data/Postgres/PostgresCache.fs index 70b79d8..14c359d 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCache.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCache.fs @@ -200,11 +200,11 @@ type DistributedCache (connStr : string) = } interface IDistributedCache with - member this.Get key = get key CancellationToken.None |> sync - member this.GetAsync (key, token) = get key token - member this.Refresh key = refresh key CancellationToken.None |> sync - member this.RefreshAsync (key, token) = refresh key token - member this.Remove key = remove key CancellationToken.None |> sync - member this.RemoveAsync (key, token) = remove key token - member this.Set (key, value, options) = set key value options CancellationToken.None |> sync - member this.SetAsync (key, value, options, token) = set key value options token + member _.Get key = get key CancellationToken.None |> sync + member _.GetAsync (key, token) = get key token + member _.Refresh key = refresh key CancellationToken.None |> sync + member _.RefreshAsync (key, token) = refresh key token + member _.Remove key = remove key CancellationToken.None |> sync + member _.RemoveAsync (key, token) = remove key token + member _.Set (key, value, options) = set key value options CancellationToken.None |> sync + member _.SetAsync (key, value, options, token) = set key value options token diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index 16bc955..e06f45b 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -9,18 +9,15 @@ open Npgsql.FSharp.Documents /// PostgreSQL myWebLog category data implementation type PostgresCategoryData (source : NpgsqlDataSource) = - /// Shorthand for turning a web log ID into a string - let wls = WebLogId.toString - /// Count all categories for the given web log let countAll webLogId = Sql.fromDataSource source - |> Query.countByContains Table.Category {| WebLogId = wls webLogId |} + |> Query.countByContains Table.Category (webLogDoc webLogId) /// Count all top-level categories for the given web log let countTopLevel webLogId = Sql.fromDataSource source - |> Query.countByContains Table.Category {| WebLogId = wls webLogId; ParentId = None |} + |> Query.countByContains Table.Category {| webLogDoc webLogId with ParentId = None |} /// Retrieve all categories for the given web log in a DotLiquid-friendly format let findAllForView webLogId = backgroundTask { @@ -30,7 +27,7 @@ type PostgresCategoryData (source : NpgsqlDataSource) = {Query.selectFromTable Table.Category} WHERE {Query.whereDataContains "@criteria"} ORDER BY LOWER(data->>'{nameof Category.empty.Name}')""" - |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData let ordered = Utils.orderByHierarchy cats None None [] let counts = @@ -53,7 +50,7 @@ type PostgresCategoryData (source : NpgsqlDataSource) = AND ({catIdSql})""" |> Sql.parameters ( ("@criteria", - Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |}) + Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}) :: catIdParams) |> Sql.executeRowAsync Map.toCount |> Async.AwaitTask diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 5f06f5f..8802d09 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -68,16 +68,17 @@ open Npgsql open Npgsql.FSharp open Npgsql.FSharp.Documents -/// Create a WHERE clause fragment for the web log ID -let webLogWhere = "data ->> 'WebLogId' = @webLogId" - /// 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 |} + /// Create a parameter for a web log document-contains query let webLogContains webLogId = - Query.jsonbDocParam {| WebLogId = WebLogId.toString webLogId |} + "@criteria", Query.jsonbDocParam (webLogDoc webLogId) /// The name of the field to select to be able to use Map.toCount let countName = "the_count" @@ -110,11 +111,11 @@ let jsonArrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) = |> List.skip 1 |> List.fold (fun (itemS, itemP) it -> idx <- idx + 1 - $"{itemS} OR data -> '%s{name}' ? @{name}{idx}", + $"{itemS} OR data->'%s{name}' ? @{name}{idx}", ($"@{name}{idx}", Sql.jsonb (valueFunc it)) :: itemP) (Seq.ofList items |> Seq.map (fun it -> - $"data -> '{name}' ? @{name}0", [ $"@{name}0", Sql.string (valueFunc it) ]) + $"data->'{name}' ? @{name}0", [ $"@{name}0", Sql.string (valueFunc it) ]) |> Seq.head) /// Get the first result of the given query @@ -179,21 +180,21 @@ module Document = SELECT EXISTS ( SELECT 1 FROM %s{table} WHERE id = @id AND {Query.whereDataContains "@criteria"} ) AS {existsName}""" - |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogIdParam webLogId ] + |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogContains webLogId ] |> 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); "@criteria", webLogContains webLogId ] + |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogContains webLogId ] |> Sql.executeAsync fromData<'TDoc> |> tryHead /// Find a document by its ID for the given web log let findByWebLog<'TDoc> source table webLogId : Task<'TDoc list> = Sql.fromDataSource source - |> Query.findByContains table {| WebLogId = WebLogId.toString webLogId |} + |> Query.findByContains table (webLogDoc webLogId) /// Functions to support revisions @@ -209,13 +210,13 @@ module Revisions = /// 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 $" + |> Sql.query $""" SELECT pr.* FROM %s{revTable} pr INNER JOIN %s{entityTable} p ON p.id = pr.{entityTable}_id - WHERE p.{webLogWhere} - ORDER BY as_of DESC" - |> Sql.parameters [ webLogIdParam webLogId ] + 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) /// Parameters for a revision INSERT statement diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 29e1bf7..7b7bf29 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -11,9 +11,6 @@ type PostgresPageData (source : NpgsqlDataSource) = // SUPPORT FUNCTIONS - /// Shorthand for turning a web log ID into a string - let wls = WebLogId.toString - /// Append revisions to a page let appendPageRevisions (page : Page) = backgroundTask { let! revisions = Revisions.findByEntityId source Table.PageRevision Table.Page page.Id PageId.toString @@ -32,27 +29,28 @@ type PostgresPageData (source : NpgsqlDataSource) = let pageExists pageId webLogId = Document.existsByWebLog source Table.Page pageId PageId.toString webLogId + /// Select pages via a JSON document containment query + let pageByCriteria = + $"""{Query.selectFromTable Table.Page} WHERE {Query.whereDataContains "@criteria"}""" + // IMPLEMENTATION FUNCTIONS /// Get all pages for a web log (without text or revisions) let all webLogId = Sql.fromDataSource source - |> Sql.query $""" - {Query.selectFromTable Table.Page} - WHERE {Query.whereDataContains "@criteria"} - ORDER BY LOWER(data->>'{nameof Page.empty.Title}')""" - |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.query $"{pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}')" + |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData /// Count all pages for the given web log let countAll webLogId = Sql.fromDataSource source - |> Query.countByContains Table.Page {| WebLogId = wls webLogId |} + |> Query.countByContains Table.Page (webLogDoc webLogId) /// Count all pages shown in the page list for the given web log let countListed webLogId = Sql.fromDataSource source - |> Query.countByContains Table.Page {| WebLogId = wls webLogId; IsInPageList = true |} + |> Query.countByContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} /// Find a page by its ID (without revisions) let findById pageId webLogId = @@ -79,7 +77,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Find a page by its permalink for the given web log let findByPermalink permalink webLogId = Sql.fromDataSource source - |> Query.findByContains Table.Page {| WebLogId = wls webLogId; Permalink = Permalink.toString permalink |} + |> Query.findByContains Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} |> tryHead /// Find the current permalink within a set of potential prior permalinks for the given web log @@ -96,7 +94,7 @@ type PostgresPageData (source : NpgsqlDataSource) = FROM page WHERE {Query.whereDataContains "@criteria"} AND ({linkSql})""" - |> Sql.parameters (("@criteria", webLogContains webLogId) :: linkParams) + |> Sql.parameters (webLogContains webLogId :: linkParams) |> Sql.executeAsync Map.toPermalink |> tryHead } @@ -114,26 +112,18 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Get all listed pages for the given web log (without revisions or text) let findListed webLogId = Sql.fromDataSource source - |> Sql.query $""" - {Query.selectFromTable Table.Page} - WHERE {Query.whereDataContains "@criteria"} - ORDER BY LOWER(data->>'{nameof Page.empty.Title}')""" - |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; IsInPageList = true |} ] + |> Sql.query $"{pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}')" + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with IsInPageList = true |} ] |> Sql.executeAsync pageWithoutText /// Get a page of pages for the given web log (without revisions) let findPageOfPages webLogId pageNbr = Sql.fromDataSource source - |> Sql.query $""" - {Query.selectFromTable Table.Page} - WHERE {Query.whereDataContains "@criteria"} + |> Sql.query $" + {pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}') - LIMIT @pageSize OFFSET @toSkip""" - |> Sql.parameters - [ "@criteria", webLogContains webLogId - "@pageSize", Sql.int 26 - "@toSkip", Sql.int ((pageNbr - 1) * 25) - ] + LIMIT @pageSize OFFSET @toSkip" + |> Sql.parameters [ webLogContains webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ] |> Sql.executeAsync fromData /// The parameters for saving a page diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index f3cca98..3336314 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -12,9 +12,6 @@ type PostgresPostData (source : NpgsqlDataSource) = // SUPPORT FUNCTIONS - /// Shorthand for turning a web log ID into a string - let wls = WebLogId.toString - /// Append revisions to a post let appendPostRevisions (post : Post) = backgroundTask { let! revisions = Revisions.findByEntityId source Table.PostRevision Table.Post post.Id PostId.toString @@ -45,7 +42,7 @@ type PostgresPostData (source : NpgsqlDataSource) = |> Sql.query $"""SELECT COUNT(id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}""" |> Sql.parameters - [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString status |} ] + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString status |} ] |> Sql.executeRowAsync Map.toCount /// Find a post by its ID for the given web log (excluding revisions) @@ -57,7 +54,7 @@ type PostgresPostData (source : NpgsqlDataSource) = Sql.fromDataSource source |> Sql.query postsByCriteria |> Sql.parameters - [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Permalink = Permalink.toString permalink |} ] + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} ] |> Sql.executeAsync fromData |> tryHead @@ -99,7 +96,7 @@ type PostgresPostData (source : NpgsqlDataSource) = FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"} AND ({linkSql})""" - |> Sql.parameters (("@criteria", webLogContains webLogId) :: linkParams) + |> Sql.parameters (webLogContains webLogId :: linkParams) |> Sql.executeAsync Map.toPermalink |> tryHead } @@ -124,7 +121,7 @@ type PostgresPostData (source : NpgsqlDataSource) = ORDER BY published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters ( - ("@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |}) + ("@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}) :: catParams) |> Sql.executeAsync fromData @@ -136,7 +133,7 @@ type PostgresPostData (source : NpgsqlDataSource) = ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC NULLS FIRST, data->>'{nameof Post.empty.UpdatedOn}' LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync postWithoutText /// Get a page of published posts for the given web log (excludes revisions) @@ -147,7 +144,7 @@ type PostgresPostData (source : NpgsqlDataSource) = ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters - [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |} ] + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} ] |> Sql.executeAsync fromData /// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks) @@ -159,7 +156,7 @@ type PostgresPostData (source : NpgsqlDataSource) = ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters - [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |} + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} "@tag", Sql.jsonb tag ] |> Sql.executeAsync fromData @@ -167,7 +164,7 @@ type PostgresPostData (source : NpgsqlDataSource) = /// Find the next newest and oldest post from a publish date for the given web log let findSurroundingPosts webLogId (publishedOn : Instant) = backgroundTask { let queryParams () = Sql.parameters [ - "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Status = PostStatus.toString Published |} + "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} typedParam "publishedOn" publishedOn ] let! older = diff --git a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs index a576924..49ee19e 100644 --- a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs @@ -9,9 +9,6 @@ open Npgsql.FSharp.Documents /// PostgreSQL myWebLog tag mapping data implementation type PostgresTagMapData (source : NpgsqlDataSource) = - /// Shorthand for turning a web log ID into a string - let wls = WebLogId.toString - /// A query to select tag map(s) by JSON document containment criteria let tagMapByCriteria = $"""{Query.selectFromTable Table.TagMap} WHERE {Query.whereDataContains "@criteria"}""" @@ -33,7 +30,7 @@ type PostgresTagMapData (source : NpgsqlDataSource) = let findByUrlValue (urlValue : string) webLogId = Sql.fromDataSource source |> Sql.query tagMapByCriteria - |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; UrlValue = urlValue |} ] + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ] |> Sql.executeAsync fromData |> tryHead @@ -41,7 +38,7 @@ type PostgresTagMapData (source : NpgsqlDataSource) = let findByWebLog webLogId = Sql.fromDataSource source |> Sql.query $"{tagMapByCriteria} ORDER BY data->>'tag'" - |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData /// Find any tag mappings in a list of tags for the given web log @@ -49,7 +46,7 @@ type PostgresTagMapData (source : NpgsqlDataSource) = let tagSql, tagParams = jsonArrayInClause (nameof TagMap.empty.Tag) id tags Sql.fromDataSource source |> Sql.query $"{tagMapByCriteria} AND ({tagSql})" - |> Sql.parameters (("@criteria", webLogContains webLogId) :: tagParams) + |> Sql.parameters (webLogContains webLogId :: tagParams) |> Sql.executeAsync fromData /// The parameters for saving a tag mapping diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs index 25a14ed..4e1dace 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs @@ -33,7 +33,7 @@ type PostgresWebLogData (source : NpgsqlDataSource) = DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId; DELETE FROM {Table.WebLogUser} WHERE {criteria}; DELETE FROM {Table.WebLog} WHERE id = @webLogId" - |> Sql.parameters [ webLogIdParam webLogId; "@criteria", webLogContains webLogId ] + |> Sql.parameters [ webLogIdParam webLogId; webLogContains webLogId ] |> Sql.executeNonQueryAsync () } diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index 68a29d9..58d8f7a 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -9,9 +9,6 @@ open Npgsql.FSharp.Documents /// PostgreSQL myWebLog user data implementation type PostgresWebLogUserData (source : NpgsqlDataSource) = - /// Shorthand for making a web log ID into a string - let wls = WebLogId.toString - /// Query to get users by JSON document containment criteria let userByCriteria = $"""{Query.selectFromTable Table.WebLogUser} WHERE {Query.whereDataContains "@criteria"}""" @@ -51,7 +48,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) = let findByEmail (email : string) webLogId = Sql.fromDataSource source |> Sql.query userByCriteria - |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| WebLogId = wls webLogId; Email = email |} ] + |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ] |> Sql.executeAsync fromData |> tryHead @@ -59,7 +56,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) = let findByWebLog webLogId = Sql.fromDataSource source |> Sql.query $"{userByCriteria} ORDER BY LOWER(data->>'{nameof WebLogUser.empty.PreferredName}')" - |> Sql.parameters [ "@criteria", webLogContains webLogId ] + |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData /// Find the names of users by their IDs for the given web log @@ -68,7 +65,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) = let! users = Sql.fromDataSource source |> Sql.query $"{userByCriteria} {idSql}" - |> Sql.parameters (("@criteria", webLogContains webLogId) :: idParams) + |> Sql.parameters (webLogContains webLogId :: idParams) |> Sql.executeAsync fromData return users -- 2.45.1 From 0fd1760fa427d15c7c8807adfec884f0483ffeb3 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Tue, 7 Feb 2023 23:01:13 -0500 Subject: [PATCH 05/17] WIP on import / render with doc db --- src/MyWebLog.Data/MyWebLog.Data.fsproj | 3 +- .../Postgres/PostgresPageData.fs | 26 ++++++++++++++-- src/MyWebLog.Data/PostgresData.fs | 28 ++++++++--------- src/MyWebLog.Domain/MyWebLog.Domain.fsproj | 2 +- src/MyWebLog/Program.fs | 30 +++++++++++-------- src/MyWebLog/appsettings.json | 3 +- 6 files changed, 58 insertions(+), 34 deletions(-) diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index a788fee..9e43e6e 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -11,9 +11,8 @@ - - + diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 7b7bf29..2730976 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -1,5 +1,6 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open Npgsql @@ -7,22 +8,25 @@ open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostgreSQL myWebLog page data implementation -type PostgresPageData (source : NpgsqlDataSource) = +type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = // SUPPORT FUNCTIONS /// Append revisions to a page let appendPageRevisions (page : Page) = backgroundTask { + log.LogTrace "PostgresPageData.appendPageRevisions" let! revisions = Revisions.findByEntityId source Table.PageRevision Table.Page page.Id PageId.toString return { page with Revisions = revisions } } /// Return a page with no text or revisions - let pageWithoutText row = + let pageWithoutText (row : RowReader) = + log.LogDebug ("data: {0}", row.string "data") { fromData row with Text = "" } /// Update a page's revisions let updatePageRevisions pageId oldRevs newRevs = + log.LogTrace "PostgresPageData.updatePageRevisions" Revisions.update source Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs /// Does the given page exist? @@ -37,6 +41,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Get all pages for a web log (without text or revisions) let all webLogId = + log.LogTrace "PostgresPageData.all" Sql.fromDataSource source |> Sql.query $"{pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}')" |> Sql.parameters [ webLogContains webLogId ] @@ -44,20 +49,24 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Count all pages for the given web log let countAll webLogId = + log.LogTrace "PostgresPageData.countAll" Sql.fromDataSource source |> Query.countByContains Table.Page (webLogDoc webLogId) /// Count all pages shown in the page list for the given web log let countListed webLogId = + log.LogTrace "PostgresPageData.countListed" Sql.fromDataSource source |> Query.countByContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} /// Find a page by its ID (without revisions) let findById pageId webLogId = + log.LogTrace "PostgresPageData.findById" Document.findByIdAndWebLog source Table.Page pageId PageId.toString webLogId /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { + log.LogTrace "PostgresPageData.findFullById" match! findById pageId webLogId with | Some page -> let! withMore = appendPageRevisions page @@ -67,6 +76,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Delete a page by its ID let delete pageId webLogId = backgroundTask { + log.LogTrace "PostgresPageData.delete" match! pageExists pageId webLogId with | true -> do! Sql.fromDataSource source |> Query.deleteById Table.Page (PageId.toString pageId) @@ -76,12 +86,14 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Find a page by its permalink for the given web log let findByPermalink permalink webLogId = + log.LogTrace "PostgresPageData.findByPermalink" Sql.fromDataSource source |> Query.findByContains Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} |> tryHead /// Find the current permalink within a set of potential prior permalinks for the given web log let findCurrentPermalink permalinks webLogId = backgroundTask { + log.LogTrace "PostgresPageData.findCurrentPermalink" if List.isEmpty permalinks then return None else let linkSql, linkParams = @@ -101,6 +113,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Get all complete pages for the given web log let findFullByWebLog webLogId = backgroundTask { + log.LogTrace "PostgresPageData.findFullByWebLog" let! pages = Document.findByWebLog source Table.Page webLogId let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId return @@ -111,6 +124,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Get all listed pages for the given web log (without revisions or text) let findListed webLogId = + log.LogTrace "PostgresPageData.findListed" Sql.fromDataSource source |> Sql.query $"{pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}')" |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with IsInPageList = true |} ] @@ -118,6 +132,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Get a page of pages for the given web log (without revisions) let findPageOfPages webLogId pageNbr = + log.LogTrace "PostgresPageData.findPageOfPages" Sql.fromDataSource source |> Sql.query $" {pageByCriteria} @@ -132,11 +147,14 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Restore pages from a backup let restore (pages : Page list) = backgroundTask { + log.LogTrace "PostgresPageData.restore" let revisions = pages |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.Page, pages |> List.map pageParams + Query.insertQuery Table.Page, + pages + |> List.map (fun page -> Query.docParameters (PageId.toString page.Id) { page with Revisions = [] }) Revisions.insertSql Table.PageRevision, revisions |> List.map (fun (pageId, rev) -> Revisions.revParams pageId PageId.toString rev) ] @@ -145,6 +163,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Save a page let save (page : Page) = backgroundTask { + log.LogTrace "PostgresPageData.save" let! oldPage = findFullById page.Id page.WebLogId do! Sql.fromDataSource source |> Query.save Table.Page (PageId.toString page.Id) page do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions @@ -153,6 +172,7 @@ type PostgresPageData (source : NpgsqlDataSource) = /// Update a page's prior permalinks let updatePriorPermalinks pageId webLogId permalinks = backgroundTask { + log.LogTrace "PostgresPageData.updatePriorPermalinks" match! findById pageId webLogId with | Some page -> do! Sql.fromDataSource source diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index 932a80a..f3b63ca 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -60,10 +60,10 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : // Page tables if needsTable Table.Page then Definition.createTable Table.Page - $"CREATE INDEX page_web_log_idx ON {Table.Page} (data ->> '{nameof Page.empty.WebLogId}')" - $"CREATE INDEX page_author_idx ON {Table.Page} (data ->> '{nameof Page.empty.AuthorId}')" + $"CREATE INDEX page_web_log_idx ON {Table.Page} ((data ->> '{nameof Page.empty.WebLogId}'))" + $"CREATE INDEX page_author_idx ON {Table.Page} ((data ->> '{nameof Page.empty.AuthorId}'))" $"CREATE INDEX page_permalink_idx ON {Table.Page} - (data ->> '{nameof Page.empty.WebLogId}', data ->> '{nameof Page.empty.Permalink}')" + ((data ->> '{nameof Page.empty.WebLogId}'), (data ->> '{nameof Page.empty.Permalink}'))" if needsTable Table.PageRevision then $"CREATE TABLE {Table.PageRevision} ( page_id TEXT NOT NULL REFERENCES {Table.Page} (id) ON DELETE CASCADE, @@ -74,16 +74,15 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : // Post tables if needsTable Table.Post then Definition.createTable Table.Post - $"CREATE INDEX post_web_log_idx ON {Table.Post} (data ->> '{nameof Post.empty.WebLogId}')" - $"CREATE INDEX post_author_idx ON {Table.Post} (data ->> '{nameof Post.empty.AuthorId}')" + $"CREATE INDEX post_web_log_idx ON {Table.Post} ((data ->> '{nameof Post.empty.WebLogId}'))" + $"CREATE INDEX post_author_idx ON {Table.Post} ((data ->> '{nameof Post.empty.AuthorId}'))" $"CREATE INDEX post_status_idx ON {Table.Post} - (data ->> '{nameof Post.empty.WebLogId}', data ->> '{nameof Post.empty.Status}', - data ->> '{nameof Post.empty.UpdatedOn}')" + ((data ->> '{nameof Post.empty.WebLogId}'), (data ->> '{nameof Post.empty.Status}'), + (data ->> '{nameof Post.empty.UpdatedOn}'))" $"CREATE INDEX post_permalink_idx ON {Table.Post} - (data ->> '{nameof Post.empty.WebLogId}', data ->> '{nameof Post.empty.Permalink}')" - $"CREATE INDEX post_category_idx ON {Table.Post} USING GIN - (data ->> '{nameof Post.empty.CategoryIds}')" - $"CREATE INDEX post_tag_idx ON {Table.Post} USING GIN (data ->> '{nameof Post.empty.Tags}')" + ((data ->> '{nameof Post.empty.WebLogId}'), (data ->> '{nameof Post.empty.Permalink}'))" + $"CREATE INDEX post_category_idx ON {Table.Post} USING GIN ((data['{nameof Post.empty.CategoryIds}']))" + $"CREATE INDEX post_tag_idx ON {Table.Post} USING GIN ((data['{nameof Post.empty.Tags}']))" if needsTable Table.PostRevision then $"CREATE TABLE {Table.PostRevision} ( post_id TEXT NOT NULL REFERENCES {Table.Post} (id) ON DELETE CASCADE, @@ -92,7 +91,8 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : PRIMARY KEY (post_id, as_of))" if needsTable Table.PostComment then Definition.createTable Table.PostComment - $"CREATE INDEX post_comment_post_idx ON {Table.PostComment} (data ->> '{nameof Comment.empty.PostId}')" + $"CREATE INDEX post_comment_post_idx ON {Table.PostComment} + ((data ->> '{nameof Comment.empty.PostId}'))" // Tag map table if needsTable Table.TagMap then @@ -120,7 +120,7 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : |> Sql.executeTransactionAsync (sql |> Seq.map (fun s -> - let parts = s.Split ' ' + let parts = s.Replace(" IF NOT EXISTS", "", System.StringComparison.OrdinalIgnoreCase).Split ' ' if parts[1].ToLowerInvariant () = "table" then log.LogInformation $"Creating {parts[2]} table..." s, [ [] ]) @@ -153,7 +153,7 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : interface IData with member _.Category = PostgresCategoryData source - member _.Page = PostgresPageData source + member _.Page = PostgresPageData (source, log) member _.Post = PostgresPostData source member _.TagMap = PostgresTagMapData source member _.Theme = PostgresThemeData source diff --git a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj index ad1ed87..83c76c1 100644 --- a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj +++ b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj @@ -9,7 +9,7 @@ - + diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index 7ae623d..192f4a8 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -36,10 +36,16 @@ open Npgsql module DataImplementation = open MyWebLog.Converters - // open Npgsql.Logging open RethinkDb.Driver.FSharp open RethinkDb.Driver.Net + /// Create an NpgsqlDataSource from the connection string, configuring appropriately + let createNpgsqlDataSource (cfg : IConfiguration) = + let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL") + let _ = builder.UseNodaTime () + let _ = builder.UseLoggerFactory(LoggerFactory.Create(fun it -> it.AddConsole () |> ignore)) + builder.Build () + /// Get the configured data implementation let get (sp : IServiceProvider) : IData = let config = sp.GetRequiredService () @@ -62,12 +68,10 @@ module DataImplementation = let conn = await (rethinkCfg.CreateConnectionAsync log) RethinkDbData (conn, rethinkCfg, log) elif hasConnStr "PostgreSQL" then - let log = sp.GetRequiredService> () - // NpgsqlLogManager.Provider <- ConsoleLoggingProvider NpgsqlLogLevel.Debug - let builder = NpgsqlDataSourceBuilder (connStr "PostgreSQL") - let _ = builder.UseNodaTime () - let source = builder.Build () + let source = createNpgsqlDataSource config use conn = source.CreateConnection () + let log = sp.GetRequiredService> () + log.LogWarning (sprintf "%s %s" conn.DataSource conn.Database) log.LogInformation $"Using PostgreSQL database {conn.Host}:{conn.Port}/{conn.Database}" PostgresData (source, log, Json.configure (JsonSerializer.CreateDefault ())) else @@ -155,16 +159,16 @@ let rec main args = let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db" let _ = builder.Services.AddSqliteCache (fun o -> o.CachePath <- cachePath) () - | :? PostgresData -> - // ADO.NET connections are designed to work as per-request instantiation - let cfg = sp.GetRequiredService () + | :? PostgresData as postgres -> + // ADO.NET Data Sources are designed to work as singletons let _ = - builder.Services.AddScoped (fun sp -> - new NpgsqlConnection (cfg.GetConnectionString "PostgreSQL")) - let _ = builder.Services.AddScoped () + builder.Services.AddSingleton (fun sp -> + DataImplementation.createNpgsqlDataSource (sp.GetRequiredService ())) + let _ = builder.Services.AddSingleton postgres let _ = builder.Services.AddSingleton (fun sp -> - Postgres.DistributedCache (cfg.GetConnectionString "PostgreSQL") :> IDistributedCache) + Postgres.DistributedCache ((sp.GetRequiredService ()).GetConnectionString "PostgreSQL") + :> IDistributedCache) () | _ -> () diff --git a/src/MyWebLog/appsettings.json b/src/MyWebLog/appsettings.json index 62fa309..e7412a4 100644 --- a/src/MyWebLog/appsettings.json +++ b/src/MyWebLog/appsettings.json @@ -2,7 +2,8 @@ "Generator": "myWebLog 2.0-rc2", "Logging": { "LogLevel": { - "MyWebLog.Handlers": "Information" + "MyWebLog.Handlers": "Information", + "MyWebLog.Data": "Trace" } } } -- 2.45.1 From e09638d8fdef41ed7014273f87d3944552531221 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 8 Feb 2023 20:09:57 -0500 Subject: [PATCH 06/17] WIP on PostgreSQL doc implementation --- src/MyWebLog.Data/Converters.fs | 1 + src/MyWebLog.Data/Postgres/PostgresCache.fs | 18 ++-- .../Postgres/PostgresCategoryData.fs | 33 +++--- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 25 ++--- .../Postgres/PostgresPageData.fs | 61 +++++------ .../Postgres/PostgresPostData.fs | 100 ++++++++++-------- .../Postgres/PostgresTagMapData.fs | 31 +++--- .../Postgres/PostgresThemeData.fs | 18 +++- .../Postgres/PostgresUploadData.fs | 13 ++- .../Postgres/PostgresWebLogData.fs | 12 ++- .../Postgres/PostgresWebLogUserData.fs | 31 +++--- src/MyWebLog.Data/PostgresData.fs | 17 +-- src/MyWebLog/Program.fs | 8 +- src/MyWebLog/appsettings.json | 3 +- 14 files changed, 203 insertions(+), 168 deletions(-) diff --git a/src/MyWebLog.Data/Converters.fs b/src/MyWebLog.Data/Converters.fs index 82ff4c7..52a132c 100644 --- a/src/MyWebLog.Data/Converters.fs +++ b/src/MyWebLog.Data/Converters.fs @@ -165,6 +165,7 @@ module Json = Converters = ser.Converters, DefaultValueHandling = ser.DefaultValueHandling, DateFormatHandling = ser.DateFormatHandling, + DateParseHandling = ser.DateParseHandling, MetadataPropertyHandling = ser.MetadataPropertyHandling, MissingMemberHandling = ser.MissingMemberHandling, NullValueHandling = ser.NullValueHandling, diff --git a/src/MyWebLog.Data/Postgres/PostgresCache.fs b/src/MyWebLog.Data/Postgres/PostgresCache.fs index 14c359d..8b706a6 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCache.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCache.fs @@ -39,15 +39,17 @@ module private Helpers = typedParam "expireAt" +open Npgsql + /// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog -type DistributedCache (connStr : string) = +type DistributedCache (dataSource : NpgsqlDataSource) = // ~~~ INITIALIZATION ~~~ do task { let! exists = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query $" SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session') @@ -55,7 +57,7 @@ type DistributedCache (connStr : string) = |> Sql.executeRowAsync Map.toExists if not exists then let! _ = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query "CREATE TABLE session ( id TEXT NOT NULL PRIMARY KEY, @@ -74,7 +76,7 @@ type DistributedCache (connStr : string) = let getEntry key = backgroundTask { let idParam = "@id", Sql.string key let! tryEntry = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query "SELECT * FROM session WHERE id = @id" |> Sql.parameters [ idParam ] |> Sql.executeAsync (fun row -> @@ -97,7 +99,7 @@ type DistributedCache (connStr : string) = else true, { entry with ExpireAt = now.Plus slideExp } if needsRefresh then let! _ = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id" |> Sql.parameters [ expireParam item.ExpireAt; idParam ] |> Sql.executeNonQueryAsync @@ -114,7 +116,7 @@ type DistributedCache (connStr : string) = let now = getNow () if lastPurge.Plus (Duration.FromMinutes 30L) < now then let! _ = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query "DELETE FROM session WHERE expire_at < @expireAt" |> Sql.parameters [ expireParam now ] |> Sql.executeNonQueryAsync @@ -124,7 +126,7 @@ type DistributedCache (connStr : string) = /// Remove a cache entry let removeEntry key = backgroundTask { let! _ = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query "DELETE FROM session WHERE id = @id" |> Sql.parameters [ "@id", Sql.string key ] |> Sql.executeNonQueryAsync @@ -149,7 +151,7 @@ type DistributedCache (connStr : string) = let slide = Duration.FromHours 1 now.Plus slide, Some slide, None let! _ = - Sql.connect connStr + Sql.fromDataSource dataSource |> Sql.query "INSERT INTO session ( id, payload, expire_at, sliding_expiration, absolute_expiration diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index e06f45b..ce0bda7 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -1,5 +1,6 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open Npgsql @@ -7,26 +8,26 @@ open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostgreSQL myWebLog category data implementation -type PostgresCategoryData (source : NpgsqlDataSource) = +type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = /// Count all categories for the given web log let countAll webLogId = + log.LogTrace "Category.countAll" Sql.fromDataSource source |> Query.countByContains Table.Category (webLogDoc webLogId) /// Count all top-level categories for the given web log let countTopLevel webLogId = + log.LogTrace "Category.countTopLevel" Sql.fromDataSource source |> Query.countByContains Table.Category {| webLogDoc webLogId with ParentId = None |} /// Retrieve all categories for the given web log in a DotLiquid-friendly format let findAllForView webLogId = backgroundTask { + log.LogTrace "Category.findAllForView" let! cats = Sql.fromDataSource source - |> Sql.query $""" - {Query.selectFromTable Table.Category} - WHERE {Query.whereDataContains "@criteria"} - ORDER BY LOWER(data->>'{nameof Category.empty.Name}')""" + |> Sql.query $"{selectWithCriteria Table.Category} ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')" |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData let ordered = Utils.orderByHierarchy cats None None [] @@ -40,18 +41,19 @@ type PostgresCategoryData (source : NpgsqlDataSource) = |> Seq.map (fun cat -> cat.Id) |> Seq.append (Seq.singleton it.Id) |> List.ofSeq - |> jsonArrayInClause (nameof Post.empty.CategoryIds) id + |> arrayContains (nameof Post.empty.CategoryIds) id let postCount = Sql.fromDataSource source |> Sql.query $""" SELECT COUNT(DISTINCT id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"} - AND ({catIdSql})""" - |> Sql.parameters ( - ("@criteria", - Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}) - :: catIdParams) + AND {catIdSql}""" + |> Sql.parameters + [ "@criteria", + Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} + catIdParams + ] |> Sql.executeRowAsync Map.toCount |> Async.AwaitTask |> Async.RunSynchronously @@ -70,10 +72,12 @@ type PostgresCategoryData (source : NpgsqlDataSource) = } /// 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 /// Find all categories for the given web log let findByWebLog webLogId = + log.LogTrace "Category.findByWebLog" Document.findByWebLog source Table.Category webLogId /// Create parameters for a category insert / update @@ -82,6 +86,7 @@ type PostgresCategoryData (source : NpgsqlDataSource) = /// Delete a category let delete catId webLogId = backgroundTask { + log.LogTrace "Category.delete" match! findById catId webLogId with | Some cat -> // Reassign any children to the category's parent category @@ -100,8 +105,8 @@ type PostgresCategoryData (source : NpgsqlDataSource) = // 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", Sql.jsonb (CategoryId.toString catId) ] + |> 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 if not (List.isEmpty posts) then let! _ = @@ -125,11 +130,13 @@ type PostgresCategoryData (source : NpgsqlDataSource) = /// Save a category let save (cat : Category) = backgroundTask { + log.LogTrace "Category.save" do! Sql.fromDataSource source |> Query.save Table.Category (CategoryId.toString cat.Id) cat } /// Restore categories from a backup let restore cats = backgroundTask { + log.LogTrace "Category.restore" let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 8802d09..573236c 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -86,6 +86,10 @@ let countName = "the_count" /// The name of the field to select to be able to use Map.toExists let existsName = "does_exist" +/// A SQL string to select data from a table with the given JSON document contains criteria +let selectWithCriteria tableName = + $"""{Query.selectFromTable tableName} WHERE {Query.whereDataContains "@criteria"}""" + /// Create the SQL and parameters for an IN clause let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : 'T list) = if List.isEmpty items then "", [] @@ -102,22 +106,11 @@ let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : ' |> Seq.head) |> function sql, ps -> $"{sql})", ps -/// Create the SQL and parameters for the array-in-JSON equivalent of an IN clause -let jsonArrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) = - if List.isEmpty items then "TRUE = FALSE", [] - else - let mutable idx = 0 - items - |> List.skip 1 - |> List.fold (fun (itemS, itemP) it -> - idx <- idx + 1 - $"{itemS} OR data->'%s{name}' ? @{name}{idx}", - ($"@{name}{idx}", Sql.jsonb (valueFunc it)) :: itemP) - (Seq.ofList items - |> Seq.map (fun it -> - $"data->'{name}' ? @{name}0", [ $"@{name}0", Sql.string (valueFunc it) ]) - |> Seq.head) - +/// Create the SQL and parameters for match-any array query +let arrayContains<'T> name (valueFunc : 'T -> string) (items : 'T list) = + $"data['{name}'] ?| @{name}Values", + ($"@{name}Values", Sql.stringArray (items |> List.map valueFunc |> Array.ofList)) + /// Get the first result of the given query let tryHead<'T> (query : Task<'T list>) = backgroundTask { let! results = query diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 2730976..c1b1679 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -14,59 +14,55 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Append revisions to a page let appendPageRevisions (page : Page) = backgroundTask { - log.LogTrace "PostgresPageData.appendPageRevisions" + log.LogTrace "Page.appendPageRevisions" let! revisions = Revisions.findByEntityId source Table.PageRevision Table.Page page.Id PageId.toString return { page with Revisions = revisions } } /// Return a page with no text or revisions let pageWithoutText (row : RowReader) = - log.LogDebug ("data: {0}", row.string "data") { fromData row with Text = "" } /// Update a page's revisions let updatePageRevisions pageId oldRevs newRevs = - log.LogTrace "PostgresPageData.updatePageRevisions" + log.LogTrace "Page.updatePageRevisions" Revisions.update source 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 - /// Select pages via a JSON document containment query - let pageByCriteria = - $"""{Query.selectFromTable Table.Page} WHERE {Query.whereDataContains "@criteria"}""" - // IMPLEMENTATION FUNCTIONS /// Get all pages for a web log (without text or revisions) let all webLogId = - log.LogTrace "PostgresPageData.all" + log.LogTrace "Page.all" Sql.fromDataSource source - |> Sql.query $"{pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}')" + |> Sql.query $"{selectWithCriteria Table.Page} ORDER BY LOWER(data ->> '{nameof Page.empty.Title}')" |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData /// Count all pages for the given web log let countAll webLogId = - log.LogTrace "PostgresPageData.countAll" + log.LogTrace "Page.countAll" Sql.fromDataSource source |> Query.countByContains Table.Page (webLogDoc webLogId) /// Count all pages shown in the page list for the given web log let countListed webLogId = - log.LogTrace "PostgresPageData.countListed" + log.LogTrace "Page.countListed" Sql.fromDataSource source |> Query.countByContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} /// Find a page by its ID (without revisions) let findById pageId webLogId = - log.LogTrace "PostgresPageData.findById" + log.LogTrace "Page.findById" Document.findByIdAndWebLog source Table.Page pageId PageId.toString webLogId /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { - log.LogTrace "PostgresPageData.findFullById" + log.LogTrace "Page.findFullById" match! findById pageId webLogId with | Some page -> let! withMore = appendPageRevisions page @@ -76,7 +72,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Delete a page by its ID let delete pageId webLogId = backgroundTask { - log.LogTrace "PostgresPageData.delete" + log.LogTrace "Page.delete" match! pageExists pageId webLogId with | true -> do! Sql.fromDataSource source |> Query.deleteById Table.Page (PageId.toString pageId) @@ -86,34 +82,33 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Find a page by its permalink for the given web log let findByPermalink permalink webLogId = - log.LogTrace "PostgresPageData.findByPermalink" + log.LogTrace "Page.findByPermalink" Sql.fromDataSource source |> Query.findByContains Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} |> tryHead /// Find the current permalink within a set of potential prior permalinks for the given web log let findCurrentPermalink permalinks webLogId = backgroundTask { - log.LogTrace "PostgresPageData.findCurrentPermalink" + log.LogTrace "Page.findCurrentPermalink" if List.isEmpty permalinks then return None else - let linkSql, linkParams = - jsonArrayInClause (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks + let linkSql, linkParam = + arrayContains (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks return! - // TODO: stopped here Sql.fromDataSource source |> Sql.query $""" - SELECT data->>'{nameof Page.empty.Permalink}' AS permalink + SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink FROM page WHERE {Query.whereDataContains "@criteria"} - AND ({linkSql})""" - |> Sql.parameters (webLogContains webLogId :: linkParams) + AND {linkSql}""" + |> Sql.parameters [ webLogContains webLogId; linkParam ] |> Sql.executeAsync Map.toPermalink |> tryHead } /// Get all complete pages for the given web log let findFullByWebLog webLogId = backgroundTask { - log.LogTrace "PostgresPageData.findFullByWebLog" + log.LogTrace "Page.findFullByWebLog" let! pages = Document.findByWebLog source Table.Page webLogId let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId return @@ -124,30 +119,26 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Get all listed pages for the given web log (without revisions or text) let findListed webLogId = - log.LogTrace "PostgresPageData.findListed" + log.LogTrace "Page.findListed" Sql.fromDataSource source - |> Sql.query $"{pageByCriteria} ORDER BY LOWER(data->>'{nameof Page.empty.Title}')" + |> 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 /// Get a page of pages for the given web log (without revisions) let findPageOfPages webLogId pageNbr = - log.LogTrace "PostgresPageData.findPageOfPages" + log.LogTrace "Page.findPageOfPages" Sql.fromDataSource source |> Sql.query $" - {pageByCriteria} + {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 - /// The parameters for saving a page - let pageParams (page : Page) = - Query.docParameters (PageId.toString page.Id) page - /// Restore pages from a backup let restore (pages : Page list) = backgroundTask { - log.LogTrace "PostgresPageData.restore" + log.LogTrace "Page.restore" let revisions = pages |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = Sql.fromDataSource source @@ -163,16 +154,16 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Save a page let save (page : Page) = backgroundTask { - log.LogTrace "PostgresPageData.save" + log.LogTrace "Page.save" let! oldPage = findFullById page.Id page.WebLogId - do! Sql.fromDataSource source |> Query.save Table.Page (PageId.toString page.Id) page + do! Sql.fromDataSource source |> Query.save Table.Page (PageId.toString page.Id) { page with Revisions = [] } do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions () } /// Update a page's prior permalinks let updatePriorPermalinks pageId webLogId permalinks = backgroundTask { - log.LogTrace "PostgresPageData.updatePriorPermalinks" + log.LogTrace "Page.updatePriorPermalinks" match! findById pageId webLogId with | Some page -> do! Sql.fromDataSource source diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index 3336314..01537b2 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -1,19 +1,22 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open NodaTime +open NodaTime.Text open Npgsql open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostgreSQL myWebLog post data implementation -type PostgresPostData (source : NpgsqlDataSource) = +type PostgresPostData (source : NpgsqlDataSource, 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 return { post with Revisions = revisions } } @@ -24,20 +27,19 @@ type PostgresPostData (source : NpgsqlDataSource) = /// 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 /// Does the given post exist? let postExists postId webLogId = + log.LogTrace "Post.postExists" Document.existsByWebLog source Table.Post postId PostId.toString webLogId - /// Query to select posts by JSON document containment criteria - let postsByCriteria = - $"""{Query.selectFromTable Table.Post} WHERE {Query.whereDataContains "@criteria"}""" - // IMPLEMENTATION FUNCTIONS /// Count posts in a status for the given web log let countByStatus status webLogId = + log.LogTrace "Post.countByStatus" Sql.fromDataSource source |> Sql.query $"""SELECT COUNT(id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}""" @@ -47,12 +49,14 @@ type PostgresPostData (source : NpgsqlDataSource) = /// 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 /// 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 postsByCriteria + |> Sql.query (selectWithCriteria Table.Post) |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} ] |> Sql.executeAsync fromData @@ -60,6 +64,7 @@ type PostgresPostData (source : NpgsqlDataSource) = /// Find a complete post by its ID for the given web log let findFullById postId webLogId = backgroundTask { + log.LogTrace "Post.findFullById" match! findById postId webLogId with | Some post -> let! withRevisions = appendPostRevisions post @@ -69,6 +74,7 @@ type PostgresPostData (source : NpgsqlDataSource) = /// Delete a post by its ID for the given web log let delete postId webLogId = backgroundTask { + log.LogTrace "Post.delete" match! postExists postId webLogId with | true -> let theId = PostId.toString postId @@ -85,24 +91,26 @@ type PostgresPostData (source : NpgsqlDataSource) = /// Find the current permalink from a list of potential prior permalinks for the given web log let findCurrentPermalink permalinks webLogId = backgroundTask { + log.LogTrace "Post.findCurrentPermalink" if List.isEmpty permalinks then return None else - let linkSql, linkParams = - jsonArrayInClause (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks + 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 + SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"} - AND ({linkSql})""" - |> Sql.parameters (webLogContains webLogId :: linkParams) + AND {linkSql}""" + |> Sql.parameters [ webLogContains webLogId; linkParam ] |> Sql.executeAsync Map.toPermalink |> tryHead } /// Get all complete posts for the given web log let findFullByWebLog webLogId = backgroundTask { + log.LogTrace "Post.findFullByWebLog" let! posts = Document.findByWebLog source Table.Post webLogId let! revisions = Revisions.findByWebLog source Table.PostRevision Table.Post PostId webLogId return @@ -113,35 +121,39 @@ type PostgresPostData (source : NpgsqlDataSource) = /// Get a page of categorized posts for the given web log (excludes revisions) let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = - let catSql, catParams = jsonArrayInClause (nameof Post.empty.CategoryIds) CategoryId.toString categoryIds + log.LogTrace "Post.findPageOfCategorizedPosts" + let catSql, catParam = arrayContains (nameof Post.empty.CategoryIds) CategoryId.toString categoryIds Sql.fromDataSource source |> Sql.query $" - {postsByCriteria} - AND ({catSql}) - ORDER BY published_on DESC + {selectWithCriteria Table.Post} + AND {catSql} + ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - |> Sql.parameters ( - ("@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}) - :: catParams) + |> Sql.parameters + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} + catParam + ] |> Sql.executeAsync 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 $" - {postsByCriteria} - ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC NULLS FIRST, - data->>'{nameof Post.empty.UpdatedOn}' + {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 /// 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 $" - {postsByCriteria} - ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC + {selectWithCriteria Table.Post} + ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} ] @@ -149,63 +161,66 @@ type PostgresPostData (source : NpgsqlDataSource) = /// 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 $" - {postsByCriteria} - AND data->'{nameof Post.empty.Tags}' ? @tag - ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC + {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 [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} - "@tag", Sql.jsonb tag + "@tag", Query.jsonbDocParam [| tag |] ] |> Sql.executeAsync fromData /// Find the next newest and oldest post from a publish date for the given web log - let findSurroundingPosts webLogId (publishedOn : Instant) = backgroundTask { + let findSurroundingPosts webLogId publishedOn = backgroundTask { + log.LogTrace "Post.findSurroundingPosts" let queryParams () = Sql.parameters [ - "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} - typedParam "publishedOn" publishedOn + "@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 $" - {postsByCriteria} - AND data->>'{nameof Post.empty.PublishedOn}' < @publishedOn - ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC + {selectWithCriteria Table.Post} + AND SUBSTR(data ->> '{pubField}', 1, 19) < @publishedOn + ORDER BY data ->> '{pubField}' DESC LIMIT 1" |> queryParams () |> Sql.executeAsync fromData let! newer = Sql.fromDataSource source |> Sql.query $" - {postsByCriteria} - AND data->>'{nameof Post.empty.PublishedOn}' > @publishedOn - ORDER BY data->>'{nameof Post.empty.PublishedOn}' + {selectWithCriteria Table.Post} + AND SUBSTR(data ->> '{pubField}', 1, 19) > @publishedOn + ORDER BY data ->> '{pubField}' LIMIT 1" |> queryParams () |> Sql.executeAsync fromData return List.tryHead older, List.tryHead newer } - /// The parameters for saving a post - let postParams (post : Post) = - Query.docParameters (PostId.toString post.Id) post - /// Save a post let save (post : Post) = backgroundTask { + log.LogTrace "Post.save" let! oldPost = findFullById post.Id post.WebLogId - do! Sql.fromDataSource source |> Query.save Table.Post (PostId.toString post.Id) post + do! Sql.fromDataSource source |> Query.save Table.Post (PostId.toString post.Id) { post with Revisions = [] } do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions } /// Restore posts from a backup let restore posts = backgroundTask { + log.LogTrace "Post.restore" let revisions = posts |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.Post, posts |> List.map postParams + Query.insertQuery Table.Post, + posts + |> List.map (fun post -> Query.docParameters (PostId.toString post.Id) { post with Revisions = [] }) Revisions.insertSql Table.PostRevision, revisions |> List.map (fun (postId, rev) -> Revisions.revParams postId PostId.toString rev) ] @@ -214,6 +229,7 @@ type PostgresPostData (source : NpgsqlDataSource) = /// Update prior permalinks for a post let updatePriorPermalinks postId webLogId permalinks = backgroundTask { + log.LogTrace "Post.updatePriorPermalinks" match! findById postId webLogId with | Some post -> do! Sql.fromDataSource source diff --git a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs index 49ee19e..3993685 100644 --- a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs @@ -1,5 +1,6 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open Npgsql @@ -7,18 +8,16 @@ open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostgreSQL myWebLog tag mapping data implementation -type PostgresTagMapData (source : NpgsqlDataSource) = - - /// A query to select tag map(s) by JSON document containment criteria - let tagMapByCriteria = - $"""{Query.selectFromTable Table.TagMap} WHERE {Query.whereDataContains "@criteria"}""" +type PostgresTagMapData (source : NpgsqlDataSource, 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 /// 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 if exists then do! Sql.fromDataSource source |> Query.deleteById Table.TagMap (TagMapId.toString tagMapId) @@ -28,42 +27,42 @@ type PostgresTagMapData (source : NpgsqlDataSource) = /// 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 tagMapByCriteria + |> Sql.query (selectWithCriteria Table.TagMap) |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ] |> Sql.executeAsync fromData |> tryHead /// Get all tag mappings for the given web log let findByWebLog webLogId = + log.LogTrace "TagMap.findByWebLog" Sql.fromDataSource source - |> Sql.query $"{tagMapByCriteria} ORDER BY data->>'tag'" + |> Sql.query $"{selectWithCriteria Table.TagMap} ORDER BY data ->> 'tag'" |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync fromData /// Find any tag mappings in a list of tags for the given web log let findMappingForTags tags webLogId = - let tagSql, tagParams = jsonArrayInClause (nameof TagMap.empty.Tag) id tags + log.LogTrace "TagMap.findMappingForTags" + let tagSql, tagParam = arrayContains (nameof TagMap.empty.Tag) id tags Sql.fromDataSource source - |> Sql.query $"{tagMapByCriteria} AND ({tagSql})" - |> Sql.parameters (webLogContains webLogId :: tagParams) + |> Sql.query $"{selectWithCriteria Table.TagMap} AND {tagSql}" + |> Sql.parameters [ webLogContains webLogId; tagParam ] |> Sql.executeAsync fromData - /// The parameters for saving a tag mapping - let tagMapParams (tagMap : TagMap) = - Query.docParameters (TagMapId.toString tagMap.Id) tagMap - /// Save a tag mapping let save (tagMap : TagMap) = backgroundTask { do! Sql.fromDataSource source |> Query.save Table.TagMap (TagMapId.toString tagMap.Id) tagMap } /// Restore tag mappings from a backup - let restore tagMaps = backgroundTask { + let restore (tagMaps : TagMap list) = backgroundTask { let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.TagMap, tagMaps |> List.map tagMapParams + Query.insertQuery 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 1c56fca..757b8d4 100644 --- a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs @@ -1,14 +1,14 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data -open Newtonsoft.Json open Npgsql open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostreSQL myWebLog theme data implementation -type PostgresThemeData (source : NpgsqlDataSource) = +type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) = /// Clear out the template text from a theme let withoutTemplateText row = @@ -17,22 +17,26 @@ type PostgresThemeData (source : NpgsqlDataSource) = /// 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 /// Does a given theme exist? let exists themeId = + log.LogTrace "Theme.exists" Sql.fromDataSource source |> Query.existsById Table.Theme (ThemeId.toString themeId) /// Find a theme by its ID let findById themeId = + log.LogTrace "Theme.findById" Sql.fromDataSource source |> Query.tryById Table.Theme (ThemeId.toString themeId) /// 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) ] @@ -41,6 +45,7 @@ type PostgresThemeData (source : NpgsqlDataSource) = /// Delete a theme by its ID let delete themeId = backgroundTask { + log.LogTrace "Theme.delete" match! exists themeId with | true -> do! Sql.fromDataSource source |> Query.deleteById Table.Theme (ThemeId.toString themeId) @@ -50,6 +55,7 @@ type PostgresThemeData (source : NpgsqlDataSource) = /// Save a theme let save (theme : Theme) = + log.LogTrace "Theme.save" Sql.fromDataSource source |> Query.save Table.Theme (ThemeId.toString theme.Id) theme interface IThemeData with @@ -62,16 +68,18 @@ type PostgresThemeData (source : NpgsqlDataSource) = /// PostreSQL myWebLog theme data implementation -type PostgresThemeAssetData (source : NpgsqlDataSource) = +type PostgresThemeAssetData (source : NpgsqlDataSource, 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) /// Delete all assets for the given theme let deleteByTheme themeId = backgroundTask { + log.LogTrace "ThemeAsset.deleteByTheme" let! _ = Sql.fromDataSource source |> Sql.query $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @themeId" @@ -82,6 +90,7 @@ type PostgresThemeAssetData (source : NpgsqlDataSource) = /// 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" @@ -91,6 +100,7 @@ type PostgresThemeAssetData (source : NpgsqlDataSource) = /// 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) ] @@ -98,6 +108,7 @@ type PostgresThemeAssetData (source : NpgsqlDataSource) = /// 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) ] @@ -105,6 +116,7 @@ type PostgresThemeAssetData (source : NpgsqlDataSource) = /// Save a theme asset let save (asset : ThemeAsset) = backgroundTask { + log.LogTrace "ThemeAsset.save" let (ThemeAssetId (ThemeId themeId, path)) = asset.Id let! _ = Sql.fromDataSource source diff --git a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs index 08f2f2f..7c032b3 100644 --- a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs @@ -1,12 +1,13 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open Npgsql open Npgsql.FSharp /// PostgreSQL myWebLog uploaded file data implementation -type PostgresUploadData (source : NpgsqlDataSource) = +type PostgresUploadData (source : NpgsqlDataSource, log : ILogger) = /// The INSERT statement for an uploaded file let upInsert = $" @@ -27,6 +28,7 @@ type PostgresUploadData (source : NpgsqlDataSource) = /// Save an uploaded file let add upload = backgroundTask { + log.LogTrace "Upload.add" let! _ = Sql.fromDataSource source |> Sql.query upInsert @@ -37,6 +39,7 @@ type PostgresUploadData (source : NpgsqlDataSource) = /// 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 @@ -56,6 +59,7 @@ type PostgresUploadData (source : NpgsqlDataSource) = /// 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 ] @@ -64,6 +68,7 @@ type PostgresUploadData (source : NpgsqlDataSource) = /// 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 ] @@ -71,6 +76,7 @@ type PostgresUploadData (source : NpgsqlDataSource) = /// 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 ] @@ -78,12 +84,11 @@ type PostgresUploadData (source : NpgsqlDataSource) = /// 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 - |> Sql.executeTransactionAsync [ - upInsert, batch |> List.map upParams - ] + |> Sql.executeTransactionAsync [ upInsert, batch |> List.map upParams ] () } diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs index 4e1dace..5af36e3 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs @@ -1,5 +1,6 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open Npgsql @@ -7,19 +8,22 @@ open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostgreSQL myWebLog web log data implementation -type PostgresWebLogData (source : NpgsqlDataSource) = +type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) = /// Add a web log let add (webLog : WebLog) = + log.LogTrace "WebLog.add" Sql.fromDataSource source |> Query.insert Table.WebLog (WebLogId.toString webLog.Id) webLog /// Retrieve all web logs let all () = + log.LogTrace "WebLog.all" Sql.fromDataSource source |> Query.all Table.WebLog /// Delete a web log by its ID let delete webLogId = backgroundTask { + log.LogTrace "WebLog.delete" let criteria = Query.whereDataContains "@criteria" let! _ = Sql.fromDataSource source @@ -40,23 +44,27 @@ type PostgresWebLogData (source : NpgsqlDataSource) = /// Find a web log by its host (URL base) let findByHost (url : string) = + log.LogTrace "WebLog.findByHost" Sql.fromDataSource source - |> Sql.query $"""{Query.selectFromTable Table.WebLog} WHERE {Query.whereDataContains "@criteria"}""" + |> Sql.query (selectWithCriteria Table.WebLog) |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| UrlBase = url |} ] |> Sql.executeAsync fromData |> tryHead /// Find a web log by its ID let findById webLogId = + log.LogTrace "WebLog.findById" Sql.fromDataSource source |> Query.tryById Table.WebLog (WebLogId.toString webLogId) /// Update settings for a web log let updateSettings (webLog : WebLog) = + log.LogTrace "WebLog.updateSettings" Sql.fromDataSource source |> Query.update Table.WebLog (WebLogId.toString webLog.Id) webLog /// Update RSS options for a web log let updateRssOptions (webLog : WebLog) = backgroundTask { + log.LogTrace "WebLog.updateRssOptions" match! findById webLog.Id with | Some blog -> do! Sql.fromDataSource source diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index 58d8f7a..ebc03c5 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -1,5 +1,6 @@ namespace MyWebLog.Data.Postgres +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open Npgsql @@ -7,23 +8,17 @@ open Npgsql.FSharp open Npgsql.FSharp.Documents /// PostgreSQL myWebLog user data implementation -type PostgresWebLogUserData (source : NpgsqlDataSource) = +type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) = - /// Query to get users by JSON document containment criteria - let userByCriteria = - $"""{Query.selectFromTable Table.WebLogUser} WHERE {Query.whereDataContains "@criteria"}""" - - /// Parameters for saving web log users - let userParams (user : WebLogUser) = - Query.docParameters (WebLogUserId.toString user.Id) user - /// 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 /// Delete a user if they have no posts or pages let delete userId webLogId = backgroundTask { + log.LogTrace "WebLogUser.delete" match! findById userId webLogId with | Some _ -> let criteria = Query.whereDataContains "@criteria" @@ -46,25 +41,29 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) = /// 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 userByCriteria + |> Sql.query (selectWithCriteria Table.WebLogUser) |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ] |> Sql.executeAsync fromData |> tryHead /// Get all users for the given web log let findByWebLog webLogId = + log.LogTrace "WebLogUser.findByWebLog" Sql.fromDataSource source - |> Sql.query $"{userByCriteria} ORDER BY LOWER(data->>'{nameof WebLogUser.empty.PreferredName}')" + |> Sql.query + $"{selectWithCriteria Table.WebLogUser} ORDER BY LOWER(data->>'{nameof WebLogUser.empty.PreferredName}')" |> Sql.parameters [ webLogContains webLogId ] |> Sql.executeAsync 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 $"{userByCriteria} {idSql}" + |> Sql.query $"{selectWithCriteria Table.WebLogUser} {idSql}" |> Sql.parameters (webLogContains webLogId :: idParams) |> Sql.executeAsync fromData return @@ -73,17 +72,20 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) = } /// Restore users from a backup - let restore users = backgroundTask { + let restore (users : WebLogUser list) = backgroundTask { + log.LogTrace "WebLogUser.restore" let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.WebLogUser, users |> List.map userParams + Query.insertQuery Table.WebLogUser, + users |> List.map (fun user -> Query.docParameters (WebLogUserId.toString user.Id) user) ] () } /// Set a user's last seen date/time to now let setLastSeen userId webLogId = backgroundTask { + log.LogTrace "WebLogUser.setLastSeen" match! findById userId webLogId with | Some user -> do! Sql.fromDataSource source @@ -94,6 +96,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) = /// Save a user let save (user : WebLogUser) = + log.LogTrace "WebLogUser.save" Sql.fromDataSource source |> Query.save Table.WebLogUser (WebLogUserId.toString user.Id) user interface IWebLogUserData with diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index f3b63ca..4af3459 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -152,19 +152,20 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : interface IData with - member _.Category = PostgresCategoryData source + member _.Category = PostgresCategoryData (source, log) member _.Page = PostgresPageData (source, log) - member _.Post = PostgresPostData source - member _.TagMap = PostgresTagMapData source - member _.Theme = PostgresThemeData source - member _.ThemeAsset = PostgresThemeAssetData source - member _.Upload = PostgresUploadData source - member _.WebLog = PostgresWebLogData source - member _.WebLogUser = PostgresWebLogUserData source + 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 _.Serializer = ser member _.StartUp () = backgroundTask { + log.LogTrace "PostgresData.StartUp" do! ensureTables () let! version = diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index 192f4a8..0ea8258 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -43,7 +43,7 @@ module DataImplementation = let createNpgsqlDataSource (cfg : IConfiguration) = let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL") let _ = builder.UseNodaTime () - let _ = builder.UseLoggerFactory(LoggerFactory.Create(fun it -> it.AddConsole () |> ignore)) + // let _ = builder.UseLoggerFactory(LoggerFactory.Create(fun it -> it.AddConsole () |> ignore)) builder.Build () /// Get the configured data implementation @@ -71,8 +71,7 @@ module DataImplementation = let source = createNpgsqlDataSource config use conn = source.CreateConnection () let log = sp.GetRequiredService> () - log.LogWarning (sprintf "%s %s" conn.DataSource conn.Database) - log.LogInformation $"Using PostgreSQL database {conn.Host}:{conn.Port}/{conn.Database}" + log.LogInformation $"Using PostgreSQL database {conn.Database}" PostgresData (source, log, Json.configure (JsonSerializer.CreateDefault ())) else createSQLite "Data Source=./myweblog.db;Cache=Shared" @@ -167,8 +166,7 @@ let rec main args = let _ = builder.Services.AddSingleton postgres let _ = builder.Services.AddSingleton (fun sp -> - Postgres.DistributedCache ((sp.GetRequiredService ()).GetConnectionString "PostgreSQL") - :> IDistributedCache) + Postgres.DistributedCache (sp.GetRequiredService ()) :> IDistributedCache) () | _ -> () diff --git a/src/MyWebLog/appsettings.json b/src/MyWebLog/appsettings.json index e7412a4..62fa309 100644 --- a/src/MyWebLog/appsettings.json +++ b/src/MyWebLog/appsettings.json @@ -2,8 +2,7 @@ "Generator": "myWebLog 2.0-rc2", "Logging": { "LogLevel": { - "MyWebLog.Handlers": "Information", - "MyWebLog.Data": "Trace" + "MyWebLog.Handlers": "Information" } } } -- 2.45.1 From 99054801c7843740d4bb50cabf3e7dcfd58e29b7 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 8 Feb 2023 20:57:02 -0500 Subject: [PATCH 07/17] Fix back link (#34) --- src/admin-theme/custom-feed-edit.liquid | 247 ++++++++++++++++++------ 1 file changed, 185 insertions(+), 62 deletions(-) diff --git a/src/admin-theme/custom-feed-edit.liquid b/src/admin-theme/custom-feed-edit.liquid index 6f7d8f9..78e0b85 100644 --- a/src/admin-theme/custom-feed-edit.liquid +++ b/src/admin-theme/custom-feed-edit.liquid @@ -1,13 +1,19 @@

{{ page_title }}

- - + + {%- assign typ = model.source_type -%}
@@ -17,8 +23,13 @@
- + Appended to {{ web_log.url_base }}/
@@ -27,8 +38,13 @@
- +
@@ -41,21 +57,31 @@
- +
- {% for cat in categories -%} - {%- endfor %} @@ -64,16 +90,25 @@
- +
- +
@@ -83,27 +118,47 @@
-
+
Podcast Settings
- +
- +
- +
@@ -111,12 +166,20 @@
- + - + iTunes Category / Subcategory List @@ -124,17 +187,26 @@
- +
- + + + @@ -145,31 +217,54 @@
- +
- + For iTunes, must match registered e-mail
- + Optional; blank for no default
- + Relative URL will be appended to {{ web_log.url_base }}/
@@ -178,8 +273,14 @@
- + Displayed in podcast directories
@@ -188,8 +289,13 @@
- + Optional; prepended to episode media file if present
@@ -198,8 +304,13 @@
- + Optional; URL describing donation options for this podcast, relative URL supported @@ -208,8 +319,14 @@
- + Optional; text for the funding link
@@ -218,21 +335,28 @@
- + Optional; v5 UUID uniquely identifying this podcast; once entered, do not change this value - (documentation) + (documentation)
- {% for med in medium_values -%} - {%- endfor %} @@ -240,8 +364,7 @@ Optional; medium of the podcast content - (documentation) + (documentation)
@@ -256,4 +379,4 @@
-
+ \ No newline at end of file -- 2.45.1 From 1963d226ae211397053c735f0faac30dad479e55 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Wed, 8 Feb 2023 21:57:07 -0500 Subject: [PATCH 08/17] Show theme upload messages (#28) --- src/MyWebLog/Handlers/Helpers.fs | 1 - src/admin-theme/wwwroot/admin.js | 33 ++++++++++++++++++++------------ 2 files changed, 21 insertions(+), 13 deletions(-) diff --git a/src/MyWebLog/Handlers/Helpers.fs b/src/MyWebLog/Handlers/Helpers.fs index ee7075c..2edefe8 100644 --- a/src/MyWebLog/Handlers/Helpers.fs +++ b/src/MyWebLog/Handlers/Helpers.fs @@ -120,7 +120,6 @@ module ViewContext = /// The current web log [] let WebLog = "web_log" - /// The HTTP item key for loading the session diff --git a/src/admin-theme/wwwroot/admin.js b/src/admin-theme/wwwroot/admin.js index 555b81b..308b78a 100644 --- a/src/admin-theme/wwwroot/admin.js +++ b/src/admin-theme/wwwroot/admin.js @@ -334,27 +334,34 @@ this.Admin = { const theToast = new bootstrap.Toast(toast, options) theToast.show() }) + }, + + /** + * Initialize any toasts that were pre-rendered from the server + */ + showPreRenderedMessages() { + [...document.querySelectorAll(".toast")].forEach(el => { + if (el.getAttribute("data-mwl-shown") === "true" && el.className.indexOf("hide") >= 0) { + document.removeChild(el) + } else { + const toast = new bootstrap.Toast(el, + el.getAttribute("data-bs-autohide") === "false" + ? { autohide: false } : { delay: 6000, autohide: true }) + toast.show() + el.setAttribute("data-mwl-shown", "true") + } + }) } } htmx.on("htmx:afterOnLoad", function (evt) { const hdrs = evt.detail.xhr.getAllResponseHeaders() + // Initialize any toasts that were pre-rendered from the server + Admin.showPreRenderedMessages() // Show messages if there were any in the response if (hdrs.indexOf("x-message") >= 0) { Admin.showMessage(evt.detail.xhr.getResponseHeader("x-message")) } - // Initialize any toasts that were pre-rendered from the server - [...document.querySelectorAll(".toast")].forEach(el => { - if (el.getAttribute("data-mwl-shown") === "true" && el.className.indexOf("hide") >= 0) { - document.removeChild(el) - } else { - const toast = new bootstrap.Toast(el, - el.getAttribute("data-bs-autohide") === "false" - ? { autohide: false } : { delay: 6000, autohide: true }) - toast.show() - el.setAttribute("data-mwl-shown", "true") - } - }) }) htmx.on("htmx:responseError", function (evt) { @@ -365,3 +372,5 @@ htmx.on("htmx:responseError", function (evt) { Admin.showMessage(`danger|||${xhr.status}: ${xhr.statusText}`) } }) + +document.addEventListener("DOMContentLoaded", Admin.showPreRenderedMessages, { once: true}) -- 2.45.1 From 612f04accc605bebd91ea397e66d37b5714a1da2 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 17 Feb 2023 20:26:17 -0600 Subject: [PATCH 09/17] Update to PG doc library --- src/MyWebLog.Data/MyWebLog.Data.fsproj | 2 +- .../Postgres/PostgresCategoryData.fs | 22 ++++++++----------- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 7 +++--- .../Postgres/PostgresPageData.fs | 20 +++++++---------- .../Postgres/PostgresPostData.fs | 9 ++++---- .../Postgres/PostgresTagMapData.fs | 9 ++++---- .../Postgres/PostgresThemeData.fs | 10 ++++----- .../Postgres/PostgresWebLogData.fs | 14 +++++------- .../Postgres/PostgresWebLogUserData.fs | 10 ++++----- src/MyWebLog.Data/PostgresData.fs | 2 +- 10 files changed, 43 insertions(+), 62 deletions(-) diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index 9e43e6e..38a5455 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -2,7 +2,7 @@ - + diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index ce0bda7..28d491a 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -13,14 +13,12 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = /// Count all categories for the given web log let countAll webLogId = log.LogTrace "Category.countAll" - Sql.fromDataSource source - |> Query.countByContains Table.Category (webLogDoc webLogId) + Count.byContains Table.Category (webLogDoc webLogId) /// Count all top-level categories for the given web log let countTopLevel webLogId = log.LogTrace "Category.countTopLevel" - Sql.fromDataSource source - |> Query.countByContains Table.Category {| webLogDoc webLogId with ParentId = None |} + Count.byContains Table.Category {| webLogDoc webLogId with ParentId = None |} /// Retrieve all categories for the given web log in a DotLiquid-friendly format let findAllForView webLogId = backgroundTask { @@ -78,7 +76,7 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = /// Find all categories for the given web log let findByWebLog webLogId = log.LogTrace "Category.findByWebLog" - Document.findByWebLog source Table.Category webLogId + Document.findByWebLog Table.Category webLogId /// Create parameters for a category insert / update let catParameters (cat : Category) = @@ -90,15 +88,13 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = match! findById catId webLogId with | Some cat -> // Reassign any children to the category's parent category - let! children = - Sql.fromDataSource source - |> Query.findByContains Table.Category {| ParentId = CategoryId.toString catId |} + let! children = Find.byContains Table.Category {| ParentId = CategoryId.toString catId |} let hasChildren = not (List.isEmpty children) if hasChildren then let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.updateQuery Table.Category, + Query.update Table.Category, children |> List.map (fun child -> catParameters { child with ParentId = cat.ParentId }) ] () @@ -112,7 +108,7 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.updateQuery Table.Post, + Query.update Table.Post, posts |> List.map (fun post -> [ "@id", Sql.string (PostId.toString post.Id) "@data", Query.jsonbDocParam @@ -123,7 +119,7 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = ] () // Delete the category itself - do! Sql.fromDataSource source |> Query.deleteById Table.Category (CategoryId.toString catId) + do! Delete.byId Table.Category (CategoryId.toString catId) return if hasChildren then ReassignedChildCategories else CategoryDeleted | None -> return CategoryNotFound } @@ -131,7 +127,7 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = /// Save a category let save (cat : Category) = backgroundTask { log.LogTrace "Category.save" - do! Sql.fromDataSource source |> Query.save Table.Category (CategoryId.toString cat.Id) cat + do! save Table.Category (CategoryId.toString cat.Id) cat } /// Restore categories from a backup @@ -140,7 +136,7 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.Category, cats |> List.map catParameters + 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 573236c..c413fd3 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -185,10 +185,9 @@ module Document = |> tryHead /// Find a document by its ID for the given web log - let findByWebLog<'TDoc> source table webLogId : Task<'TDoc list> = - Sql.fromDataSource source - |> Query.findByContains table (webLogDoc webLogId) - + let findByWebLog<'TDoc> table webLogId : Task<'TDoc list> = + Find.byContains table (webLogDoc webLogId) + /// Functions to support revisions module Revisions = diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index c1b1679..729381a 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -46,14 +46,12 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Count all pages for the given web log let countAll webLogId = log.LogTrace "Page.countAll" - Sql.fromDataSource source - |> Query.countByContains Table.Page (webLogDoc webLogId) + Count.byContains Table.Page (webLogDoc webLogId) /// Count all pages shown in the page list for the given web log let countListed webLogId = log.LogTrace "Page.countListed" - Sql.fromDataSource source - |> Query.countByContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} + Count.byContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} /// Find a page by its ID (without revisions) let findById pageId webLogId = @@ -75,7 +73,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = log.LogTrace "Page.delete" match! pageExists pageId webLogId with | true -> - do! Sql.fromDataSource source |> Query.deleteById Table.Page (PageId.toString pageId) + do! Delete.byId Table.Page (PageId.toString pageId) return true | false -> return false } @@ -83,8 +81,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Find a page by its permalink for the given web log let findByPermalink permalink webLogId = log.LogTrace "Page.findByPermalink" - Sql.fromDataSource source - |> Query.findByContains Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} + Find.byContains Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} |> tryHead /// Find the current permalink within a set of potential prior permalinks for the given web log @@ -109,7 +106,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Get all complete pages for the given web log let findFullByWebLog webLogId = backgroundTask { log.LogTrace "Page.findFullByWebLog" - let! pages = Document.findByWebLog source Table.Page webLogId + let! pages = Document.findByWebLog Table.Page webLogId let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId return pages @@ -143,7 +140,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.Page, + Query.insert Table.Page, pages |> List.map (fun page -> Query.docParameters (PageId.toString page.Id) { page with Revisions = [] }) Revisions.insertSql Table.PageRevision, @@ -156,7 +153,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = let save (page : Page) = backgroundTask { log.LogTrace "Page.save" let! oldPage = findFullById page.Id page.WebLogId - do! Sql.fromDataSource source |> Query.save Table.Page (PageId.toString page.Id) { page with Revisions = [] } + do! save Table.Page (PageId.toString page.Id) { page with Revisions = [] } do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions () } @@ -166,8 +163,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = log.LogTrace "Page.updatePriorPermalinks" match! findById pageId webLogId with | Some page -> - do! Sql.fromDataSource source - |> Query.update Table.Page (PageId.toString page.Id) { page with PriorPermalinks = permalinks } + do! update Table.Page (PageId.toString page.Id) { page with PriorPermalinks = permalinks } return true | None -> return false } diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index 01537b2..4f5cafd 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -111,7 +111,7 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) = /// Get all complete posts for the given web log let findFullByWebLog webLogId = backgroundTask { log.LogTrace "Post.findFullByWebLog" - let! posts = Document.findByWebLog source Table.Post webLogId + let! posts = Document.findByWebLog Table.Post webLogId let! revisions = Revisions.findByWebLog source Table.PostRevision Table.Post PostId webLogId return posts @@ -207,7 +207,7 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) = let save (post : Post) = backgroundTask { log.LogTrace "Post.save" let! oldPost = findFullById post.Id post.WebLogId - do! Sql.fromDataSource source |> Query.save Table.Post (PostId.toString post.Id) { post with Revisions = [] } + do! save Table.Post (PostId.toString post.Id) { post with Revisions = [] } do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions } @@ -218,7 +218,7 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) = let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.Post, + Query.insert Table.Post, posts |> List.map (fun post -> Query.docParameters (PostId.toString post.Id) { post with Revisions = [] }) Revisions.insertSql Table.PostRevision, @@ -232,8 +232,7 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) = log.LogTrace "Post.updatePriorPermalinks" match! findById postId webLogId with | Some post -> - do! Sql.fromDataSource source - |> Query.update Table.Post (PostId.toString post.Id) { post with PriorPermalinks = permalinks } + do! update Table.Post (PostId.toString post.Id) { post with PriorPermalinks = permalinks } return true | None -> return false } diff --git a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs index 3993685..c4a5a4e 100644 --- a/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresTagMapData.fs @@ -20,7 +20,7 @@ type PostgresTagMapData (source : NpgsqlDataSource, log : ILogger) = log.LogTrace "TagMap.delete" let! exists = Document.existsByWebLog source Table.TagMap tagMapId TagMapId.toString webLogId if exists then - do! Sql.fromDataSource source |> Query.deleteById Table.TagMap (TagMapId.toString tagMapId) + do! Delete.byId Table.TagMap (TagMapId.toString tagMapId) return true else return false } @@ -52,16 +52,15 @@ type PostgresTagMapData (source : NpgsqlDataSource, log : ILogger) = |> Sql.executeAsync fromData /// Save a tag mapping - let save (tagMap : TagMap) = backgroundTask { - do! Sql.fromDataSource source |> Query.save Table.TagMap (TagMapId.toString tagMap.Id) tagMap - } + let save (tagMap : TagMap) = + save Table.TagMap (TagMapId.toString tagMap.Id) tagMap /// Restore tag mappings from a backup let restore (tagMaps : TagMap list) = backgroundTask { let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.TagMap, + 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 757b8d4..54166fc 100644 --- a/src/MyWebLog.Data/Postgres/PostgresThemeData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresThemeData.fs @@ -25,14 +25,12 @@ type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) = /// Does a given theme exist? let exists themeId = log.LogTrace "Theme.exists" - Sql.fromDataSource source - |> Query.existsById Table.Theme (ThemeId.toString themeId) + Exists.byId Table.Theme (ThemeId.toString themeId) /// Find a theme by its ID let findById themeId = log.LogTrace "Theme.findById" - Sql.fromDataSource source - |> Query.tryById Table.Theme (ThemeId.toString themeId) + Find.byId Table.Theme (ThemeId.toString themeId) /// Find a theme by its ID (excludes the text of templates) let findByIdWithoutText themeId = @@ -48,7 +46,7 @@ type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) = log.LogTrace "Theme.delete" match! exists themeId with | true -> - do! Sql.fromDataSource source |> Query.deleteById Table.Theme (ThemeId.toString themeId) + do! Delete.byId Table.Theme (ThemeId.toString themeId) return true | false -> return false } @@ -56,7 +54,7 @@ type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) = /// Save a theme let save (theme : Theme) = log.LogTrace "Theme.save" - Sql.fromDataSource source |> Query.save Table.Theme (ThemeId.toString theme.Id) theme + save Table.Theme (ThemeId.toString theme.Id) theme interface IThemeData with member _.All () = all () diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs index 5af36e3..f3d2d2f 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs @@ -13,13 +13,12 @@ type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) = /// Add a web log let add (webLog : WebLog) = log.LogTrace "WebLog.add" - Sql.fromDataSource source |> Query.insert Table.WebLog (WebLogId.toString webLog.Id) webLog + insert Table.WebLog (WebLogId.toString webLog.Id) webLog /// Retrieve all web logs let all () = log.LogTrace "WebLog.all" - Sql.fromDataSource source - |> Query.all Table.WebLog + all Table.WebLog /// Delete a web log by its ID let delete webLogId = backgroundTask { @@ -54,21 +53,18 @@ type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) = /// Find a web log by its ID let findById webLogId = log.LogTrace "WebLog.findById" - Sql.fromDataSource source - |> Query.tryById Table.WebLog (WebLogId.toString webLogId) + Find.byId Table.WebLog (WebLogId.toString webLogId) /// Update settings for a web log let updateSettings (webLog : WebLog) = log.LogTrace "WebLog.updateSettings" - Sql.fromDataSource source |> Query.update Table.WebLog (WebLogId.toString webLog.Id) webLog + update Table.WebLog (WebLogId.toString webLog.Id) webLog /// Update RSS options for a web log let updateRssOptions (webLog : WebLog) = backgroundTask { log.LogTrace "WebLog.updateRssOptions" match! findById webLog.Id with - | Some blog -> - do! Sql.fromDataSource source - |> Query.update Table.WebLog (WebLogId.toString webLog.Id) { blog with Rss = webLog.Rss } + | Some blog -> do! update Table.WebLog (WebLogId.toString webLog.Id) { blog with Rss = webLog.Rss } | None -> () } diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index ebc03c5..33bd3cc 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -34,7 +34,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) = if isAuthor then return Error "User has pages or posts; cannot delete" else - do! Sql.fromDataSource source |> Query.deleteById Table.WebLogUser usrId + do! Delete.byId Table.WebLogUser usrId return Ok true | None -> return Error "User does not exist" } @@ -77,7 +77,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) = let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.insertQuery Table.WebLogUser, + Query.insert Table.WebLogUser, users |> List.map (fun user -> Query.docParameters (WebLogUserId.toString user.Id) user) ] () @@ -88,16 +88,14 @@ type PostgresWebLogUserData (source : NpgsqlDataSource, log : ILogger) = log.LogTrace "WebLogUser.setLastSeen" match! findById userId webLogId with | Some user -> - do! Sql.fromDataSource source - |> Query.update Table.WebLogUser (WebLogUserId.toString userId) - { user with LastSeenOn = Some (Noda.now ()) } + do! update Table.WebLogUser (WebLogUserId.toString userId) { user with LastSeenOn = Some (Noda.now ()) } | None -> () } /// Save a user let save (user : WebLogUser) = log.LogTrace "WebLogUser.save" - Sql.fromDataSource source |> Query.save Table.WebLogUser (WebLogUserId.toString user.Id) user + save Table.WebLogUser (WebLogUserId.toString user.Id) user interface IWebLogUserData with member _.Add user = save user diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index 4af3459..81218c6 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -16,7 +16,7 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : // Set up the PostgreSQL document store Configuration.useDataSource source Configuration.useSerializer - { new IDocumentSerializer with + { new Documents.IDocumentSerializer with member _.Serialize<'T> (it : 'T) : string = Utils.serialize ser it member _.Deserialize<'T> (it : string) : 'T = Utils.deserialize ser it } -- 2.45.1 From 46a7402c8e39902f473b1fd611e6daf6fceebaee Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 17 Feb 2023 22:10:10 -0500 Subject: [PATCH 10/17] Update updates :) --- .../Postgres/PostgresCategoryData.fs | 15 ++++++------ .../Postgres/PostgresPageData.fs | 8 +++---- .../Postgres/PostgresPostData.fs | 9 ++++--- .../Postgres/PostgresUploadData.fs | 2 +- .../Postgres/PostgresWebLogData.fs | 24 +++++++++---------- .../Postgres/PostgresWebLogUserData.fs | 8 +++---- 6 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index 28d491a..cbed623 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -88,14 +88,17 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = match! findById catId webLogId with | Some cat -> // Reassign any children to the category's parent category - let! children = Find.byContains Table.Category {| ParentId = CategoryId.toString catId |} + let! children = Find.byContains Table.Category {| ParentId = CategoryId.toString catId |} let hasChildren = not (List.isEmpty children) if hasChildren then let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.update Table.Category, - children |> List.map (fun child -> catParameters { child with ParentId = cat.ParentId }) + Query.Update.partialById Table.Category, + children |> List.map (fun child -> [ + "@id", Sql.string (CategoryId.toString child.Id) + "@data", Query.jsonbDocParam {| ParentId = cat.ParentId |} + ]) ] () // Delete the category off all posts where it is assigned @@ -108,13 +111,11 @@ type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) = let! _ = Sql.fromDataSource source |> Sql.executeTransactionAsync [ - Query.update Table.Post, + Query.Update.partialById Table.Post, posts |> List.map (fun post -> [ "@id", Sql.string (PostId.toString post.Id) "@data", Query.jsonbDocParam - { post with - CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) - } + {| CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) |} ]) ] () diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 729381a..01182a2 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -161,11 +161,11 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) = /// Update a page's prior permalinks let updatePriorPermalinks pageId webLogId permalinks = backgroundTask { log.LogTrace "Page.updatePriorPermalinks" - match! findById pageId webLogId with - | Some page -> - do! update Table.Page (PageId.toString page.Id) { page with PriorPermalinks = permalinks } + match! pageExists pageId webLogId with + | true -> + do! Update.partialById Table.Page (PageId.toString pageId) {| PriorPermalinks = permalinks |} return true - | None -> return false + | false -> return false } interface IPageData with diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index 4f5cafd..71a42a7 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -3,7 +3,6 @@ namespace MyWebLog.Data.Postgres open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data -open NodaTime open NodaTime.Text open Npgsql open Npgsql.FSharp @@ -230,11 +229,11 @@ type PostgresPostData (source : NpgsqlDataSource, log : ILogger) = /// Update prior permalinks for a post let updatePriorPermalinks postId webLogId permalinks = backgroundTask { log.LogTrace "Post.updatePriorPermalinks" - match! findById postId webLogId with - | Some post -> - do! update Table.Post (PostId.toString post.Id) { post with PriorPermalinks = permalinks } + match! postExists postId webLogId with + | true -> + do! Update.partialById Table.Post (PostId.toString postId) {| PriorPermalinks = permalinks |} return true - | None -> return false + | false -> return false } interface IPostData with diff --git a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs index 7c032b3..d713a19 100644 --- a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs @@ -50,7 +50,7 @@ type PostgresUploadData (source : NpgsqlDataSource, log : ILogger) = if Option.isSome path then let! _ = Sql.fromDataSource source - |> Sql.query $"DELETE FROM {Table.Upload} WHERE id = @id" + |> Sql.query (Documents.Query.Delete.byId Table.Upload) |> Sql.parameters idParam |> Sql.executeNonQueryAsync return Ok path.Value diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs index f3d2d2f..67da00f 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogData.fs @@ -23,19 +23,19 @@ type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) = /// Delete a web log by its ID let delete webLogId = backgroundTask { log.LogTrace "WebLog.delete" - let criteria = Query.whereDataContains "@criteria" let! _ = Sql.fromDataSource source - |> Sql.query $" + |> Sql.query $""" DELETE FROM {Table.PostComment} - WHERE data->>'{nameof Comment.empty.PostId}' IN (SELECT id FROM {Table.Post} WHERE {criteria}); - DELETE FROM {Table.Post} WHERE {criteria}; - DELETE FROM {Table.Page} WHERE {criteria}; - DELETE FROM {Table.Category} WHERE {criteria}; - DELETE FROM {Table.TagMap} WHERE {criteria}; - DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId; - DELETE FROM {Table.WebLogUser} WHERE {criteria}; - DELETE FROM {Table.WebLog} WHERE id = @webLogId" + WHERE data ->> '{nameof Comment.empty.PostId}' IN + (SELECT id FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}); + {Query.Delete.byContains Table.Post}; + {Query.Delete.byContains Table.Page}; + {Query.Delete.byContains Table.Category}; + {Query.Delete.byContains Table.TagMap}; + {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 () @@ -58,13 +58,13 @@ type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) = /// Update settings for a web log let updateSettings (webLog : WebLog) = log.LogTrace "WebLog.updateSettings" - update Table.WebLog (WebLogId.toString webLog.Id) webLog + Update.full Table.WebLog (WebLogId.toString webLog.Id) webLog /// Update RSS options for a web log let updateRssOptions (webLog : WebLog) = backgroundTask { log.LogTrace "WebLog.updateRssOptions" match! findById webLog.Id with - | Some blog -> do! update Table.WebLog (WebLogId.toString webLog.Id) { blog with Rss = webLog.Rss } + | Some _ -> do! Update.partialById Table.WebLog (WebLogId.toString webLog.Id) {| Rss = webLog.Rss |} | None -> () } diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index 33bd3cc..b1ea453 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -86,10 +86,10 @@ 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! findById userId webLogId with - | Some user -> - do! update Table.WebLogUser (WebLogUserId.toString userId) { user with LastSeenOn = Some (Noda.now ()) } - | None -> () + match! Document.existsByWebLog source Table.WebLogUser userId WebLogUserId.toString webLogId with + | true -> + do! Update.partialById Table.WebLogUser (WebLogUserId.toString userId) {| LastSeenOn = Some (Noda.now ()) |} + | false -> () } /// Save a user -- 2.45.1 From 3399a19ac8d1efefb82abfba524e21c6b6ebd349 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 20 Feb 2023 16:51:00 -0500 Subject: [PATCH 11/17] 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 @@ - - - - - - - - - - - - -- 2.45.1 From 5f556b60be42522de117b6c690a6de55a69e3d5a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 20 Feb 2023 17:19:38 -0500 Subject: [PATCH 12/17] Fix admin page list paging (#35) --- src/MyWebLog/Handlers/Page.fs | 7 ++++++- src/admin-theme/page-list.liquid | 6 +++--- src/admin-theme/post-list.liquid | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/MyWebLog/Handlers/Page.fs b/src/MyWebLog/Handlers/Page.fs index 5dee988..6ddeae8 100644 --- a/src/MyWebLog/Handlers/Page.fs +++ b/src/MyWebLog/Handlers/Page.fs @@ -12,9 +12,14 @@ let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task { return! hashForPage "Pages" |> withAntiCsrf ctx - |> addToHash "pages" (pages |> List.map (DisplayPage.fromPageMinimal ctx.WebLog)) + |> addToHash "pages" (pages + |> Seq.ofList + |> Seq.truncate 25 + |> Seq.map (DisplayPage.fromPageMinimal ctx.WebLog) + |> List.ofSeq) |> addToHash "page_nbr" pageNbr |> addToHash "prev_page" (if pageNbr = 2 then "" else $"/page/{pageNbr - 1}") + |> addToHash "has_next" (List.length pages > 25) |> addToHash "next_page" $"/page/{pageNbr + 1}" |> adminView "page-list" next ctx } diff --git a/src/admin-theme/page-list.liquid b/src/admin-theme/page-list.liquid index 1a38b51..f22871f 100644 --- a/src/admin-theme/page-list.liquid +++ b/src/admin-theme/page-list.liquid @@ -6,7 +6,7 @@ {%- assign title_col = "col-12 col-md-5" -%} {%- assign link_col = "col-12 col-md-5" -%} {%- assign upd8_col = "col-12 col-md-2" -%} -
+
@@ -49,7 +49,7 @@
{%- endfor %} - {% if page_nbr > 1 or page_count == 25 %} + {% if page_nbr > 1 or has_next %}
{% if page_nbr > 1 %} @@ -61,7 +61,7 @@ {% endif %}
- {% if page_count == 25 %} + {% if has_next %}

Next » diff --git a/src/admin-theme/post-list.liquid b/src/admin-theme/post-list.liquid index 7b46939..b597187 100644 --- a/src/admin-theme/post-list.liquid +++ b/src/admin-theme/post-list.liquid @@ -3,7 +3,7 @@ Write a New Post {%- assign post_count = model.posts | size -%} {%- if post_count > 0 %} -

+ {%- assign date_col = "col-xs-12 col-md-3 col-lg-2" -%} {%- assign title_col = "col-xs-12 col-md-7 col-lg-6 col-xl-5 col-xxl-4" -%} -- 2.45.1 From fbb202d334a36f2ac325b182cfc53c824121c84d Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 20 Feb 2023 18:50:03 -0500 Subject: [PATCH 13/17] Use static data source in cache - Provide instructions for v2-rc to v2 Postgres migration --- src/MyWebLog.Data/Postgres/PostgresCache.fs | 94 ++++++++------------- src/MyWebLog.Data/PostgresData.fs | 19 ++++- src/MyWebLog/Program.fs | 6 +- 3 files changed, 55 insertions(+), 64 deletions(-) diff --git a/src/MyWebLog.Data/Postgres/PostgresCache.fs b/src/MyWebLog.Data/Postgres/PostgresCache.fs index 8b706a6..44b7071 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCache.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCache.fs @@ -2,6 +2,7 @@ namespace MyWebLog.Data.Postgres open System.Threading open System.Threading.Tasks +open BitBadger.Npgsql.FSharp.Documents open Microsoft.Extensions.Caching.Distributed open NodaTime open Npgsql.FSharp @@ -39,35 +40,30 @@ module private Helpers = typedParam "expireAt" -open Npgsql - /// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog -type DistributedCache (dataSource : NpgsqlDataSource) = +type DistributedCache () = // ~~~ INITIALIZATION ~~~ do task { let! exists = - Sql.fromDataSource dataSource + Configuration.dataSource () + |> Sql.fromDataSource |> Sql.query $" SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session') AS {existsName}" |> Sql.executeRowAsync Map.toExists if not exists then - let! _ = - Sql.fromDataSource dataSource - |> Sql.query + do! Custom.nonQuery "CREATE TABLE session ( id TEXT NOT NULL PRIMARY KEY, payload BYTEA NOT NULL, expire_at TIMESTAMPTZ NOT NULL, sliding_expiration INTERVAL, absolute_expiration TIMESTAMPTZ); - CREATE INDEX idx_session_expiration ON session (expire_at)" - |> Sql.executeNonQueryAsync - () + CREATE INDEX idx_session_expiration ON session (expire_at)" [] } |> sync // ~~~ SUPPORT FUNCTIONS ~~~ @@ -76,16 +72,13 @@ type DistributedCache (dataSource : NpgsqlDataSource) = let getEntry key = backgroundTask { let idParam = "@id", Sql.string key let! tryEntry = - Sql.fromDataSource dataSource - |> Sql.query "SELECT * FROM session WHERE id = @id" - |> Sql.parameters [ idParam ] - |> Sql.executeAsync (fun row -> - { Id = row.string "id" - Payload = row.bytea "payload" - ExpireAt = row.fieldValue "expire_at" - SlidingExpiration = row.fieldValueOrNone "sliding_expiration" - AbsoluteExpiration = row.fieldValueOrNone "absolute_expiration" }) - |> tryHead + Custom.single "SELECT * FROM session WHERE id = @id" [ idParam ] + (fun row -> + { Id = row.string "id" + Payload = row.bytea "payload" + ExpireAt = row.fieldValue "expire_at" + SlidingExpiration = row.fieldValueOrNone "sliding_expiration" + AbsoluteExpiration = row.fieldValueOrNone "absolute_expiration" }) match tryEntry with | Some entry -> let now = getNow () @@ -98,11 +91,8 @@ type DistributedCache (dataSource : NpgsqlDataSource) = true, { entry with ExpireAt = absExp } else true, { entry with ExpireAt = now.Plus slideExp } if needsRefresh then - let! _ = - Sql.fromDataSource dataSource - |> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id" - |> Sql.parameters [ expireParam item.ExpireAt; idParam ] - |> Sql.executeNonQueryAsync + do! Custom.nonQuery "UPDATE session SET expire_at = @expireAt WHERE id = @id" + [ expireParam item.ExpireAt; idParam ] () return if item.ExpireAt > now then Some entry else None | None -> return None @@ -115,26 +105,16 @@ type DistributedCache (dataSource : NpgsqlDataSource) = let purge () = backgroundTask { let now = getNow () if lastPurge.Plus (Duration.FromMinutes 30L) < now then - let! _ = - Sql.fromDataSource dataSource - |> Sql.query "DELETE FROM session WHERE expire_at < @expireAt" - |> Sql.parameters [ expireParam now ] - |> Sql.executeNonQueryAsync + do! Custom.nonQuery "DELETE FROM session WHERE expire_at < @expireAt" [ expireParam now ] lastPurge <- now } /// Remove a cache entry - let removeEntry key = backgroundTask { - let! _ = - Sql.fromDataSource dataSource - |> Sql.query "DELETE FROM session WHERE id = @id" - |> Sql.parameters [ "@id", Sql.string key ] - |> Sql.executeNonQueryAsync - () - } + let removeEntry key = + Delete.byId "session" key /// Save an entry - let saveEntry (opts : DistributedCacheEntryOptions) key payload = backgroundTask { + let saveEntry (opts : DistributedCacheEntryOptions) key payload = let now = getNow () let expireAt, slideExp, absExp = if opts.SlidingExpiration.HasValue then @@ -150,27 +130,21 @@ type DistributedCache (dataSource : NpgsqlDataSource) = // Default to 1 hour sliding expiration let slide = Duration.FromHours 1 now.Plus slide, Some slide, None - let! _ = - Sql.fromDataSource dataSource - |> Sql.query - "INSERT INTO session ( - id, payload, expire_at, sliding_expiration, absolute_expiration - ) VALUES ( - @id, @payload, @expireAt, @slideExp, @absExp - ) ON CONFLICT (id) DO UPDATE - SET payload = EXCLUDED.payload, - expire_at = EXCLUDED.expire_at, - sliding_expiration = EXCLUDED.sliding_expiration, - absolute_expiration = EXCLUDED.absolute_expiration" - |> Sql.parameters - [ "@id", Sql.string key - "@payload", Sql.bytea payload - expireParam expireAt - optParam "slideExp" slideExp - optParam "absExp" absExp ] - |> Sql.executeNonQueryAsync - () - } + Custom.nonQuery + "INSERT INTO session ( + id, payload, expire_at, sliding_expiration, absolute_expiration + ) VALUES ( + @id, @payload, @expireAt, @slideExp, @absExp + ) ON CONFLICT (id) DO UPDATE + SET payload = EXCLUDED.payload, + expire_at = EXCLUDED.expire_at, + sliding_expiration = EXCLUDED.sliding_expiration, + absolute_expiration = EXCLUDED.absolute_expiration" + [ "@id", Sql.string key + "@payload", Sql.bytea payload + expireParam expireAt + optParam "slideExp" slideExp + optParam "absExp" absExp ] // ~~~ IMPLEMENTATION FUNCTIONS ~~~ diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index 6bee4e6..a6987d1 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -1,8 +1,8 @@ namespace MyWebLog.Data -open Microsoft.Extensions.Logging open BitBadger.Npgsql.Documents open BitBadger.Npgsql.FSharp.Documents +open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data.Postgres open Newtonsoft.Json @@ -139,6 +139,23 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : let migrate version = backgroundTask { match version with | Some "v2-rc2" -> () + | Some "v2" -> + printfn "** MANUAL DATABASE UPGRADE REQUIRED **\n" + printfn "The data structure for PostgreSQL changed significantly between v2-rc2 and v2." + printfn "To migrate your data:" + printfn " - Using a v2-rc2 executable, back up each web log" + printfn " - Drop all tables from the database" + printfn " - Using this executable, restore each backup" + + let! webLogs = + Configuration.dataSource () + |> Sql.fromDataSource + |> Sql.query $"SELECT url_base FROM {Table.WebLog}" + |> Sql.executeAsync (fun row -> row.string "url_base") + + printfn "\nCommands to back up all web logs:" + webLogs |> List.iter (printfn "myWebLog backup %s") + exit 1 // Future versions will be inserted here | Some _ | None -> diff --git a/src/MyWebLog/Program.fs b/src/MyWebLog/Program.fs index 0ea8258..f114259 100644 --- a/src/MyWebLog/Program.fs +++ b/src/MyWebLog/Program.fs @@ -10,7 +10,7 @@ type WebLogMiddleware (next : RequestDelegate, log : ILogger) /// Is the debug level enabled on the logger? let isDebug = log.IsEnabled LogLevel.Debug - member this.InvokeAsync (ctx : HttpContext) = task { + member _.InvokeAsync (ctx : HttpContext) = task { /// Create the full path of the request let path = $"{ctx.Request.Scheme}://{ctx.Request.Host.Value}{ctx.Request.Path.Value}" match WebLogCache.tryGet path with @@ -165,8 +165,8 @@ let rec main args = DataImplementation.createNpgsqlDataSource (sp.GetRequiredService ())) let _ = builder.Services.AddSingleton postgres let _ = - builder.Services.AddSingleton (fun sp -> - Postgres.DistributedCache (sp.GetRequiredService ()) :> IDistributedCache) + builder.Services.AddSingleton (fun _ -> + Postgres.DistributedCache () :> IDistributedCache) () | _ -> () -- 2.45.1 From 1562699c379504266cdb7c42825c5c9a6fe3860e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 20 Feb 2023 21:07:43 -0500 Subject: [PATCH 14/17] Add v2-rc2 to v2 db migrations - Bump versions in settings and themes - Convert FAKE build from script to project --- .config/dotnet-tools.json | 12 -- build.fsx => build.fs | 83 ++++++----- build.fsproj | 20 +++ build.fsx.lock | 227 ----------------------------- fake.cmd | 2 - fake.sh | 7 - src/Directory.Build.props | 1 - src/MyWebLog.Data/PostgresData.fs | 46 +++--- src/MyWebLog.Data/RethinkDbData.fs | 9 +- src/MyWebLog.Data/SQLiteData.fs | 9 +- src/MyWebLog.Data/Utils.fs | 2 +- src/MyWebLog/appsettings.json | 2 +- src/admin-theme/version.txt | 2 +- src/default-theme/version.txt | 2 +- 14 files changed, 113 insertions(+), 311 deletions(-) delete mode 100644 .config/dotnet-tools.json rename build.fsx => build.fs (78%) create mode 100644 build.fsproj delete mode 100644 build.fsx.lock delete mode 100644 fake.cmd delete mode 100755 fake.sh diff --git a/.config/dotnet-tools.json b/.config/dotnet-tools.json deleted file mode 100644 index 37fc584..0000000 --- a/.config/dotnet-tools.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "version": 1, - "isRoot": true, - "tools": { - "fake-cli": { - "version": "5.22.0", - "commands": [ - "fake" - ] - } - } -} \ No newline at end of file diff --git a/build.fsx b/build.fs similarity index 78% rename from build.fsx rename to build.fs index c2521a7..b56d513 100644 --- a/build.fsx +++ b/build.fs @@ -1,17 +1,11 @@ -#r "paket: -nuget Fake.DotNet.Cli -nuget Fake.IO.FileSystem -nuget Fake.IO.Zip -nuget Fake.Core.Target //" -#load ".fake/build.fsx/intellisense.fsx" open System.IO open Fake.Core open Fake.DotNet open Fake.IO open Fake.IO.Globbing.Operators -open Fake.Core.TargetOperators -Target.initEnvironment () +let execContext = Context.FakeExecutionContext.Create false "build.fsx" [] +Context.setExecutionContext (Context.RuntimeContext.Fake execContext) /// The output directory for release ZIPs let releasePath = "releases" @@ -105,43 +99,56 @@ Target.create "RemoveThemeArchives" (fun _ -> Target.create "CI" ignore -"Clean" - ==> "All" +open Fake.Core.TargetOperators -"Clean" - ?=> "Build" - ==> "All" +let dependencies = [ + "Clean" + ==> "All" -"Clean" - ?=> "ZipDefaultTheme" - ==> "All" + "Clean" + ?=> "Build" + ==> "All" -"Clean" - ?=> "ZipAdminTheme" - ==> "All" + "Clean" + ?=> "ZipDefaultTheme" + ==> "All" -"Build" - ==> "PublishWindows" - ==> "All" + "Clean" + ?=> "ZipAdminTheme" + ==> "All" -"Build" - ==> "PublishLinux" - ==> "All" + "Build" + ==> "PublishWindows" + ==> "All" -"PublishWindows" - ==> "PackageWindows" - ==> "All" + "Build" + ==> "PublishLinux" + ==> "All" -"PublishLinux" - ==> "PackageLinux" - ==> "All" + "PublishWindows" + ==> "PackageWindows" + ==> "All" -"PackageLinux" - ==> "RepackageLinux" - ==> "All" + "PublishLinux" + ==> "PackageLinux" + ==> "All" -"All" - ==> "RemoveThemeArchives" - ==> "CI" + "PackageLinux" + ==> "RepackageLinux" + ==> "All" -Target.runOrDefault "All" + "All" + ==> "RemoveThemeArchives" + ==> "CI" +] + +[] +let main args = + try + match args with + | [| target |] -> Target.runOrDefault target + | _ -> Target.runOrDefault "All" + 0 + with e -> + printfn "%A" e + 1 diff --git a/build.fsproj b/build.fsproj new file mode 100644 index 0000000..449dd30 --- /dev/null +++ b/build.fsproj @@ -0,0 +1,20 @@ + + + + Exe + net7.0 + + + + + + + + + + + + + + + diff --git a/build.fsx.lock b/build.fsx.lock deleted file mode 100644 index 610ff8f..0000000 --- a/build.fsx.lock +++ /dev/null @@ -1,227 +0,0 @@ -STORAGE: NONE -RESTRICTION: || (== net6.0) (== netstandard2.0) -NUGET - remote: https://api.nuget.org/v3/index.json - BlackFox.VsWhere (1.1) - FSharp.Core (>= 4.2.3) - Microsoft.Win32.Registry (>= 4.7) - Fake.Core.CommandLineParsing (5.22) - FParsec (>= 1.1.1) - FSharp.Core (>= 6.0) - Fake.Core.Context (5.22) - FSharp.Core (>= 6.0) - Fake.Core.Environment (5.22) - FSharp.Core (>= 6.0) - Fake.Core.FakeVar (5.22) - Fake.Core.Context (>= 5.22) - FSharp.Core (>= 6.0) - Fake.Core.Process (5.22) - Fake.Core.Environment (>= 5.22) - Fake.Core.FakeVar (>= 5.22) - Fake.Core.String (>= 5.22) - Fake.Core.Trace (>= 5.22) - Fake.IO.FileSystem (>= 5.22) - FSharp.Core (>= 6.0) - System.Collections.Immutable (>= 5.0) - Fake.Core.SemVer (5.22) - FSharp.Core (>= 6.0) - Fake.Core.String (5.22) - FSharp.Core (>= 6.0) - Fake.Core.Target (5.22) - Fake.Core.CommandLineParsing (>= 5.22) - Fake.Core.Context (>= 5.22) - Fake.Core.Environment (>= 5.22) - Fake.Core.FakeVar (>= 5.22) - Fake.Core.Process (>= 5.22) - Fake.Core.String (>= 5.22) - Fake.Core.Trace (>= 5.22) - FSharp.Control.Reactive (>= 5.0.2) - FSharp.Core (>= 6.0) - Fake.Core.Tasks (5.22) - Fake.Core.Trace (>= 5.22) - FSharp.Core (>= 6.0) - Fake.Core.Trace (5.22) - Fake.Core.Environment (>= 5.22) - Fake.Core.FakeVar (>= 5.22) - FSharp.Core (>= 6.0) - Fake.Core.Xml (5.22) - Fake.Core.String (>= 5.22) - FSharp.Core (>= 6.0) - Fake.DotNet.Cli (5.22) - Fake.Core.Environment (>= 5.22) - Fake.Core.Process (>= 5.22) - Fake.Core.String (>= 5.22) - Fake.Core.Trace (>= 5.22) - Fake.DotNet.MSBuild (>= 5.22) - Fake.DotNet.NuGet (>= 5.22) - Fake.IO.FileSystem (>= 5.22) - FSharp.Core (>= 6.0) - Mono.Posix.NETStandard (>= 1.0) - Newtonsoft.Json (>= 13.0.1) - Fake.DotNet.MSBuild (5.22) - BlackFox.VsWhere (>= 1.1) - Fake.Core.Environment (>= 5.22) - Fake.Core.Process (>= 5.22) - Fake.Core.String (>= 5.22) - Fake.Core.Trace (>= 5.22) - Fake.IO.FileSystem (>= 5.22) - FSharp.Core (>= 6.0) - MSBuild.StructuredLogger (>= 2.1.545) - Fake.DotNet.NuGet (5.22) - Fake.Core.Environment (>= 5.22) - Fake.Core.Process (>= 5.22) - Fake.Core.SemVer (>= 5.22) - Fake.Core.String (>= 5.22) - Fake.Core.Tasks (>= 5.22) - Fake.Core.Trace (>= 5.22) - Fake.Core.Xml (>= 5.22) - Fake.IO.FileSystem (>= 5.22) - Fake.Net.Http (>= 5.22) - FSharp.Core (>= 6.0) - Newtonsoft.Json (>= 13.0.1) - NuGet.Protocol (>= 5.11) - Fake.IO.FileSystem (5.22) - Fake.Core.String (>= 5.22) - FSharp.Core (>= 6.0) - Fake.IO.Zip (5.22) - Fake.Core.String (>= 5.22) - Fake.IO.FileSystem (>= 5.22) - FSharp.Core (>= 6.0) - Fake.Net.Http (5.22) - Fake.Core.Trace (>= 5.22) - FSharp.Core (>= 6.0) - FParsec (1.1.1) - FSharp.Core (>= 4.3.4) - FSharp.Control.Reactive (5.0.5) - FSharp.Core (>= 4.7.2) - System.Reactive (>= 5.0 < 6.0) - FSharp.Core (6.0.5) - Microsoft.Build (17.2) - Microsoft.Build.Framework (>= 17.2) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - Microsoft.NET.StringTools (>= 1.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - Microsoft.Win32.Registry (>= 4.3) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) - System.Collections.Immutable (>= 5.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - System.Configuration.ConfigurationManager (>= 4.7) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - System.Reflection.Metadata (>= 1.6) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) - System.Security.Principal.Windows (>= 4.7) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) - System.Text.Encoding.CodePages (>= 4.0.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net6.0)) - System.Text.Json (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - System.Threading.Tasks.Dataflow (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - Microsoft.Build.Framework (17.2) - Microsoft.Win32.Registry (>= 4.3) - System.Security.Permissions (>= 4.7) - Microsoft.Build.Tasks.Core (17.2) - Microsoft.Build.Framework (>= 17.2) - Microsoft.Build.Utilities.Core (>= 17.2) - Microsoft.NET.StringTools (>= 1.0) - Microsoft.Win32.Registry (>= 4.3) - System.CodeDom (>= 4.4) - System.Collections.Immutable (>= 5.0) - System.Reflection.Metadata (>= 1.6) - System.Resources.Extensions (>= 4.6) - System.Security.Cryptography.Pkcs (>= 4.7) - System.Security.Cryptography.Xml (>= 4.7) - System.Security.Permissions (>= 4.7) - System.Threading.Tasks.Dataflow (>= 6.0) - Microsoft.Build.Utilities.Core (17.2) - Microsoft.Build.Framework (>= 17.2) - Microsoft.NET.StringTools (>= 1.0) - Microsoft.Win32.Registry (>= 4.3) - System.Collections.Immutable (>= 5.0) - System.Configuration.ConfigurationManager (>= 4.7) - System.Security.Permissions (>= 4.7) - restriction: == netstandard2.0 - System.Text.Encoding.CodePages (>= 4.0.1) - restriction: == netstandard2.0 - Microsoft.NET.StringTools (1.0) - System.Memory (>= 4.5.4) - System.Runtime.CompilerServices.Unsafe (>= 5.0) - Microsoft.NETCore.Platforms (6.0.4) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard1.2)) (&& (== net6.0) (< netstandard1.3)) (&& (== net6.0) (< netstandard1.5)) (== netstandard2.0) - Microsoft.NETCore.Targets (5.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard1.2)) (&& (== net6.0) (< netstandard1.3)) (&& (== net6.0) (< netstandard1.5)) (== netstandard2.0) - Microsoft.Win32.Registry (5.0) - System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monoandroid) (< netstandard1.3)) (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0) - System.Security.AccessControl (>= 5.0) - System.Security.Principal.Windows (>= 5.0) - Microsoft.Win32.SystemEvents (6.0.1) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) - Mono.Posix.NETStandard (1.0) - MSBuild.StructuredLogger (2.1.669) - Microsoft.Build (>= 16.10) - Microsoft.Build.Framework (>= 16.10) - Microsoft.Build.Tasks.Core (>= 16.10) - Microsoft.Build.Utilities.Core (>= 16.10) - Newtonsoft.Json (13.0.1) - NuGet.Common (6.2.1) - NuGet.Frameworks (>= 6.2.1) - NuGet.Configuration (6.2.1) - NuGet.Common (>= 6.2.1) - System.Security.Cryptography.ProtectedData (>= 4.4) - NuGet.Frameworks (6.2.1) - NuGet.Packaging (6.2.1) - Newtonsoft.Json (>= 13.0.1) - NuGet.Configuration (>= 6.2.1) - NuGet.Versioning (>= 6.2.1) - System.Security.Cryptography.Cng (>= 5.0) - System.Security.Cryptography.Pkcs (>= 5.0) - NuGet.Protocol (6.2.1) - NuGet.Packaging (>= 6.2.1) - NuGet.Versioning (6.2.1) - System.Buffers (4.5.1) - restriction: || (&& (== net6.0) (>= monoandroid) (< netstandard1.3)) (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0) - System.CodeDom (6.0) - System.Collections.Immutable (6.0) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) - System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Configuration.ConfigurationManager (6.0) - System.Security.Cryptography.ProtectedData (>= 6.0) - System.Security.Permissions (>= 6.0) - System.Drawing.Common (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) - Microsoft.Win32.SystemEvents (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) - System.Formats.Asn1 (6.0) - System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) - System.Memory (4.5.5) - System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0) - System.Numerics.Vectors (>= 4.4) - restriction: || (&& (== net6.0) (< netcoreapp2.0)) (== netstandard2.0) - System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= monotouch)) (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.0)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.1)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= uap10.1)) (&& (== net6.0) (>= xamarinios)) (&& (== net6.0) (>= xamarinmac)) (&& (== net6.0) (>= xamarintvos)) (&& (== net6.0) (>= xamarinwatchos)) (== netstandard2.0) - System.Numerics.Vectors (4.5) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) - System.Reactive (5.0) - System.Runtime.InteropServices.WindowsRuntime (>= 4.3) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (== netstandard2.0) - System.Threading.Tasks.Extensions (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net472)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0) - System.Reflection.Metadata (6.0.1) - System.Collections.Immutable (>= 6.0) - System.Resources.Extensions (6.0) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) - System.Runtime (4.3.1) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (== netstandard2.0) - Microsoft.NETCore.Platforms (>= 1.1.1) - Microsoft.NETCore.Targets (>= 1.1.3) - System.Runtime.CompilerServices.Unsafe (6.0) - System.Runtime.InteropServices.WindowsRuntime (4.3) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (== netstandard2.0) - System.Runtime (>= 4.3) - System.Security.AccessControl (6.0) - System.Security.Principal.Windows (>= 5.0) - restriction: || (&& (== net6.0) (>= net461)) (== netstandard2.0) - System.Security.Cryptography.Cng (5.0) - System.Formats.Asn1 (>= 5.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.0)) - System.Security.Cryptography.Pkcs (6.0.1) - System.Buffers (>= 4.5.1) - restriction: || (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) - System.Formats.Asn1 (>= 6.0) - System.Memory (>= 4.5.4) - restriction: || (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) - System.Security.Cryptography.Cng (>= 5.0) - restriction: || (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (< netstandard2.1)) (== netstandard2.0) - System.Security.Cryptography.ProtectedData (6.0) - System.Security.Cryptography.Xml (6.0) - System.Memory (>= 4.5.4) - restriction: == netstandard2.0 - System.Security.AccessControl (>= 6.0) - System.Security.Cryptography.Pkcs (>= 6.0) - System.Security.Permissions (6.0) - System.Security.AccessControl (>= 6.0) - System.Windows.Extensions (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) - System.Security.Principal.Windows (5.0) - System.Text.Encoding.CodePages (6.0) - System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Encodings.Web (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Json (6.0.5) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= net472)) (&& (== netstandard2.0) (>= net6.0)) - System.Runtime.CompilerServices.Unsafe (>= 6.0) - System.Text.Encodings.Web (>= 6.0) - System.Threading.Tasks.Dataflow (6.0) - System.Threading.Tasks.Extensions (4.5.4) - restriction: || (&& (== net6.0) (>= net472)) (&& (== net6.0) (< netcoreapp3.1)) (&& (== net6.0) (>= uap10.1)) (== netstandard2.0) - System.Runtime.CompilerServices.Unsafe (>= 4.5.3) - restriction: || (&& (== net6.0) (>= net461)) (&& (== net6.0) (< netcoreapp2.1)) (&& (== net6.0) (< netstandard1.0)) (&& (== net6.0) (< netstandard2.0)) (&& (== net6.0) (>= wp8)) (== netstandard2.0) - System.Windows.Extensions (6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) - System.Drawing.Common (>= 6.0) - restriction: || (== net6.0) (&& (== netstandard2.0) (>= netcoreapp3.1)) diff --git a/fake.cmd b/fake.cmd deleted file mode 100644 index 43536ae..0000000 --- a/fake.cmd +++ /dev/null @@ -1,2 +0,0 @@ -dotnet tool restore -dotnet fake %* \ No newline at end of file diff --git a/fake.sh b/fake.sh deleted file mode 100755 index bc92bc9..0000000 --- a/fake.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/usr/bin/env bash - -set -eu -set -o pipefail - -dotnet tool restore -dotnet fake "$@" \ No newline at end of file diff --git a/src/Directory.Build.props b/src/Directory.Build.props index a53054e..b1811e6 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -5,6 +5,5 @@ 2.0.0.0 2.0.0.0 2.0.0 - rc2 diff --git a/src/MyWebLog.Data/PostgresData.fs b/src/MyWebLog.Data/PostgresData.fs index a6987d1..0650379 100644 --- a/src/MyWebLog.Data/PostgresData.fs +++ b/src/MyWebLog.Data/PostgresData.fs @@ -135,27 +135,37 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger, ser : let setDbVersion version = Custom.nonQuery $"DELETE FROM db_version; INSERT INTO db_version VALUES ('%s{version}')" [] + /// Migrate from v2-rc2 to v2 (manual migration required) + let migrateV2Rc2ToV2 () = backgroundTask { + Utils.logMigrationStep log "v2-rc2 to v2" "Requires user action" + + let! webLogs = + Configuration.dataSource () + |> Sql.fromDataSource + |> Sql.query $"SELECT url_base, slug FROM {Table.WebLog}" + |> Sql.executeAsync (fun row -> row.string "url_base", row.string "slug") + + [ "** MANUAL DATABASE UPGRADE REQUIRED **"; "" + "The data structure for PostgreSQL changed significantly between v2-rc2 and v2." + "To migrate your data:" + " - Use a v2-rc2 executable to back up each web log" + " - Drop all tables from the database" + " - Use this executable to restore each backup"; "" + "Commands to back up all web logs:" + yield! webLogs |> List.map (fun (url, slug) -> sprintf "./myWebLog backup %s v2-rc2.%s.json" url slug) + ] + |> String.concat "\n" + |> log.LogWarning + + log.LogCritical "myWebLog will now exit" + exit 1 + } + /// Do required data migration between versions let migrate version = backgroundTask { match version with - | Some "v2-rc2" -> () - | Some "v2" -> - printfn "** MANUAL DATABASE UPGRADE REQUIRED **\n" - printfn "The data structure for PostgreSQL changed significantly between v2-rc2 and v2." - printfn "To migrate your data:" - printfn " - Using a v2-rc2 executable, back up each web log" - printfn " - Drop all tables from the database" - printfn " - Using this executable, restore each backup" - - let! webLogs = - Configuration.dataSource () - |> Sql.fromDataSource - |> Sql.query $"SELECT url_base FROM {Table.WebLog}" - |> Sql.executeAsync (fun row -> row.string "url_base") - - printfn "\nCommands to back up all web logs:" - webLogs |> List.iter (printfn "myWebLog backup %s") - exit 1 + | Some "v2" -> () + | Some "v2-rc2" -> do! migrateV2Rc2ToV2 () // Future versions will be inserted here | Some _ | None -> diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index c3968d2..92ace6e 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -214,11 +214,18 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger () + | Some v when v = "v2" -> () + | Some v when v = "v2-rc2" -> do! migrateV2Rc2ToV2 () | Some v when v = "v2-rc1" -> do! migrateV2Rc1ToV2Rc2 () | Some _ | None -> diff --git a/src/MyWebLog.Data/SQLiteData.fs b/src/MyWebLog.Data/SQLiteData.fs index 3c3bf91..873945c 100644 --- a/src/MyWebLog.Data/SQLiteData.fs +++ b/src/MyWebLog.Data/SQLiteData.fs @@ -529,11 +529,18 @@ type SQLiteData (conn : SqliteConnection, log : ILogger, ser : JsonS do! setDbVersion "v2-rc2" } + /// Migrate from v2-rc2 to v2 + let migrateV2Rc2ToV2 () = backgroundTask { + Utils.logMigrationStep log "v2-rc2 to v2" "Setting database version; no migration required" + do! setDbVersion "v2" + } + /// Migrate data among versions (up only) let migrate version = backgroundTask { match version with - | Some v when v = "v2-rc2" -> () + | Some v when v = "v2" -> () + | Some v when v = "v2-rc2" -> do! migrateV2Rc2ToV2 () | Some v when v = "v2-rc1" -> do! migrateV2Rc1ToV2Rc2 () | Some _ | None -> diff --git a/src/MyWebLog.Data/Utils.fs b/src/MyWebLog.Data/Utils.fs index 59ad5dc..9f08592 100644 --- a/src/MyWebLog.Data/Utils.fs +++ b/src/MyWebLog.Data/Utils.fs @@ -6,7 +6,7 @@ open MyWebLog open MyWebLog.ViewModels /// The current database version -let currentDbVersion = "v2-rc2" +let currentDbVersion = "v2" /// Create a category hierarchy from the given list of categories let rec orderByHierarchy (cats : Category list) parentId slugBase parentNames = seq { diff --git a/src/MyWebLog/appsettings.json b/src/MyWebLog/appsettings.json index 62fa309..e89af6d 100644 --- a/src/MyWebLog/appsettings.json +++ b/src/MyWebLog/appsettings.json @@ -1,5 +1,5 @@ { - "Generator": "myWebLog 2.0-rc2", + "Generator": "myWebLog 2.0", "Logging": { "LogLevel": { "MyWebLog.Handlers": "Information" diff --git a/src/admin-theme/version.txt b/src/admin-theme/version.txt index 80104df..821af72 100644 --- a/src/admin-theme/version.txt +++ b/src/admin-theme/version.txt @@ -1,2 +1,2 @@ myWebLog Admin -2.0.0-rc2 \ No newline at end of file +2.0.0 \ No newline at end of file diff --git a/src/default-theme/version.txt b/src/default-theme/version.txt index 9757c99..ec5e044 100644 --- a/src/default-theme/version.txt +++ b/src/default-theme/version.txt @@ -1,2 +1,2 @@ myWebLog Default Theme -2.0.0-rc2 \ No newline at end of file +2.0.0 \ No newline at end of file -- 2.45.1 From d1f3442118b7224ff0f4a61a7f5ed9d72640cc0e Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Mon, 20 Feb 2023 21:37:26 -0500 Subject: [PATCH 15/17] Update build to capture net7.0 directories --- build.fs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.fs b/build.fs index b56d513..e5b087a 100644 --- a/build.fs +++ b/build.fs @@ -38,7 +38,7 @@ let publishFor rid (_ : TargetParameter) = /// Package published output for the given runtime ID let packageFor (rid : string) (_ : TargetParameter) = - let path = $"{projectPath}/bin/Release/net6.0/{rid}/publish" + let path = $"{projectPath}/bin/Release/net7.0/{rid}/publish" let prodSettings = $"{path}/appsettings.Production.json" if File.exists prodSettings then File.delete prodSettings [ !! $"{path}/**/*" -- 2.45.1 From 4b4330574683b407c952f8fa9b81a77835d949fc Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 24 Feb 2023 21:51:51 -0500 Subject: [PATCH 16/17] Use scalar for custom count/exists --- src/MyWebLog.Data/MyWebLog.Data.fsproj | 2 +- src/MyWebLog.Data/Postgres/PostgresCache.fs | 9 +++------ .../Postgres/PostgresCategoryData.fs | 16 ++++++---------- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 13 +++++-------- src/MyWebLog.Data/Postgres/PostgresPostData.fs | 8 +------- .../Postgres/PostgresWebLogUserData.fs | 13 +++++-------- 6 files changed, 21 insertions(+), 40 deletions(-) diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index 840751d..031fc79 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -5,7 +5,7 @@ - + diff --git a/src/MyWebLog.Data/Postgres/PostgresCache.fs b/src/MyWebLog.Data/Postgres/PostgresCache.fs index 44b7071..a7b0280 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCache.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCache.fs @@ -48,13 +48,10 @@ type DistributedCache () = do task { let! exists = - Configuration.dataSource () - |> Sql.fromDataSource - |> Sql.query $" - SELECT EXISTS + Custom.scalar + $"SELECT EXISTS (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session') - AS {existsName}" - |> Sql.executeRowAsync Map.toExists + AS {existsName}" [] Map.toExists if not exists then do! Custom.nonQuery "CREATE TABLE session ( diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index ba15a4e..244faed 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -38,19 +38,15 @@ type PostgresCategoryData (log : ILogger) = |> List.ofSeq |> arrayContains (nameof Post.empty.CategoryIds) id let postCount = - Configuration.dataSource () - |> Sql.fromDataSource - |> Sql.query $""" - SELECT COUNT(DISTINCT id) AS {countName} - FROM {Table.Post} - WHERE {Query.whereDataContains "@criteria"} - AND {catIdSql}""" - |> Sql.parameters + Custom.scalar + $"""SELECT COUNT(DISTINCT id) AS {countName} + FROM {Table.Post} + WHERE {Query.whereDataContains "@criteria"} + AND {catIdSql}""" [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} catIdParams - ] - |> Sql.executeRowAsync Map.toCount + ] Map.toCount |> Async.AwaitTask |> Async.RunSynchronously it.Id, postCount) diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 9204ab9..765e669 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -168,14 +168,11 @@ module Document = /// Determine whether a document exists with the given key for the given web log 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"} - ) AS {existsName}""" - |> Sql.parameters [ "@id", Sql.string (keyFunc key); webLogContains webLogId ] - |> Sql.executeRowAsync Map.toExists + Custom.scalar + $""" SELECT EXISTS ( + SELECT 1 FROM %s{table} WHERE id = @id AND {Query.whereDataContains "@criteria"} + ) AS {existsName}""" + [ "@id", Sql.string (keyFunc key); webLogContains webLogId ] Map.toExists /// Find a document by its ID for the given web log let findByIdAndWebLog<'TKey, 'TDoc> table (key : 'TKey) (keyFunc : 'TKey -> string) webLogId = diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index 400c1fd..d3791de 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -38,13 +38,7 @@ type PostgresPostData (log : ILogger) = /// Count posts in a status for the given web log let countByStatus status webLogId = log.LogTrace "Post.countByStatus" - Configuration.dataSource () - |> Sql.fromDataSource - |> Sql.query - $"""SELECT COUNT(id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}""" - |> Sql.parameters - [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString status |} ] - |> Sql.executeRowAsync Map.toCount + Count.byContains Table.Post {| webLogDoc webLogId with Status = PostStatus.toString status |} /// Find a post by its ID for the given web log (excluding revisions) let findById postId webLogId = diff --git a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs index fd15654..80eeee3 100644 --- a/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresWebLogUserData.fs @@ -21,14 +21,11 @@ type PostgresWebLogUserData (log : ILogger) = | Some _ -> let criteria = Query.whereDataContains "@criteria" let! isAuthor = - 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 = userId |} ] - |> Sql.executeRowAsync Map.toExists + Custom.scalar + $" SELECT ( EXISTS (SELECT 1 FROM {Table.Page} WHERE {criteria} + OR EXISTS (SELECT 1 FROM {Table.Post} WHERE {criteria}) + ) AS {existsName}" + [ "@criteria", Query.jsonbDocParam {| AuthorId = userId |} ] Map.toExists if isAuthor then return Error "User has pages or posts; cannot delete" else -- 2.45.1 From e658b073ee56ea14c154e0ab01ca1c56acf2e222 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 26 Feb 2023 12:18:51 -0500 Subject: [PATCH 17/17] Update deps, add .net 6/7 support --- build.fs | 66 +++++++++++++--------- src/Directory.Build.props | 2 +- src/MyWebLog.Data/MyWebLog.Data.fsproj | 12 ++-- src/MyWebLog.Domain/MyWebLog.Domain.fsproj | 4 +- src/MyWebLog/MyWebLog.fsproj | 8 +-- 5 files changed, 52 insertions(+), 40 deletions(-) diff --git a/build.fs b/build.fs index e5b087a..e841ab6 100644 --- a/build.fs +++ b/build.fs @@ -32,24 +32,34 @@ let zipTheme (name : string) (_ : TargetParameter) = |> Seq.filter (fun (_, name) -> not (name.EndsWith ".zip")) |> Zip.zipSpec $"{releasePath}/{name}-theme.zip" +/// Frameworks supported by this build +let frameworks = [ "net6.0"; "net7.0" ] + /// Publish the project for the given runtime ID let publishFor rid (_ : TargetParameter) = - DotNet.publish (fun opts -> { opts with Runtime = Some rid; SelfContained = Some false; NoLogo = true }) projName + frameworks + |> List.iter (fun fwk -> + DotNet.publish + (fun opts -> + { opts with Runtime = Some rid; SelfContained = Some false; NoLogo = true; Framework = Some fwk }) + projName) /// Package published output for the given runtime ID -let packageFor (rid : string) (_ : TargetParameter) = - let path = $"{projectPath}/bin/Release/net7.0/{rid}/publish" - let prodSettings = $"{path}/appsettings.Production.json" - if File.exists prodSettings then File.delete prodSettings - [ !! $"{path}/**/*" - |> Zip.filesAsSpecs path - |> Seq.map (fun (orig, dest) -> - orig, if dest.StartsWith "MyWebLog" then dest.Replace ("MyWebLog", "myWebLog") else dest) - Seq.singleton ($"{releasePath}/admin-theme.zip", "admin-theme.zip") - Seq.singleton ($"{releasePath}/default-theme.zip", "default-theme.zip") - ] - |> Seq.concat - |> Zip.zipSpec $"{releasePath}/myWebLog-{version}.{rid}.zip" +let packageFor rid (_ : TargetParameter) = + frameworks + |> List.iter (fun fwk -> + let path = $"{projectPath}/bin/Release/{fwk}/%s{rid}/publish" + let prodSettings = $"{path}/appsettings.Production.json" + if File.exists prodSettings then File.delete prodSettings + [ !! $"{path}/**/*" + |> Zip.filesAsSpecs path + |> Seq.map (fun (orig, dest) -> + orig, if dest.StartsWith "MyWebLog" then dest.Replace ("MyWebLog", "myWebLog") else dest) + Seq.singleton ($"{releasePath}/admin-theme.zip", "admin-theme.zip") + Seq.singleton ($"{releasePath}/default-theme.zip", "default-theme.zip") + ] + |> Seq.concat + |> Zip.zipSpec $"{releasePath}/myWebLog-{version}.{fwk}.{rid}.zip") Target.create "Clean" (fun _ -> @@ -74,19 +84,21 @@ Target.create "PackageLinux" (packageFor "linux-x64") Target.create "RepackageLinux" (fun _ -> let workDir = $"{releasePath}/linux" - let zipArchive = $"{releasePath}/myWebLog-{version}.linux-x64.zip" - let sh command args = - CreateProcess.fromRawCommand command args - |> CreateProcess.redirectOutput - |> Proc.run - |> ignore - Shell.mkdir workDir - Zip.unzip workDir zipArchive - Shell.cd workDir - sh "chmod" [ "+x"; "./myWebLog" ] - sh "tar" [ "cfj"; $"../myWebLog-{version}.linux-x64.tar.bz2"; "." ] - Shell.cd "../.." - Shell.rm zipArchive + frameworks + |> List.iter (fun fwk -> + let zipArchive = $"{releasePath}/myWebLog-{version}.{fwk}.linux-x64.zip" + let sh command args = + CreateProcess.fromRawCommand command args + |> CreateProcess.redirectOutput + |> Proc.run + |> ignore + Shell.mkdir workDir + Zip.unzip workDir zipArchive + Shell.cd workDir + sh "chmod" [ "+x"; "./myWebLog" ] + sh "tar" [ "cfj"; $"../myWebLog-{version}.{fwk}.linux-x64.tar.bz2"; "." ] + Shell.cd "../.." + Shell.rm zipArchive) Shell.rm_rf workDir ) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index b1811e6..5529e72 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,6 +1,6 @@ - net7.0 + net6.0;net7.0 embedded 2.0.0.0 2.0.0.0 diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj index 031fc79..1f1cf76 100644 --- a/src/MyWebLog.Data/MyWebLog.Data.fsproj +++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj @@ -6,13 +6,13 @@ - - - + + + - - - + + + diff --git a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj index 83c76c1..9511caa 100644 --- a/src/MyWebLog.Domain/MyWebLog.Domain.fsproj +++ b/src/MyWebLog.Domain/MyWebLog.Domain.fsproj @@ -7,8 +7,8 @@ - - + + diff --git a/src/MyWebLog/MyWebLog.fsproj b/src/MyWebLog/MyWebLog.fsproj index 646f1b1..78ad373 100644 --- a/src/MyWebLog/MyWebLog.fsproj +++ b/src/MyWebLog/MyWebLog.fsproj @@ -23,13 +23,13 @@ - + - - + + - + -- 2.45.1