V2 #36

Merged
danieljsummers merged 17 commits from v2-out-the-door into main 2023-02-26 18:01:21 +00:00
14 changed files with 203 additions and 168 deletions
Showing only changes of commit e09638d8fd - Show all commits

View File

@ -165,6 +165,7 @@ module Json =
Converters = ser.Converters, Converters = ser.Converters,
DefaultValueHandling = ser.DefaultValueHandling, DefaultValueHandling = ser.DefaultValueHandling,
DateFormatHandling = ser.DateFormatHandling, DateFormatHandling = ser.DateFormatHandling,
DateParseHandling = ser.DateParseHandling,
MetadataPropertyHandling = ser.MetadataPropertyHandling, MetadataPropertyHandling = ser.MetadataPropertyHandling,
MissingMemberHandling = ser.MissingMemberHandling, MissingMemberHandling = ser.MissingMemberHandling,
NullValueHandling = ser.NullValueHandling, NullValueHandling = ser.NullValueHandling,

View File

@ -39,15 +39,17 @@ module private Helpers =
typedParam "expireAt" typedParam "expireAt"
open Npgsql
/// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog /// A distributed cache implementation in PostgreSQL used to handle sessions for myWebLog
type DistributedCache (connStr : string) = type DistributedCache (dataSource : NpgsqlDataSource) =
// ~~~ INITIALIZATION ~~~ // ~~~ INITIALIZATION ~~~
do do
task { task {
let! exists = let! exists =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query $" |> Sql.query $"
SELECT EXISTS SELECT EXISTS
(SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session') (SELECT 1 FROM pg_tables WHERE schemaname = 'public' AND tablename = 'session')
@ -55,7 +57,7 @@ type DistributedCache (connStr : string) =
|> Sql.executeRowAsync Map.toExists |> Sql.executeRowAsync Map.toExists
if not exists then if not exists then
let! _ = let! _ =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query |> Sql.query
"CREATE TABLE session ( "CREATE TABLE session (
id TEXT NOT NULL PRIMARY KEY, id TEXT NOT NULL PRIMARY KEY,
@ -74,7 +76,7 @@ type DistributedCache (connStr : string) =
let getEntry key = backgroundTask { let getEntry key = backgroundTask {
let idParam = "@id", Sql.string key let idParam = "@id", Sql.string key
let! tryEntry = let! tryEntry =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query "SELECT * FROM session WHERE id = @id" |> Sql.query "SELECT * FROM session WHERE id = @id"
|> Sql.parameters [ idParam ] |> Sql.parameters [ idParam ]
|> Sql.executeAsync (fun row -> |> Sql.executeAsync (fun row ->
@ -97,7 +99,7 @@ type DistributedCache (connStr : string) =
else true, { entry with ExpireAt = now.Plus slideExp } else true, { entry with ExpireAt = now.Plus slideExp }
if needsRefresh then if needsRefresh then
let! _ = let! _ =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id" |> Sql.query "UPDATE session SET expire_at = @expireAt WHERE id = @id"
|> Sql.parameters [ expireParam item.ExpireAt; idParam ] |> Sql.parameters [ expireParam item.ExpireAt; idParam ]
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
@ -114,7 +116,7 @@ type DistributedCache (connStr : string) =
let now = getNow () let now = getNow ()
if lastPurge.Plus (Duration.FromMinutes 30L) < now then if lastPurge.Plus (Duration.FromMinutes 30L) < now then
let! _ = let! _ =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query "DELETE FROM session WHERE expire_at < @expireAt" |> Sql.query "DELETE FROM session WHERE expire_at < @expireAt"
|> Sql.parameters [ expireParam now ] |> Sql.parameters [ expireParam now ]
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
@ -124,7 +126,7 @@ type DistributedCache (connStr : string) =
/// Remove a cache entry /// Remove a cache entry
let removeEntry key = backgroundTask { let removeEntry key = backgroundTask {
let! _ = let! _ =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query "DELETE FROM session WHERE id = @id" |> Sql.query "DELETE FROM session WHERE id = @id"
|> Sql.parameters [ "@id", Sql.string key ] |> Sql.parameters [ "@id", Sql.string key ]
|> Sql.executeNonQueryAsync |> Sql.executeNonQueryAsync
@ -149,7 +151,7 @@ type DistributedCache (connStr : string) =
let slide = Duration.FromHours 1 let slide = Duration.FromHours 1
now.Plus slide, Some slide, None now.Plus slide, Some slide, None
let! _ = let! _ =
Sql.connect connStr Sql.fromDataSource dataSource
|> Sql.query |> Sql.query
"INSERT INTO session ( "INSERT INTO session (
id, payload, expire_at, sliding_expiration, absolute_expiration id, payload, expire_at, sliding_expiration, absolute_expiration

View File

@ -1,5 +1,6 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Npgsql open Npgsql
@ -7,26 +8,26 @@ open Npgsql.FSharp
open Npgsql.FSharp.Documents open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog category data implementation /// PostgreSQL myWebLog category data implementation
type PostgresCategoryData (source : NpgsqlDataSource) = type PostgresCategoryData (source : NpgsqlDataSource, log : ILogger) =
/// Count all categories for the given web log /// Count all categories for the given web log
let countAll webLogId = let countAll webLogId =
log.LogTrace "Category.countAll"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.countByContains Table.Category (webLogDoc webLogId) |> Query.countByContains Table.Category (webLogDoc webLogId)
/// Count all top-level categories for the given web log /// Count all top-level categories for the given web log
let countTopLevel webLogId = let countTopLevel webLogId =
log.LogTrace "Category.countTopLevel"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.countByContains Table.Category {| webLogDoc webLogId with ParentId = None |} |> Query.countByContains Table.Category {| webLogDoc webLogId with ParentId = None |}
/// Retrieve all categories for the given web log in a DotLiquid-friendly format /// Retrieve all categories for the given web log in a DotLiquid-friendly format
let findAllForView webLogId = backgroundTask { let findAllForView webLogId = backgroundTask {
log.LogTrace "Category.findAllForView"
let! cats = let! cats =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $""" |> Sql.query $"{selectWithCriteria Table.Category} ORDER BY LOWER(data ->> '{nameof Category.empty.Name}')"
{Query.selectFromTable Table.Category}
WHERE {Query.whereDataContains "@criteria"}
ORDER BY LOWER(data->>'{nameof Category.empty.Name}')"""
|> Sql.parameters [ webLogContains webLogId ] |> Sql.parameters [ webLogContains webLogId ]
|> Sql.executeAsync fromData<Category> |> Sql.executeAsync fromData<Category>
let ordered = Utils.orderByHierarchy cats None None [] let ordered = Utils.orderByHierarchy cats None None []
@ -40,18 +41,19 @@ type PostgresCategoryData (source : NpgsqlDataSource) =
|> Seq.map (fun cat -> cat.Id) |> Seq.map (fun cat -> cat.Id)
|> Seq.append (Seq.singleton it.Id) |> Seq.append (Seq.singleton it.Id)
|> List.ofSeq |> List.ofSeq
|> jsonArrayInClause (nameof Post.empty.CategoryIds) id |> arrayContains (nameof Post.empty.CategoryIds) id
let postCount = let postCount =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $""" |> Sql.query $"""
SELECT COUNT(DISTINCT id) AS {countName} SELECT COUNT(DISTINCT id) AS {countName}
FROM {Table.Post} FROM {Table.Post}
WHERE {Query.whereDataContains "@criteria"} WHERE {Query.whereDataContains "@criteria"}
AND ({catIdSql})""" AND {catIdSql}"""
|> Sql.parameters ( |> Sql.parameters
("@criteria", [ "@criteria",
Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}) Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
:: catIdParams) catIdParams
]
|> Sql.executeRowAsync Map.toCount |> Sql.executeRowAsync Map.toCount
|> Async.AwaitTask |> Async.AwaitTask
|> Async.RunSynchronously |> Async.RunSynchronously
@ -70,10 +72,12 @@ type PostgresCategoryData (source : NpgsqlDataSource) =
} }
/// Find a category by its ID for the given web log /// Find a category by its ID for the given web log
let findById catId webLogId = let findById catId webLogId =
log.LogTrace "Category.findById"
Document.findByIdAndWebLog<CategoryId, Category> source Table.Category catId CategoryId.toString webLogId Document.findByIdAndWebLog<CategoryId, Category> source Table.Category catId CategoryId.toString webLogId
/// Find all categories for the given web log /// Find all categories for the given web log
let findByWebLog webLogId = let findByWebLog webLogId =
log.LogTrace "Category.findByWebLog"
Document.findByWebLog<Category> source Table.Category webLogId Document.findByWebLog<Category> source Table.Category webLogId
/// Create parameters for a category insert / update /// Create parameters for a category insert / update
@ -82,6 +86,7 @@ type PostgresCategoryData (source : NpgsqlDataSource) =
/// Delete a category /// Delete a category
let delete catId webLogId = backgroundTask { let delete catId webLogId = backgroundTask {
log.LogTrace "Category.delete"
match! findById catId webLogId with match! findById catId webLogId with
| Some cat -> | Some cat ->
// Reassign any children to the category's parent category // 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 // Delete the category off all posts where it is assigned
let! posts = let! posts =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT data FROM {Table.Post} WHERE data->'{nameof Post.empty.CategoryIds}' ? @id" |> Sql.query $"SELECT data FROM {Table.Post} WHERE data -> '{nameof Post.empty.CategoryIds}' @> @id"
|> Sql.parameters [ "@id", Sql.jsonb (CategoryId.toString catId) ] |> Sql.parameters [ "@id", Query.jsonbDocParam [| CategoryId.toString catId |] ]
|> Sql.executeAsync fromData<Post> |> Sql.executeAsync fromData<Post>
if not (List.isEmpty posts) then if not (List.isEmpty posts) then
let! _ = let! _ =
@ -125,11 +130,13 @@ type PostgresCategoryData (source : NpgsqlDataSource) =
/// Save a category /// Save a category
let save (cat : Category) = backgroundTask { let save (cat : Category) = backgroundTask {
log.LogTrace "Category.save"
do! Sql.fromDataSource source |> Query.save Table.Category (CategoryId.toString cat.Id) cat do! Sql.fromDataSource source |> Query.save Table.Category (CategoryId.toString cat.Id) cat
} }
/// Restore categories from a backup /// Restore categories from a backup
let restore cats = backgroundTask { let restore cats = backgroundTask {
log.LogTrace "Category.restore"
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.executeTransactionAsync [ |> Sql.executeTransactionAsync [

View File

@ -86,6 +86,10 @@ let countName = "the_count"
/// The name of the field to select to be able to use Map.toExists /// The name of the field to select to be able to use Map.toExists
let existsName = "does_exist" 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 /// Create the SQL and parameters for an IN clause
let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : 'T list) = let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : 'T list) =
if List.isEmpty items then "", [] if List.isEmpty items then "", []
@ -102,22 +106,11 @@ let inClause<'T> colNameAndPrefix paramName (valueFunc: 'T -> string) (items : '
|> Seq.head) |> Seq.head)
|> function sql, ps -> $"{sql})", ps |> function sql, ps -> $"{sql})", ps
/// Create the SQL and parameters for the array-in-JSON equivalent of an IN clause /// Create the SQL and parameters for match-any array query
let jsonArrayInClause<'T> name (valueFunc : 'T -> string) (items : 'T list) = let arrayContains<'T> name (valueFunc : 'T -> string) (items : 'T list) =
if List.isEmpty items then "TRUE = FALSE", [] $"data['{name}'] ?| @{name}Values",
else ($"@{name}Values", Sql.stringArray (items |> List.map valueFunc |> Array.ofList))
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)
/// Get the first result of the given query /// Get the first result of the given query
let tryHead<'T> (query : Task<'T list>) = backgroundTask { let tryHead<'T> (query : Task<'T list>) = backgroundTask {
let! results = query let! results = query

View File

@ -14,59 +14,55 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Append revisions to a page /// Append revisions to a page
let appendPageRevisions (page : Page) = backgroundTask { 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 let! revisions = Revisions.findByEntityId source Table.PageRevision Table.Page page.Id PageId.toString
return { page with Revisions = revisions } return { page with Revisions = revisions }
} }
/// Return a page with no text or revisions /// Return a page with no text or revisions
let pageWithoutText (row : RowReader) = let pageWithoutText (row : RowReader) =
log.LogDebug ("data: {0}", row.string "data")
{ fromData<Page> row with Text = "" } { fromData<Page> row with Text = "" }
/// Update a page's revisions /// Update a page's revisions
let updatePageRevisions pageId oldRevs newRevs = 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 Revisions.update source Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs
/// Does the given page exist? /// Does the given page exist?
let pageExists pageId webLogId = let pageExists pageId webLogId =
log.LogTrace "Page.pageExists"
Document.existsByWebLog source Table.Page pageId PageId.toString 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 // IMPLEMENTATION FUNCTIONS
/// Get all pages for a web log (without text or revisions) /// Get all pages for a web log (without text or revisions)
let all webLogId = let all webLogId =
log.LogTrace "PostgresPageData.all" log.LogTrace "Page.all"
Sql.fromDataSource source 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.parameters [ webLogContains webLogId ]
|> Sql.executeAsync fromData<Page> |> Sql.executeAsync fromData<Page>
/// Count all pages for the given web log /// Count all pages for the given web log
let countAll webLogId = let countAll webLogId =
log.LogTrace "PostgresPageData.countAll" log.LogTrace "Page.countAll"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.countByContains Table.Page (webLogDoc webLogId) |> Query.countByContains Table.Page (webLogDoc webLogId)
/// Count all pages shown in the page list for the given web log /// Count all pages shown in the page list for the given web log
let countListed webLogId = let countListed webLogId =
log.LogTrace "PostgresPageData.countListed" log.LogTrace "Page.countListed"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.countByContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} |> Query.countByContains Table.Page {| webLogDoc webLogId with IsInPageList = true |}
/// Find a page by its ID (without revisions) /// Find a page by its ID (without revisions)
let findById pageId webLogId = let findById pageId webLogId =
log.LogTrace "PostgresPageData.findById" log.LogTrace "Page.findById"
Document.findByIdAndWebLog<PageId, Page> source Table.Page pageId PageId.toString webLogId Document.findByIdAndWebLog<PageId, Page> source Table.Page pageId PageId.toString webLogId
/// Find a complete page by its ID /// Find a complete page by its ID
let findFullById pageId webLogId = backgroundTask { let findFullById pageId webLogId = backgroundTask {
log.LogTrace "PostgresPageData.findFullById" log.LogTrace "Page.findFullById"
match! findById pageId webLogId with match! findById pageId webLogId with
| Some page -> | Some page ->
let! withMore = appendPageRevisions page let! withMore = appendPageRevisions page
@ -76,7 +72,7 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Delete a page by its ID /// Delete a page by its ID
let delete pageId webLogId = backgroundTask { let delete pageId webLogId = backgroundTask {
log.LogTrace "PostgresPageData.delete" log.LogTrace "Page.delete"
match! pageExists pageId webLogId with match! pageExists pageId webLogId with
| true -> | true ->
do! Sql.fromDataSource source |> Query.deleteById Table.Page (PageId.toString pageId) 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 /// Find a page by its permalink for the given web log
let findByPermalink permalink webLogId = let findByPermalink permalink webLogId =
log.LogTrace "PostgresPageData.findByPermalink" log.LogTrace "Page.findByPermalink"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.findByContains<Page> Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} |> Query.findByContains<Page> Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |}
|> tryHead |> tryHead
/// Find the current permalink within a set of potential prior permalinks for the given web log /// Find the current permalink within a set of potential prior permalinks for the given web log
let findCurrentPermalink permalinks webLogId = backgroundTask { let findCurrentPermalink permalinks webLogId = backgroundTask {
log.LogTrace "PostgresPageData.findCurrentPermalink" log.LogTrace "Page.findCurrentPermalink"
if List.isEmpty permalinks then return None if List.isEmpty permalinks then return None
else else
let linkSql, linkParams = let linkSql, linkParam =
jsonArrayInClause (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks arrayContains (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks
return! return!
// TODO: stopped here
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $""" |> Sql.query $"""
SELECT data->>'{nameof Page.empty.Permalink}' AS permalink SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink
FROM page FROM page
WHERE {Query.whereDataContains "@criteria"} WHERE {Query.whereDataContains "@criteria"}
AND ({linkSql})""" AND {linkSql}"""
|> Sql.parameters (webLogContains webLogId :: linkParams) |> Sql.parameters [ webLogContains webLogId; linkParam ]
|> Sql.executeAsync Map.toPermalink |> Sql.executeAsync Map.toPermalink
|> tryHead |> tryHead
} }
/// Get all complete pages for the given web log /// Get all complete pages for the given web log
let findFullByWebLog webLogId = backgroundTask { let findFullByWebLog webLogId = backgroundTask {
log.LogTrace "PostgresPageData.findFullByWebLog" log.LogTrace "Page.findFullByWebLog"
let! pages = Document.findByWebLog<Page> source Table.Page webLogId let! pages = Document.findByWebLog<Page> source Table.Page webLogId
let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId let! revisions = Revisions.findByWebLog source Table.PageRevision Table.Page PageId webLogId
return return
@ -124,30 +119,26 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Get all listed pages for the given web log (without revisions or text) /// Get all listed pages for the given web log (without revisions or text)
let findListed webLogId = let findListed webLogId =
log.LogTrace "PostgresPageData.findListed" log.LogTrace "Page.findListed"
Sql.fromDataSource source 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.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with IsInPageList = true |} ]
|> Sql.executeAsync pageWithoutText |> Sql.executeAsync pageWithoutText
/// Get a page of pages for the given web log (without revisions) /// Get a page of pages for the given web log (without revisions)
let findPageOfPages webLogId pageNbr = let findPageOfPages webLogId pageNbr =
log.LogTrace "PostgresPageData.findPageOfPages" log.LogTrace "Page.findPageOfPages"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $" |> Sql.query $"
{pageByCriteria} {selectWithCriteria Table.Page}
ORDER BY LOWER(data->>'{nameof Page.empty.Title}') ORDER BY LOWER(data->>'{nameof Page.empty.Title}')
LIMIT @pageSize OFFSET @toSkip" LIMIT @pageSize OFFSET @toSkip"
|> Sql.parameters [ webLogContains webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ] |> Sql.parameters [ webLogContains webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ]
|> Sql.executeAsync fromData<Page> |> 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 /// Restore pages from a backup
let restore (pages : Page list) = backgroundTask { 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 revisions = pages |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r))
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
@ -163,16 +154,16 @@ type PostgresPageData (source : NpgsqlDataSource, log : ILogger) =
/// Save a page /// Save a page
let save (page : Page) = backgroundTask { let save (page : Page) = backgroundTask {
log.LogTrace "PostgresPageData.save" log.LogTrace "Page.save"
let! oldPage = findFullById page.Id page.WebLogId 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 do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions
() ()
} }
/// Update a page's prior permalinks /// Update a page's prior permalinks
let updatePriorPermalinks pageId webLogId permalinks = backgroundTask { let updatePriorPermalinks pageId webLogId permalinks = backgroundTask {
log.LogTrace "PostgresPageData.updatePriorPermalinks" log.LogTrace "Page.updatePriorPermalinks"
match! findById pageId webLogId with match! findById pageId webLogId with
| Some page -> | Some page ->
do! Sql.fromDataSource source do! Sql.fromDataSource source

View File

@ -1,19 +1,22 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open NodaTime open NodaTime
open NodaTime.Text
open Npgsql open Npgsql
open Npgsql.FSharp open Npgsql.FSharp
open Npgsql.FSharp.Documents open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog post data implementation /// PostgreSQL myWebLog post data implementation
type PostgresPostData (source : NpgsqlDataSource) = type PostgresPostData (source : NpgsqlDataSource, log : ILogger) =
// SUPPORT FUNCTIONS // SUPPORT FUNCTIONS
/// Append revisions to a post /// Append revisions to a post
let appendPostRevisions (post : Post) = backgroundTask { 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 source Table.PostRevision Table.Post post.Id PostId.toString
return { post with Revisions = revisions } return { post with Revisions = revisions }
} }
@ -24,20 +27,19 @@ type PostgresPostData (source : NpgsqlDataSource) =
/// Update a post's revisions /// Update a post's revisions
let updatePostRevisions postId oldRevs newRevs = let updatePostRevisions postId oldRevs newRevs =
log.LogTrace "Post.updatePostRevisions"
Revisions.update source 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? /// Does the given post exist?
let postExists postId webLogId = let postExists postId webLogId =
log.LogTrace "Post.postExists"
Document.existsByWebLog source Table.Post postId PostId.toString webLogId 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 // IMPLEMENTATION FUNCTIONS
/// Count posts in a status for the given web log /// Count posts in a status for the given web log
let countByStatus status webLogId = let countByStatus status webLogId =
log.LogTrace "Post.countByStatus"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query |> Sql.query
$"""SELECT COUNT(id) AS {countName} FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"}""" $"""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) /// Find a post by its ID for the given web log (excluding revisions)
let findById postId webLogId = let findById postId webLogId =
log.LogTrace "Post.findById"
Document.findByIdAndWebLog<PostId, Post> source Table.Post postId PostId.toString webLogId 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) /// Find a post by its permalink for the given web log (excluding revisions and prior permalinks)
let findByPermalink permalink webLogId = let findByPermalink permalink webLogId =
log.LogTrace "Post.findByPermalink"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query postsByCriteria |> Sql.query (selectWithCriteria Table.Post)
|> Sql.parameters |> Sql.parameters
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} ] [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} ]
|> Sql.executeAsync fromData<Post> |> Sql.executeAsync fromData<Post>
@ -60,6 +64,7 @@ type PostgresPostData (source : NpgsqlDataSource) =
/// Find a complete post by its ID for the given web log /// Find a complete post by its ID for the given web log
let findFullById postId webLogId = backgroundTask { let findFullById postId webLogId = backgroundTask {
log.LogTrace "Post.findFullById"
match! findById postId webLogId with match! findById postId webLogId with
| Some post -> | Some post ->
let! withRevisions = appendPostRevisions post let! withRevisions = appendPostRevisions post
@ -69,6 +74,7 @@ type PostgresPostData (source : NpgsqlDataSource) =
/// Delete a post by its ID for the given web log /// Delete a post by its ID for the given web log
let delete postId webLogId = backgroundTask { let delete postId webLogId = backgroundTask {
log.LogTrace "Post.delete"
match! postExists postId webLogId with match! postExists postId webLogId with
| true -> | true ->
let theId = PostId.toString postId 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 /// Find the current permalink from a list of potential prior permalinks for the given web log
let findCurrentPermalink permalinks webLogId = backgroundTask { let findCurrentPermalink permalinks webLogId = backgroundTask {
log.LogTrace "Post.findCurrentPermalink"
if List.isEmpty permalinks then return None if List.isEmpty permalinks then return None
else else
let linkSql, linkParams = let linkSql, linkParam =
jsonArrayInClause (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks arrayContains (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks
return! return!
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $""" |> Sql.query $"""
SELECT data->>'{nameof Post.empty.Permalink}' AS permalink SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink
FROM {Table.Post} FROM {Table.Post}
WHERE {Query.whereDataContains "@criteria"} WHERE {Query.whereDataContains "@criteria"}
AND ({linkSql})""" AND {linkSql}"""
|> Sql.parameters (webLogContains webLogId :: linkParams) |> Sql.parameters [ webLogContains webLogId; linkParam ]
|> Sql.executeAsync Map.toPermalink |> Sql.executeAsync Map.toPermalink
|> tryHead |> tryHead
} }
/// Get all complete posts for the given web log /// Get all complete posts for the given web log
let findFullByWebLog webLogId = backgroundTask { let findFullByWebLog webLogId = backgroundTask {
log.LogTrace "Post.findFullByWebLog"
let! posts = Document.findByWebLog<Post> source Table.Post webLogId let! posts = Document.findByWebLog<Post> source Table.Post webLogId
let! revisions = Revisions.findByWebLog source Table.PostRevision Table.Post PostId webLogId let! revisions = Revisions.findByWebLog source Table.PostRevision Table.Post PostId webLogId
return return
@ -113,35 +121,39 @@ type PostgresPostData (source : NpgsqlDataSource) =
/// Get a page of categorized posts for the given web log (excludes revisions) /// Get a page of categorized posts for the given web log (excludes revisions)
let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = 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.fromDataSource source
|> Sql.query $" |> Sql.query $"
{postsByCriteria} {selectWithCriteria Table.Post}
AND ({catSql}) AND {catSql}
ORDER BY published_on DESC ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|> Sql.parameters ( |> Sql.parameters
("@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}) [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
:: catParams) catParam
]
|> Sql.executeAsync fromData<Post> |> Sql.executeAsync fromData<Post>
/// Get a page of posts for the given web log (excludes text and revisions) /// Get a page of posts for the given web log (excludes text and revisions)
let findPageOfPosts webLogId pageNbr postsPerPage = let findPageOfPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPosts"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $" |> Sql.query $"
{postsByCriteria} {selectWithCriteria Table.Post}
ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC NULLS FIRST, ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC NULLS FIRST,
data->>'{nameof Post.empty.UpdatedOn}' data ->> '{nameof Post.empty.UpdatedOn}'
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|> Sql.parameters [ webLogContains webLogId ] |> Sql.parameters [ webLogContains webLogId ]
|> Sql.executeAsync postWithoutText |> Sql.executeAsync postWithoutText
/// Get a page of published posts for the given web log (excludes revisions) /// Get a page of published posts for the given web log (excludes revisions)
let findPageOfPublishedPosts webLogId pageNbr postsPerPage = let findPageOfPublishedPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPublishedPosts"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $" |> Sql.query $"
{postsByCriteria} {selectWithCriteria Table.Post}
ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|> Sql.parameters |> Sql.parameters
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} ] [ "@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) /// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks)
let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage = let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage =
log.LogTrace "Post.findPageOfTaggedPosts"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $" |> Sql.query $"
{postsByCriteria} {selectWithCriteria Table.Post}
AND data->'{nameof Post.empty.Tags}' ? @tag AND data['{nameof Post.empty.Tags}'] @> @tag
ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|> Sql.parameters |> Sql.parameters
[ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
"@tag", Sql.jsonb tag "@tag", Query.jsonbDocParam [| tag |]
] ]
|> Sql.executeAsync fromData<Post> |> Sql.executeAsync fromData<Post>
/// Find the next newest and oldest post from a publish date for the given web log /// 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 [ let queryParams () = Sql.parameters [
"@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |}
typedParam "publishedOn" publishedOn "@publishedOn", Sql.string ((InstantPattern.General.Format publishedOn).Substring (0, 19))
] ]
let pubField = nameof Post.empty.PublishedOn
let! older = let! older =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $" |> Sql.query $"
{postsByCriteria} {selectWithCriteria Table.Post}
AND data->>'{nameof Post.empty.PublishedOn}' < @publishedOn AND SUBSTR(data ->> '{pubField}', 1, 19) < @publishedOn
ORDER BY data->>'{nameof Post.empty.PublishedOn}' DESC ORDER BY data ->> '{pubField}' DESC
LIMIT 1" LIMIT 1"
|> queryParams () |> queryParams ()
|> Sql.executeAsync fromData<Post> |> Sql.executeAsync fromData<Post>
let! newer = let! newer =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $" |> Sql.query $"
{postsByCriteria} {selectWithCriteria Table.Post}
AND data->>'{nameof Post.empty.PublishedOn}' > @publishedOn AND SUBSTR(data ->> '{pubField}', 1, 19) > @publishedOn
ORDER BY data->>'{nameof Post.empty.PublishedOn}' ORDER BY data ->> '{pubField}'
LIMIT 1" LIMIT 1"
|> queryParams () |> queryParams ()
|> Sql.executeAsync fromData<Post> |> Sql.executeAsync fromData<Post>
return List.tryHead older, List.tryHead newer 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 /// Save a post
let save (post : Post) = backgroundTask { let save (post : Post) = backgroundTask {
log.LogTrace "Post.save"
let! oldPost = findFullById post.Id post.WebLogId 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 do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions
} }
/// Restore posts from a backup /// Restore posts from a backup
let restore posts = backgroundTask { 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 revisions = posts |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r))
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.executeTransactionAsync [ |> 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.insertSql Table.PostRevision,
revisions |> List.map (fun (postId, rev) -> Revisions.revParams postId PostId.toString rev) 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 /// Update prior permalinks for a post
let updatePriorPermalinks postId webLogId permalinks = backgroundTask { let updatePriorPermalinks postId webLogId permalinks = backgroundTask {
log.LogTrace "Post.updatePriorPermalinks"
match! findById postId webLogId with match! findById postId webLogId with
| Some post -> | Some post ->
do! Sql.fromDataSource source do! Sql.fromDataSource source

View File

@ -1,5 +1,6 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Npgsql open Npgsql
@ -7,18 +8,16 @@ open Npgsql.FSharp
open Npgsql.FSharp.Documents open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog tag mapping data implementation /// PostgreSQL myWebLog tag mapping data implementation
type PostgresTagMapData (source : NpgsqlDataSource) = type PostgresTagMapData (source : NpgsqlDataSource, log : ILogger) =
/// 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 /// Find a tag mapping by its ID for the given web log
let findById tagMapId webLogId = let findById tagMapId webLogId =
log.LogTrace "TagMap.findById"
Document.findByIdAndWebLog<TagMapId, TagMap> source Table.TagMap tagMapId TagMapId.toString webLogId Document.findByIdAndWebLog<TagMapId, TagMap> source Table.TagMap tagMapId TagMapId.toString webLogId
/// Delete a tag mapping for the given web log /// Delete a tag mapping for the given web log
let delete tagMapId webLogId = backgroundTask { let delete tagMapId webLogId = backgroundTask {
log.LogTrace "TagMap.delete"
let! exists = Document.existsByWebLog source Table.TagMap tagMapId TagMapId.toString webLogId let! exists = Document.existsByWebLog source Table.TagMap tagMapId TagMapId.toString webLogId
if exists then if exists then
do! Sql.fromDataSource source |> Query.deleteById Table.TagMap (TagMapId.toString tagMapId) 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 /// Find a tag mapping by its URL value for the given web log
let findByUrlValue (urlValue : string) webLogId = let findByUrlValue (urlValue : string) webLogId =
log.LogTrace "TagMap.findByUrlValue"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query tagMapByCriteria |> Sql.query (selectWithCriteria Table.TagMap)
|> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ] |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with UrlValue = urlValue |} ]
|> Sql.executeAsync fromData<TagMap> |> Sql.executeAsync fromData<TagMap>
|> tryHead |> tryHead
/// Get all tag mappings for the given web log /// Get all tag mappings for the given web log
let findByWebLog webLogId = let findByWebLog webLogId =
log.LogTrace "TagMap.findByWebLog"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"{tagMapByCriteria} ORDER BY data->>'tag'" |> Sql.query $"{selectWithCriteria Table.TagMap} ORDER BY data ->> 'tag'"
|> Sql.parameters [ webLogContains webLogId ] |> Sql.parameters [ webLogContains webLogId ]
|> Sql.executeAsync fromData<TagMap> |> Sql.executeAsync fromData<TagMap>
/// Find any tag mappings in a list of tags for the given web log /// Find any tag mappings in a list of tags for the given web log
let findMappingForTags tags webLogId = 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.fromDataSource source
|> Sql.query $"{tagMapByCriteria} AND ({tagSql})" |> Sql.query $"{selectWithCriteria Table.TagMap} AND {tagSql}"
|> Sql.parameters (webLogContains webLogId :: tagParams) |> Sql.parameters [ webLogContains webLogId; tagParam ]
|> Sql.executeAsync fromData<TagMap> |> 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 /// Save a tag mapping
let save (tagMap : TagMap) = backgroundTask { let save (tagMap : TagMap) = backgroundTask {
do! Sql.fromDataSource source |> Query.save Table.TagMap (TagMapId.toString tagMap.Id) tagMap do! Sql.fromDataSource source |> Query.save Table.TagMap (TagMapId.toString tagMap.Id) tagMap
} }
/// Restore tag mappings from a backup /// Restore tag mappings from a backup
let restore tagMaps = backgroundTask { let restore (tagMaps : TagMap list) = backgroundTask {
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.executeTransactionAsync [ |> 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)
] ]
() ()
} }

View File

@ -1,14 +1,14 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Newtonsoft.Json
open Npgsql open Npgsql
open Npgsql.FSharp open Npgsql.FSharp
open Npgsql.FSharp.Documents open Npgsql.FSharp.Documents
/// PostreSQL myWebLog theme data implementation /// PostreSQL myWebLog theme data implementation
type PostgresThemeData (source : NpgsqlDataSource) = type PostgresThemeData (source : NpgsqlDataSource, log : ILogger) =
/// Clear out the template text from a theme /// Clear out the template text from a theme
let withoutTemplateText row = let withoutTemplateText row =
@ -17,22 +17,26 @@ type PostgresThemeData (source : NpgsqlDataSource) =
/// Retrieve all themes (except 'admin'; excludes template text) /// Retrieve all themes (except 'admin'; excludes template text)
let all () = let all () =
log.LogTrace "Theme.all"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id <> 'admin' ORDER BY id" |> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id <> 'admin' ORDER BY id"
|> Sql.executeAsync withoutTemplateText |> Sql.executeAsync withoutTemplateText
/// Does a given theme exist? /// Does a given theme exist?
let exists themeId = let exists themeId =
log.LogTrace "Theme.exists"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.existsById Table.Theme (ThemeId.toString themeId) |> Query.existsById Table.Theme (ThemeId.toString themeId)
/// Find a theme by its ID /// Find a theme by its ID
let findById themeId = let findById themeId =
log.LogTrace "Theme.findById"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.tryById<Theme> Table.Theme (ThemeId.toString themeId) |> Query.tryById<Theme> Table.Theme (ThemeId.toString themeId)
/// Find a theme by its ID (excludes the text of templates) /// Find a theme by its ID (excludes the text of templates)
let findByIdWithoutText themeId = let findByIdWithoutText themeId =
log.LogTrace "Theme.findByIdWithoutText"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id = @id" |> Sql.query $"{Query.selectFromTable Table.Theme} WHERE id = @id"
|> Sql.parameters [ "@id", Sql.string (ThemeId.toString themeId) ] |> Sql.parameters [ "@id", Sql.string (ThemeId.toString themeId) ]
@ -41,6 +45,7 @@ type PostgresThemeData (source : NpgsqlDataSource) =
/// Delete a theme by its ID /// Delete a theme by its ID
let delete themeId = backgroundTask { let delete themeId = backgroundTask {
log.LogTrace "Theme.delete"
match! exists themeId with match! exists themeId with
| true -> | true ->
do! Sql.fromDataSource source |> Query.deleteById Table.Theme (ThemeId.toString themeId) do! Sql.fromDataSource source |> Query.deleteById Table.Theme (ThemeId.toString themeId)
@ -50,6 +55,7 @@ type PostgresThemeData (source : NpgsqlDataSource) =
/// Save a theme /// Save a theme
let save (theme : Theme) = let save (theme : Theme) =
log.LogTrace "Theme.save"
Sql.fromDataSource source |> Query.save Table.Theme (ThemeId.toString theme.Id) theme Sql.fromDataSource source |> Query.save Table.Theme (ThemeId.toString theme.Id) theme
interface IThemeData with interface IThemeData with
@ -62,16 +68,18 @@ type PostgresThemeData (source : NpgsqlDataSource) =
/// PostreSQL myWebLog theme data implementation /// PostreSQL myWebLog theme data implementation
type PostgresThemeAssetData (source : NpgsqlDataSource) = type PostgresThemeAssetData (source : NpgsqlDataSource, log : ILogger) =
/// Get all theme assets (excludes data) /// Get all theme assets (excludes data)
let all () = let all () =
log.LogTrace "ThemeAsset.all"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}" |> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}"
|> Sql.executeAsync (Map.toThemeAsset false) |> Sql.executeAsync (Map.toThemeAsset false)
/// Delete all assets for the given theme /// Delete all assets for the given theme
let deleteByTheme themeId = backgroundTask { let deleteByTheme themeId = backgroundTask {
log.LogTrace "ThemeAsset.deleteByTheme"
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @themeId" |> 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 /// Find a theme asset by its ID
let findById assetId = let findById assetId =
log.LogTrace "ThemeAsset.findById"
let (ThemeAssetId (ThemeId themeId, path)) = assetId let (ThemeAssetId (ThemeId themeId, path)) = assetId
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId AND path = @path" |> 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) /// Get theme assets for the given theme (excludes data)
let findByTheme themeId = let findByTheme themeId =
log.LogTrace "ThemeAsset.findByTheme"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset} 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.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ]
@ -98,6 +108,7 @@ type PostgresThemeAssetData (source : NpgsqlDataSource) =
/// Get theme assets for the given theme /// Get theme assets for the given theme
let findByThemeWithData themeId = let findByThemeWithData themeId =
log.LogTrace "ThemeAsset.findByThemeWithData"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId" |> Sql.query $"SELECT * FROM {Table.ThemeAsset} WHERE theme_id = @themeId"
|> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ] |> Sql.parameters [ "@themeId", Sql.string (ThemeId.toString themeId) ]
@ -105,6 +116,7 @@ type PostgresThemeAssetData (source : NpgsqlDataSource) =
/// Save a theme asset /// Save a theme asset
let save (asset : ThemeAsset) = backgroundTask { let save (asset : ThemeAsset) = backgroundTask {
log.LogTrace "ThemeAsset.save"
let (ThemeAssetId (ThemeId themeId, path)) = asset.Id let (ThemeAssetId (ThemeId themeId, path)) = asset.Id
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source

View File

@ -1,12 +1,13 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Npgsql open Npgsql
open Npgsql.FSharp open Npgsql.FSharp
/// PostgreSQL myWebLog uploaded file data implementation /// PostgreSQL myWebLog uploaded file data implementation
type PostgresUploadData (source : NpgsqlDataSource) = type PostgresUploadData (source : NpgsqlDataSource, log : ILogger) =
/// The INSERT statement for an uploaded file /// The INSERT statement for an uploaded file
let upInsert = $" let upInsert = $"
@ -27,6 +28,7 @@ type PostgresUploadData (source : NpgsqlDataSource) =
/// Save an uploaded file /// Save an uploaded file
let add upload = backgroundTask { let add upload = backgroundTask {
log.LogTrace "Upload.add"
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query upInsert |> Sql.query upInsert
@ -37,6 +39,7 @@ type PostgresUploadData (source : NpgsqlDataSource) =
/// Delete an uploaded file by its ID /// Delete an uploaded file by its ID
let delete uploadId webLogId = backgroundTask { let delete uploadId webLogId = backgroundTask {
log.LogTrace "Upload.delete"
let idParam = [ "@id", Sql.string (UploadId.toString uploadId) ] let idParam = [ "@id", Sql.string (UploadId.toString uploadId) ]
let! path = let! path =
Sql.fromDataSource source Sql.fromDataSource source
@ -56,6 +59,7 @@ type PostgresUploadData (source : NpgsqlDataSource) =
/// Find an uploaded file by its path for the given web log /// Find an uploaded file by its path for the given web log
let findByPath path webLogId = let findByPath path webLogId =
log.LogTrace "Upload.findByPath"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT * FROM {Table.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.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) /// Find all uploaded files for the given web log (excludes data)
let findByWebLog webLogId = let findByWebLog webLogId =
log.LogTrace "Upload.findByWebLog"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT id, web_log_id, path, updated_on FROM {Table.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.parameters [ webLogIdParam webLogId ]
@ -71,6 +76,7 @@ type PostgresUploadData (source : NpgsqlDataSource) =
/// Find all uploaded files for the given web log /// Find all uploaded files for the given web log
let findByWebLogWithData webLogId = let findByWebLogWithData webLogId =
log.LogTrace "Upload.findByWebLogWithData"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId" |> Sql.query $"SELECT * FROM {Table.Upload} WHERE web_log_id = @webLogId"
|> Sql.parameters [ webLogIdParam webLogId ] |> Sql.parameters [ webLogIdParam webLogId ]
@ -78,12 +84,11 @@ type PostgresUploadData (source : NpgsqlDataSource) =
/// Restore uploads from a backup /// Restore uploads from a backup
let restore uploads = backgroundTask { let restore uploads = backgroundTask {
log.LogTrace "Upload.restore"
for batch in uploads |> List.chunkBySize 5 do for batch in uploads |> List.chunkBySize 5 do
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.executeTransactionAsync [ |> Sql.executeTransactionAsync [ upInsert, batch |> List.map upParams ]
upInsert, batch |> List.map upParams
]
() ()
} }

View File

@ -1,5 +1,6 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Npgsql open Npgsql
@ -7,19 +8,22 @@ open Npgsql.FSharp
open Npgsql.FSharp.Documents open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog web log data implementation /// PostgreSQL myWebLog web log data implementation
type PostgresWebLogData (source : NpgsqlDataSource) = type PostgresWebLogData (source : NpgsqlDataSource, log : ILogger) =
/// Add a web log /// Add a web log
let add (webLog : WebLog) = let add (webLog : WebLog) =
log.LogTrace "WebLog.add"
Sql.fromDataSource source |> Query.insert Table.WebLog (WebLogId.toString webLog.Id) webLog Sql.fromDataSource source |> Query.insert Table.WebLog (WebLogId.toString webLog.Id) webLog
/// Retrieve all web logs /// Retrieve all web logs
let all () = let all () =
log.LogTrace "WebLog.all"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.all<WebLog> Table.WebLog |> Query.all<WebLog> Table.WebLog
/// Delete a web log by its ID /// Delete a web log by its ID
let delete webLogId = backgroundTask { let delete webLogId = backgroundTask {
log.LogTrace "WebLog.delete"
let criteria = Query.whereDataContains "@criteria" let criteria = Query.whereDataContains "@criteria"
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
@ -40,23 +44,27 @@ type PostgresWebLogData (source : NpgsqlDataSource) =
/// Find a web log by its host (URL base) /// Find a web log by its host (URL base)
let findByHost (url : string) = let findByHost (url : string) =
log.LogTrace "WebLog.findByHost"
Sql.fromDataSource source 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.parameters [ "@criteria", Query.jsonbDocParam {| UrlBase = url |} ]
|> Sql.executeAsync fromData<WebLog> |> Sql.executeAsync fromData<WebLog>
|> tryHead |> tryHead
/// Find a web log by its ID /// Find a web log by its ID
let findById webLogId = let findById webLogId =
log.LogTrace "WebLog.findById"
Sql.fromDataSource source Sql.fromDataSource source
|> Query.tryById<WebLog> Table.WebLog (WebLogId.toString webLogId) |> Query.tryById<WebLog> Table.WebLog (WebLogId.toString webLogId)
/// Update settings for a web log /// Update settings for a web log
let updateSettings (webLog : WebLog) = let updateSettings (webLog : WebLog) =
log.LogTrace "WebLog.updateSettings"
Sql.fromDataSource source |> Query.update Table.WebLog (WebLogId.toString webLog.Id) webLog Sql.fromDataSource source |> Query.update Table.WebLog (WebLogId.toString webLog.Id) webLog
/// Update RSS options for a web log /// Update RSS options for a web log
let updateRssOptions (webLog : WebLog) = backgroundTask { let updateRssOptions (webLog : WebLog) = backgroundTask {
log.LogTrace "WebLog.updateRssOptions"
match! findById webLog.Id with match! findById webLog.Id with
| Some blog -> | Some blog ->
do! Sql.fromDataSource source do! Sql.fromDataSource source

View File

@ -1,5 +1,6 @@
namespace MyWebLog.Data.Postgres namespace MyWebLog.Data.Postgres
open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Npgsql open Npgsql
@ -7,23 +8,17 @@ open Npgsql.FSharp
open Npgsql.FSharp.Documents open Npgsql.FSharp.Documents
/// PostgreSQL myWebLog user data implementation /// 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 /// Find a user by their ID for the given web log
let findById userId webLogId = let findById userId webLogId =
log.LogTrace "WebLogUser.findById"
Document.findByIdAndWebLog<WebLogUserId, WebLogUser> Document.findByIdAndWebLog<WebLogUserId, WebLogUser>
source Table.WebLogUser userId WebLogUserId.toString webLogId source Table.WebLogUser userId WebLogUserId.toString webLogId
/// Delete a user if they have no posts or pages /// Delete a user if they have no posts or pages
let delete userId webLogId = backgroundTask { let delete userId webLogId = backgroundTask {
log.LogTrace "WebLogUser.delete"
match! findById userId webLogId with match! findById userId webLogId with
| Some _ -> | Some _ ->
let criteria = Query.whereDataContains "@criteria" 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 /// Find a user by their e-mail address for the given web log
let findByEmail (email : string) webLogId = let findByEmail (email : string) webLogId =
log.LogTrace "WebLogUser.findByEmail"
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query userByCriteria |> Sql.query (selectWithCriteria Table.WebLogUser)
|> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ] |> Sql.parameters [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Email = email |} ]
|> Sql.executeAsync fromData<WebLogUser> |> Sql.executeAsync fromData<WebLogUser>
|> tryHead |> tryHead
/// Get all users for the given web log /// Get all users for the given web log
let findByWebLog webLogId = let findByWebLog webLogId =
log.LogTrace "WebLogUser.findByWebLog"
Sql.fromDataSource source 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.parameters [ webLogContains webLogId ]
|> Sql.executeAsync fromData<WebLogUser> |> Sql.executeAsync fromData<WebLogUser>
/// Find the names of users by their IDs for the given web log /// Find the names of users by their IDs for the given web log
let findNames webLogId userIds = backgroundTask { let findNames webLogId userIds = backgroundTask {
log.LogTrace "WebLogUser.findNames"
let idSql, idParams = inClause "AND id" "id" WebLogUserId.toString userIds let idSql, idParams = inClause "AND id" "id" WebLogUserId.toString userIds
let! users = let! users =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.query $"{userByCriteria} {idSql}" |> Sql.query $"{selectWithCriteria Table.WebLogUser} {idSql}"
|> Sql.parameters (webLogContains webLogId :: idParams) |> Sql.parameters (webLogContains webLogId :: idParams)
|> Sql.executeAsync fromData<WebLogUser> |> Sql.executeAsync fromData<WebLogUser>
return return
@ -73,17 +72,20 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) =
} }
/// Restore users from a backup /// Restore users from a backup
let restore users = backgroundTask { let restore (users : WebLogUser list) = backgroundTask {
log.LogTrace "WebLogUser.restore"
let! _ = let! _ =
Sql.fromDataSource source Sql.fromDataSource source
|> Sql.executeTransactionAsync [ |> 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 /// Set a user's last seen date/time to now
let setLastSeen userId webLogId = backgroundTask { let setLastSeen userId webLogId = backgroundTask {
log.LogTrace "WebLogUser.setLastSeen"
match! findById userId webLogId with match! findById userId webLogId with
| Some user -> | Some user ->
do! Sql.fromDataSource source do! Sql.fromDataSource source
@ -94,6 +96,7 @@ type PostgresWebLogUserData (source : NpgsqlDataSource) =
/// Save a user /// Save a user
let save (user : WebLogUser) = let save (user : WebLogUser) =
log.LogTrace "WebLogUser.save"
Sql.fromDataSource source |> Query.save Table.WebLogUser (WebLogUserId.toString user.Id) user Sql.fromDataSource source |> Query.save Table.WebLogUser (WebLogUserId.toString user.Id) user
interface IWebLogUserData with interface IWebLogUserData with

View File

@ -152,19 +152,20 @@ type PostgresData (source : NpgsqlDataSource, log : ILogger<PostgresData>, ser :
interface IData with interface IData with
member _.Category = PostgresCategoryData source member _.Category = PostgresCategoryData (source, log)
member _.Page = PostgresPageData (source, log) member _.Page = PostgresPageData (source, log)
member _.Post = PostgresPostData source member _.Post = PostgresPostData (source, log)
member _.TagMap = PostgresTagMapData source member _.TagMap = PostgresTagMapData (source, log)
member _.Theme = PostgresThemeData source member _.Theme = PostgresThemeData (source, log)
member _.ThemeAsset = PostgresThemeAssetData source member _.ThemeAsset = PostgresThemeAssetData (source, log)
member _.Upload = PostgresUploadData source member _.Upload = PostgresUploadData (source, log)
member _.WebLog = PostgresWebLogData source member _.WebLog = PostgresWebLogData (source, log)
member _.WebLogUser = PostgresWebLogUserData source member _.WebLogUser = PostgresWebLogUserData (source, log)
member _.Serializer = ser member _.Serializer = ser
member _.StartUp () = backgroundTask { member _.StartUp () = backgroundTask {
log.LogTrace "PostgresData.StartUp"
do! ensureTables () do! ensureTables ()
let! version = let! version =

View File

@ -43,7 +43,7 @@ module DataImplementation =
let createNpgsqlDataSource (cfg : IConfiguration) = let createNpgsqlDataSource (cfg : IConfiguration) =
let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL") let builder = NpgsqlDataSourceBuilder (cfg.GetConnectionString "PostgreSQL")
let _ = builder.UseNodaTime () 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 () builder.Build ()
/// Get the configured data implementation /// Get the configured data implementation
@ -71,8 +71,7 @@ module DataImplementation =
let source = createNpgsqlDataSource config let source = createNpgsqlDataSource config
use conn = source.CreateConnection () use conn = source.CreateConnection ()
let log = sp.GetRequiredService<ILogger<PostgresData>> () let log = sp.GetRequiredService<ILogger<PostgresData>> ()
log.LogWarning (sprintf "%s %s" conn.DataSource conn.Database) log.LogInformation $"Using PostgreSQL database {conn.Database}"
log.LogInformation $"Using PostgreSQL database {conn.Host}:{conn.Port}/{conn.Database}"
PostgresData (source, log, Json.configure (JsonSerializer.CreateDefault ())) PostgresData (source, log, Json.configure (JsonSerializer.CreateDefault ()))
else else
createSQLite "Data Source=./myweblog.db;Cache=Shared" 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<IData> postgres
let _ = let _ =
builder.Services.AddSingleton<IDistributedCache> (fun sp -> builder.Services.AddSingleton<IDistributedCache> (fun sp ->
Postgres.DistributedCache ((sp.GetRequiredService<IConfiguration> ()).GetConnectionString "PostgreSQL") Postgres.DistributedCache (sp.GetRequiredService<NpgsqlDataSource> ()) :> IDistributedCache)
:> IDistributedCache)
() ()
| _ -> () | _ -> ()

View File

@ -2,8 +2,7 @@
"Generator": "myWebLog 2.0-rc2", "Generator": "myWebLog 2.0-rc2",
"Logging": { "Logging": {
"LogLevel": { "LogLevel": {
"MyWebLog.Handlers": "Information", "MyWebLog.Handlers": "Information"
"MyWebLog.Data": "Trace"
} }
} }
} }