V2 #36
|
@ -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,
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 [
|
||||||
|
|
|
@ -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,21 +106,10 @@ 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 {
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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)
|
||||||
]
|
]
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
|
||||||
]
|
|
||||||
()
|
()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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 =
|
||||||
|
|
|
@ -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)
|
|
||||||
()
|
()
|
||||||
| _ -> ()
|
| _ -> ()
|
||||||
|
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user