WIP on PostgreSQL doc implementation
This commit is contained in:
		
							parent
							
								
									0fd1760fa4
								
							
						
					
					
						commit
						e09638d8fd
					
				| @ -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, | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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<Category> | ||||
|         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<CategoryId, Category> source Table.Category catId CategoryId.toString webLogId | ||||
|      | ||||
|     /// Find all categories for the given web log | ||||
|     let findByWebLog webLogId = | ||||
|         log.LogTrace "Category.findByWebLog" | ||||
|         Document.findByWebLog<Category> 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<Post> | ||||
|             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 [ | ||||
|  | ||||
| @ -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 | ||||
|  | ||||
| @ -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<Page> 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<Page> | ||||
|      | ||||
|     /// 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<PageId, Page> 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<Page> 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<Page> 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<Page> | ||||
|      | ||||
|     /// 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 | ||||
|  | ||||
| @ -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<PostId, Post> 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<Post> | ||||
| @ -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<Post> 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<Post> | ||||
|      | ||||
|     /// Get a page of posts for the given web log (excludes text and revisions) | ||||
|     let findPageOfPosts webLogId pageNbr postsPerPage = | ||||
|         log.LogTrace "Post.findPageOfPosts" | ||||
|         Sql.fromDataSource source | ||||
|         |> Sql.query $" | ||||
|             {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<Post> | ||||
|      | ||||
|     /// 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<Post> | ||||
|         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<Post> | ||||
|         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 | ||||
|  | ||||
| @ -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<TagMapId, TagMap> 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<TagMap> | ||||
|         |> 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<TagMap> | ||||
|      | ||||
|     /// 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<TagMap> | ||||
|      | ||||
|     /// 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) | ||||
|             ] | ||||
|         () | ||||
|     } | ||||
|  | ||||
| @ -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<Theme> 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 | ||||
|  | ||||
| @ -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 ] | ||||
|             () | ||||
|     } | ||||
|      | ||||
|  | ||||
| @ -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<WebLog> 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<WebLog> | ||||
|         |> tryHead | ||||
|      | ||||
|     /// Find a web log by its ID | ||||
|     let findById webLogId =  | ||||
|         log.LogTrace "WebLog.findById" | ||||
|         Sql.fromDataSource source | ||||
|         |> Query.tryById<WebLog> 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 | ||||
|  | ||||
| @ -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<WebLogUserId, WebLogUser> | ||||
|             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<WebLogUser> | ||||
|         |> 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<WebLogUser> | ||||
|      | ||||
|     /// Find the names of users by their IDs for the given web log | ||||
|     let findNames webLogId userIds = backgroundTask { | ||||
|         log.LogTrace "WebLogUser.findNames" | ||||
|         let idSql, idParams = inClause "AND id" "id" WebLogUserId.toString userIds | ||||
|         let! users = | ||||
|             Sql.fromDataSource source | ||||
|             |> Sql.query $"{userByCriteria} {idSql}" | ||||
|             |> Sql.query $"{selectWithCriteria Table.WebLogUser} {idSql}" | ||||
|             |> Sql.parameters (webLogContains webLogId :: idParams) | ||||
|             |> Sql.executeAsync fromData<WebLogUser> | ||||
|         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 | ||||
|  | ||||
| @ -152,19 +152,20 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger<PostgresData>, 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 = | ||||
|  | ||||
| @ -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<ILogger<PostgresData>> () | ||||
|             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<IData> postgres | ||||
|         let _ = | ||||
|             builder.Services.AddSingleton<IDistributedCache> (fun sp -> | ||||
|                 Postgres.DistributedCache ((sp.GetRequiredService<IConfiguration> ()).GetConnectionString "PostgreSQL") | ||||
|                 :> IDistributedCache) | ||||
|                 Postgres.DistributedCache (sp.GetRequiredService<NpgsqlDataSource> ()) :> IDistributedCache) | ||||
|         () | ||||
|     | _ -> () | ||||
|      | ||||
|  | ||||
| @ -2,8 +2,7 @@ | ||||
|   "Generator": "myWebLog 2.0-rc2", | ||||
|   "Logging": { | ||||
|     "LogLevel": { | ||||
|       "MyWebLog.Handlers": "Information", | ||||
|       "MyWebLog.Data": "Trace" | ||||
|       "MyWebLog.Handlers": "Information" | ||||
|     } | ||||
|   } | ||||
| } | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user