Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
9 changed files with 303 additions and 396 deletions
Showing only changes of commit bb4252f3c1 - Show all commits

View File

@ -76,13 +76,6 @@ let toList<'T> (it: SqliteDataReader -> 'T) (rdr: SqliteDataReader) =
seq { while rdr.Read () do it rdr } seq { while rdr.Read () do it rdr }
|> List.ofSeq |> List.ofSeq
/// Verify that the web log ID matches before returning an item
let verifyWebLog<'T> webLogId (prop : 'T -> WebLogId) (it : SqliteDataReader -> 'T) (rdr : SqliteDataReader) =
if rdr.Read() then
let item = it rdr
if prop item = webLogId then Some item else None
else None
/// Execute a command that returns no data /// Execute a command that returns no data
let write (cmd: SqliteCommand) = backgroundTask { let write (cmd: SqliteCommand) = backgroundTask {
let! _ = cmd.ExecuteNonQueryAsync() let! _ = cmd.ExecuteNonQueryAsync()
@ -90,7 +83,7 @@ let write (cmd: SqliteCommand) = backgroundTask {
} }
/// Add a possibly-missing parameter, substituting null for None /// Add a possibly-missing parameter, substituting null for None
let maybe<'T> (it : 'T option) : obj = match it with Some x -> x :> obj | None -> DBNull.Value let maybe<'T> (it: 'T option) : obj = match it with Some x -> x :> obj | None -> DBNull.Value
/// Create a value for a Duration /// Create a value for a Duration
let durationParam = let durationParam =
@ -261,7 +254,8 @@ let cmdToList<'TDoc> (cmd: SqliteCommand) ser = backgroundTask {
} }
/// Queries to assist with document manipulation /// Queries to assist with document manipulation
module Query = [<Obsolete("change me")>]
module QueryOld =
/// Fragment to add an ID condition to a WHERE clause (parameter @id) /// Fragment to add an ID condition to a WHERE clause (parameter @id)
let whereById = let whereById =
@ -292,6 +286,14 @@ module Query =
$"DELETE FROM %s{table} WHERE {whereById}" $"DELETE FROM %s{table} WHERE {whereById}"
/// Create a document ID parameter
let idParam (key: 'TKey) =
SqliteParameter("@id", string key)
/// Create a web log ID parameter
let webLogParam (webLogId: WebLogId) =
SqliteParameter("@webLogId", string webLogId)
let addParam (cmd: SqliteCommand) name (value: obj) = let addParam (cmd: SqliteCommand) name (value: obj) =
cmd.Parameters.AddWithValue(name, value) |> ignore cmd.Parameters.AddWithValue(name, value) |> ignore
@ -307,18 +309,39 @@ let addDocParam<'TDoc> (cmd: SqliteCommand) (doc: 'TDoc) ser =
let addWebLogId (cmd: SqliteCommand) (webLogId: WebLogId) = let addWebLogId (cmd: SqliteCommand) (webLogId: WebLogId) =
addParam cmd "@webLogId" (string webLogId) addParam cmd "@webLogId" (string webLogId)
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
/// Functions for manipulating documents /// Functions for manipulating documents
module Document = module Document =
/// Queries to assist with document manipulation
module Query =
/// Fragment to add a web log ID condition to a WHERE clause (parameter @webLogId)
let whereByWebLog =
Query.whereFieldEquals "WebLogId" "@webLogId"
/// A SELECT query to count documents for a given web log ID
let countByWebLog table =
$"{Query.Count.all table} WHERE {whereByWebLog}"
/// A query to select from a table by the document's ID and its web log ID
let selectByIdAndWebLog table =
$"{Query.Find.byFieldEquals table} AND {whereByWebLog}"
/// A query to select from a table by its web log ID
let selectByWebLog table =
$"{Query.selectFromTable table} WHERE {whereByWebLog}"
/// Count documents for the given web log ID /// Count documents for the given web log ID
let countByWebLog (conn: SqliteConnection) table webLogId = backgroundTask { let countByWebLog table (webLogId: WebLogId) conn = backgroundTask {
use cmd = conn.CreateCommand() let! count = Count.byFieldEquals table "WebLogId" webLogId conn
cmd.CommandText <- Query.countByWebLog table return int count
addWebLogId cmd webLogId
return! count cmd
} }
/// Find a document by its ID /// Find a document by its ID
[<Obsolete("replace this")>]
let findById<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) = backgroundTask { let findById<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) = backgroundTask {
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereById}" cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereById}"
@ -329,55 +352,49 @@ module Document =
} }
/// Find a document by its ID and web log ID /// Find a document by its ID and web log ID
let findByIdAndWebLog<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) webLogId = backgroundTask { let findByIdAndWebLog<'TKey, 'TDoc> table (key: 'TKey) webLogId conn =
use cmd = conn.CreateCommand() Custom.single (Query.selectByIdAndWebLog table) [ idParam key; webLogParam webLogId ] fromData<'TDoc> conn
cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereById} AND {Query.whereByWebLog}"
addDocId cmd key
addWebLogId cmd webLogId
use! rdr = cmd.ExecuteReaderAsync()
let! isFound = rdr.ReadAsync()
return if isFound then Some (Map.fromDoc<'TDoc> ser rdr) else None
}
/// Find documents for the given web log /// Find documents for the given web log
let findByWebLog<'TDoc> (conn: SqliteConnection) ser table webLogId = let findByWebLog<'TDoc> table (webLogId: WebLogId) conn =
use cmd = conn.CreateCommand() Find.byFieldEquals<'TDoc> table "WebLogId" webLogId conn
cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereByWebLog}"
addWebLogId cmd webLogId
cmdToList<'TDoc> cmd ser
/// Insert a document /// Insert a document
[<Obsolete("replace this")>]
let insert<'TDoc> (conn: SqliteConnection) ser table (doc: 'TDoc) = backgroundTask { let insert<'TDoc> (conn: SqliteConnection) ser table (doc: 'TDoc) = backgroundTask {
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- Query.insert table cmd.CommandText <- QueryOld.insert table
addDocParam<'TDoc> cmd doc ser addDocParam<'TDoc> cmd doc ser
do! write cmd do! write cmd
} }
/// Update (replace) a document by its ID /// Update (replace) a document by its ID
[<Obsolete("replace this")>]
let update<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) (doc: 'TDoc) = backgroundTask { let update<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) (doc: 'TDoc) = backgroundTask {
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- Query.updateById table cmd.CommandText <- QueryOld.updateById table
addDocId cmd key addDocId cmd key
addDocParam<'TDoc> cmd doc ser addDocParam<'TDoc> cmd doc ser
do! write cmd do! write cmd
} }
/// Update a field in a document by its ID /// Update a field in a document by its ID
[<Obsolete("replace this")>]
let updateField<'TKey, 'TValue> (conn: SqliteConnection) ser table (key: 'TKey) jsonField let updateField<'TKey, 'TValue> (conn: SqliteConnection) ser table (key: 'TKey) jsonField
(value: 'TValue) = backgroundTask { (value: 'TValue) = backgroundTask {
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- cmd.CommandText <-
$"UPDATE %s{table} SET data = json_set(data, '$.{jsonField}', json(@it)) WHERE {Query.whereById}" $"UPDATE %s{table} SET data = json_set(data, '$.{jsonField}', json(@it)) WHERE {QueryOld.whereById}"
addDocId cmd key addDocId cmd key
addParam cmd "@it" (Utils.serialize ser value) addParam cmd "@it" (Utils.serialize ser value)
do! write cmd do! write cmd
} }
/// Delete a document by its ID /// Delete a document by its ID
[<Obsolete("replace this")>]
let delete<'TKey> (conn: SqliteConnection) table (key: 'TKey) = backgroundTask { let delete<'TKey> (conn: SqliteConnection) table (key: 'TKey) = backgroundTask {
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- Query.deleteById table cmd.CommandText <- QueryOld.deleteById table
addDocId cmd key addDocId cmd key
do! write cmd do! write cmd
} }
@ -386,29 +403,24 @@ module Document =
module Revisions = module Revisions =
/// Find all revisions for the given entity /// Find all revisions for the given entity
let findByEntityId<'TKey> (conn: SqliteConnection) revTable entityTable (key: 'TKey) = backgroundTask { let findByEntityId<'TKey> revTable entityTable (key: 'TKey) conn =
use cmd = conn.CreateCommand() Custom.list
cmd.CommandText <-
$"SELECT as_of, revision_text FROM %s{revTable} WHERE %s{entityTable}_id = @id ORDER BY as_of DESC" $"SELECT as_of, revision_text FROM %s{revTable} WHERE %s{entityTable}_id = @id ORDER BY as_of DESC"
addDocId cmd key [ idParam key ]
use! rdr = cmd.ExecuteReaderAsync() Map.toRevision
return toList Map.toRevision rdr conn
}
/// Find all revisions for all posts for the given web log /// Find all revisions for all posts for the given web log
let findByWebLog<'TKey> (conn: SqliteConnection) revTable entityTable (keyFunc: string -> 'TKey) let findByWebLog<'TKey> revTable entityTable (keyFunc: string -> 'TKey) webLogId conn =
webLogId = backgroundTask { Custom.list
use cmd = conn.CreateCommand()
cmd.CommandText <-
$"SELECT pr.* $"SELECT pr.*
FROM %s{revTable} pr FROM %s{revTable} pr
INNER JOIN %s{entityTable} p ON p.data ->> 'Id' = pr.{entityTable}_id INNER JOIN %s{entityTable} p ON p.data ->> 'Id' = pr.{entityTable}_id
WHERE p.{Query.whereByWebLog} WHERE p.{Document.Query.whereByWebLog}
ORDER BY as_of DESC" ORDER BY as_of DESC"
addWebLogId cmd webLogId [ webLogParam webLogId ]
use! rdr = cmd.ExecuteReaderAsync() (fun rdr -> keyFunc (Map.getString $"{entityTable}_id" rdr), Map.toRevision rdr)
return toList (fun rdr -> keyFunc (Map.getString $"{entityTable}_id" rdr), Map.toRevision rdr) rdr conn
}
/// Parameters for a revision INSERT statement /// Parameters for a revision INSERT statement
let revParams<'TKey> (key: 'TKey) rev = let revParams<'TKey> (key: 'TKey) rev =
@ -416,26 +428,15 @@ module Revisions =
SqliteParameter("@id", string key) SqliteParameter("@id", string key)
SqliteParameter("@text", rev.Text) ] SqliteParameter("@text", rev.Text) ]
/// The SQL statement to insert a revision
let insertSql table =
$"INSERT INTO %s{table} VALUES (@id, @asOf, @text)"
/// Update a page or post's revisions /// Update a page or post's revisions
let update<'TKey> (conn: SqliteConnection) revTable entityTable (key: 'TKey) oldRevs newRevs = backgroundTask { let update<'TKey> revTable entityTable (key: 'TKey) oldRevs newRevs conn = backgroundTask {
let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs
if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then
use cmd = conn.CreateCommand()
if not (List.isEmpty toDelete) then
cmd.CommandText <- $"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf"
for delRev in toDelete do for delRev in toDelete do
cmd.Parameters.Clear() do! Custom.nonQuery
addDocId cmd key $"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf"
addParam cmd "@asOf" delRev.AsOf [ idParam key; SqliteParameter("@asOf", instantParam delRev.AsOf) ]
do! write cmd conn
if not (List.isEmpty toAdd) then
cmd.CommandText <- insertSql revTable
for addRev in toAdd do for addRev in toAdd do
cmd.Parameters.Clear() do! Custom.nonQuery $"INSERT INTO {revTable} VALUES (@id, @asOf, @text)" (revParams key addRev) conn
cmd.Parameters.AddRange(revParams key addRev)
do! write cmd
} }

View File

@ -1,6 +1,8 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open System.Threading.Tasks open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -13,29 +15,24 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
/// The name of the parent ID field /// The name of the parent ID field
let parentIdField = nameof Category.Empty.ParentId let parentIdField = nameof Category.Empty.ParentId
/// Add a category
let add (cat: Category) =
log.LogTrace "Category.add"
Document.insert conn ser Table.Category cat
/// 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" log.LogTrace "Category.countAll"
Document.countByWebLog conn Table.Category webLogId Document.countByWebLog Table.Category webLogId conn
/// Count all top-level categories for the given web log /// Count all top-level categories for the given web log
let countTopLevel webLogId = backgroundTask { let countTopLevel webLogId =
log.LogTrace "Category.countTopLevel" log.LogTrace "Category.countTopLevel"
use cmd = conn.CreateCommand() Custom.scalar
cmd.CommandText <- $"{Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL" $"{Document.Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL"
addWebLogId cmd webLogId [ webLogParam webLogId ]
return! count cmd (fun rdr -> int (rdr.GetInt64(0)))
} conn
/// 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" log.LogTrace "Category.findByWebLog"
Document.findByWebLog<Category> conn ser Table.Category webLogId Document.findByWebLog<Category> Table.Category webLogId conn
/// 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 {
@ -53,104 +50,74 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
|> Seq.append (Seq.singleton it.Id) |> Seq.append (Seq.singleton it.Id)
|> List.ofSeq |> List.ofSeq
|> inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId" |> inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId"
use cmd = conn.CreateCommand() let query = $"""
cmd.CommandText <- $"
SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}') SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}')
FROM {Table.Post} FROM {Table.Post}
WHERE {Query.whereByWebLog} WHERE {Document.Query.whereByWebLog}
AND data ->> '{nameof Post.Empty.Status}' = '{string Published}' AND {Query.whereFieldEquals (nameof Post.Empty.Status) $"'{string Published}'"}
AND {catSql}" AND {catSql}"""
addWebLogId cmd webLogId let! postCount = Custom.scalar query (webLogParam webLogId :: catParams) (_.GetInt64(0)) conn
cmd.Parameters.AddRange catParams return it.Id, int postCount
let! postCount = count cmd
return it.Id, postCount
}) })
|> Task.WhenAll |> Task.WhenAll
return return
ordered ordered
|> Seq.map (fun cat -> |> Seq.map (fun cat ->
{ cat with { cat with
PostCount = PostCount = defaultArg (counts |> Array.tryFind (fun c -> fst c = cat.Id) |> Option.map snd) 0
counts })
|> Array.tryFind (fun c -> fst c = cat.Id)
|> Option.map snd
|> Option.defaultValue 0 })
|> Array.ofSeq |> Array.ofSeq
} }
/// 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" log.LogTrace "Category.findById"
Document.findByIdAndWebLog<CategoryId, Category> conn ser Table.Category catId webLogId Document.findByIdAndWebLog<CategoryId, Category> Table.Category catId webLogId conn
/// Delete a category /// Delete a category
let delete catId webLogId = backgroundTask { let delete catId webLogId = backgroundTask {
log.LogTrace "Category.delete" log.LogTrace "Category.delete"
match! findById catId webLogId with match! findById catId webLogId with
| Some cat -> | Some cat ->
use cmd = conn.CreateCommand()
// Reassign any children to the category's parent category // Reassign any children to the category's parent category
cmd.CommandText <- $"SELECT COUNT(*) FROM {Table.Category} WHERE data ->> '{parentIdField}' = @parentId" let! children = Count.byFieldEquals Table.Category parentIdField catId conn
addParam cmd "@parentId" (string catId)
let! children = count cmd
if children > 0 then if children > 0 then
cmd.CommandText <- $" do! Update.partialByFieldEquals Table.Category parentIdField catId {| ParentId = cat.ParentId |} conn
UPDATE {Table.Category}
SET data = json_set(data, '$.{parentIdField}', @newParentId)
WHERE data ->> '{parentIdField}' = @parentId"
addParam cmd "@newParentId" (maybe (cat.ParentId |> Option.map string))
do! write cmd
// Delete the category off all posts where it is assigned, and the category itself // Delete the category off all posts where it is assigned, and the category itself
let catIdField = Post.Empty.CategoryIds let catIdField = Post.Empty.CategoryIds
cmd.CommandText <- $" let! posts =
SELECT data ->> '{Post.Empty.Id}' AS id, data -> '{catIdField}' AS cat_ids Custom.list
$"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}'
FROM {Table.Post} FROM {Table.Post}
WHERE {Query.whereByWebLog} WHERE {Document.Query.whereByWebLog}
AND EXISTS AND EXISTS
(SELECT 1 FROM json_each({Table.Post}.data -> '{catIdField}') WHERE json_each.value = @id)" (SELECT 1
cmd.Parameters.Clear() FROM json_each({Table.Post}.data -> '{catIdField}')
addDocId cmd catId WHERE json_each.value = @id)"
addWebLogId cmd webLogId [ idParam catId; webLogParam webLogId ]
use! postRdr = cmd.ExecuteReaderAsync() (fun rdr -> rdr.GetString(0), Utils.deserialize<string list> ser (rdr.GetString(1)))
if postRdr.HasRows then conn
let postIdAndCats = for postId, cats in posts do
toList do! Update.partialById
(fun rdr -> Table.Post postId {| CategoryIds = cats |> List.filter (fun it -> it <> string catId) |} conn
Map.getString "id" rdr, Utils.deserialize<string list> ser (Map.getString "cat_ids" rdr))
postRdr
do! postRdr.CloseAsync()
for postId, cats in postIdAndCats do
cmd.CommandText <- $"
UPDATE {Table.Post}
SET data = json_set(data, '$.{catIdField}', json(@catIds))
WHERE {Query.whereById}"
cmd.Parameters.Clear()
addDocId cmd postId
addParam cmd "@catIds" (cats |> List.filter (fun it -> it <> string catId) |> Utils.serialize ser)
do! write cmd
do! Document.delete conn Table.Category catId do! Document.delete conn Table.Category catId
return if children = 0 then CategoryDeleted else ReassignedChildCategories return if children = 0L then CategoryDeleted else ReassignedChildCategories
| None -> return CategoryNotFound | None -> return CategoryNotFound
} }
/// Save a category
let save cat =
log.LogTrace "Category.save"
save<Category> Table.Category cat conn
/// Restore categories from a backup /// Restore categories from a backup
let restore cats = backgroundTask { let restore cats = backgroundTask {
for cat in cats do log.LogTrace "Category.restore"
do! add cat for cat in cats do do! save cat
}
/// Update a category
let update (cat: Category) = backgroundTask {
use cmd = conn.CreateCommand()
cmd.CommandText <- $"{Query.updateById} AND {Query.whereByWebLog}"
addDocId cmd cat.Id
addDocParam cmd cat ser
addWebLogId cmd cat.WebLogId
do! write cmd
} }
interface ICategoryData with interface ICategoryData with
member _.Add cat = add cat member _.Add cat = save cat
member _.CountAll webLogId = countAll webLogId member _.CountAll webLogId = countAll webLogId
member _.CountTopLevel webLogId = countTopLevel webLogId member _.CountTopLevel webLogId = countTopLevel webLogId
member _.FindAllForView webLogId = findAllForView webLogId member _.FindAllForView webLogId = findAllForView webLogId
@ -158,4 +125,4 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
member _.FindByWebLog webLogId = findByWebLog webLogId member _.FindByWebLog webLogId = findByWebLog webLogId
member _.Delete catId webLogId = delete catId webLogId member _.Delete catId webLogId = delete catId webLogId
member _.Restore cats = restore cats member _.Restore cats = restore cats
member _.Update cat = update cat member _.Update cat = save cat

View File

@ -1,20 +1,21 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open System.Threading.Tasks open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Newtonsoft.Json
/// SQLite myWebLog page data implementation /// SQLite myWebLog page data implementation
type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// The JSON field for the permalink /// The JSON field name for the permalink
let linkField = $"data ->> '{nameof Page.Empty.Permalink}'" let linkName = nameof Page.Empty.Permalink
/// The JSON field for the "is in page list" flag /// The JSON field name for the "is in page list" flag
let pgListField = $"data ->> '{nameof Page.Empty.IsInPageList}'" let pgListName = nameof Page.Empty.IsInPageList
/// The JSON field for the title of the page /// The JSON field for the title of the page
let titleField = $"data ->> '{nameof Page.Empty.Title}'" let titleField = $"data ->> '{nameof Page.Empty.Title}'"
@ -24,57 +25,44 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
/// Append revisions to a page /// Append revisions to a page
let appendPageRevisions (page : Page) = backgroundTask { let appendPageRevisions (page : Page) = backgroundTask {
log.LogTrace "Page.appendPageRevisions" log.LogTrace "Page.appendPageRevisions"
let! revisions = Revisions.findByEntityId conn Table.PageRevision Table.Page page.Id let! revisions = Revisions.findByEntityId Table.PageRevision Table.Page page.Id conn
return { page with Revisions = revisions } return { page with Revisions = revisions }
} }
/// Return a page with no text
let withoutText (page: Page) =
{ page with Text = "" }
/// Update a page's revisions /// Update a page's revisions
let updatePageRevisions (pageId: PageId) oldRevs newRevs = let updatePageRevisions (pageId: PageId) oldRevs newRevs =
log.LogTrace "Page.updatePageRevisions" log.LogTrace "Page.updatePageRevisions"
Revisions.update conn Table.PageRevision Table.Page pageId oldRevs newRevs Revisions.update Table.PageRevision Table.Page pageId oldRevs newRevs conn
// IMPLEMENTATION FUNCTIONS // IMPLEMENTATION FUNCTIONS
/// Add a page
let add page = backgroundTask {
log.LogTrace "Page.add"
do! Document.insert<Page> conn ser Table.Page { page with Revisions = [] }
do! updatePageRevisions page.Id [] page.Revisions
}
/// 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 = backgroundTask { let all webLogId =
log.LogTrace "Page.all" log.LogTrace "Page.all"
use cmd = conn.CreateCommand() Custom.list
cmd.CommandText <- $"{Query.selectFromTable Table.Page} WHERE {Document.Query.whereByWebLog} ORDER BY LOWER({titleField})"
$"{Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} ORDER BY LOWER({titleField})" [ webLogParam webLogId ]
addWebLogId cmd webLogId (fun rdr -> { fromData<Page> rdr with Text = "" })
let! pages = cmdToList<Page> cmd ser conn
return pages |> List.map withoutText
}
/// Count all pages for the given web log /// Count all pages for the given web log
let countAll webLogId = let countAll webLogId =
log.LogTrace "Page.countAll" log.LogTrace "Page.countAll"
Document.countByWebLog conn Table.Page webLogId Document.countByWebLog Table.Page webLogId conn
/// 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 = backgroundTask { let countListed webLogId =
log.LogTrace "Page.countListed" log.LogTrace "Page.countListed"
use cmd = conn.CreateCommand() Custom.scalar
cmd.CommandText <- $"{Query.countByWebLog} AND {pgListField} = 'true'" $"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals pgListName "'true'"}"""
addWebLogId cmd webLogId [ webLogParam webLogId ]
return! count cmd (fun rdr -> int (rdr.GetInt64(0)))
} conn
/// 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 "Page.findById" log.LogTrace "Page.findById"
Document.findByIdAndWebLog<PageId, Page> conn ser Table.Page pageId webLogId Document.findByIdAndWebLog<PageId, Page> Table.Page pageId webLogId conn
/// Find a complete page by its ID /// Find a complete page by its ID
let findFullById pageId webLogId = backgroundTask { let findFullById pageId webLogId = backgroundTask {
@ -92,93 +80,74 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
log.LogTrace "Page.delete" log.LogTrace "Page.delete"
match! findById pageId webLogId with match! findById pageId webLogId with
| Some _ -> | Some _ ->
use cmd = conn.CreateCommand() do! Custom.nonQuery
cmd.CommandText <- $"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.deleteById}" $"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.Delete.byId Table.Page}"
addDocId cmd pageId [ idParam pageId ]
do! write cmd conn
return true return true
| None -> return false | None -> return false
} }
/// 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: Permalink) webLogId = backgroundTask { let findByPermalink (permalink: Permalink) webLogId =
log.LogTrace "Page.findByPermalink" log.LogTrace "Page.findByPermalink"
use cmd = conn.CreateCommand() Custom.single
cmd.CommandText <- $" {Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} AND {linkField} = @link" $"""{Document.Query.selectByWebLog} AND {Query.whereFieldEquals linkName "@link"}"""
addWebLogId cmd webLogId [ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
addParam cmd "@link" (string permalink) fromData<Page>
use! rdr = cmd.ExecuteReaderAsync() conn
let! isFound = rdr.ReadAsync()
return if isFound then Some (Map.fromDoc<Page> ser rdr) else None
}
/// 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: Permalink list) webLogId = backgroundTask { let findCurrentPermalink (permalinks: Permalink list) webLogId =
log.LogTrace "Page.findCurrentPermalink" log.LogTrace "Page.findCurrentPermalink"
let linkSql, linkParams = inJsonArray Table.Page (nameof Page.Empty.PriorPermalinks) "link" permalinks let linkSql, linkParams = inJsonArray Table.Page (nameof Page.Empty.PriorPermalinks) "link" permalinks
use cmd = conn.CreateCommand() Custom.single
cmd.CommandText <- $"SELECT data ->> '{linkName}' AS permalink
$"SELECT {linkField} AS permalink FROM {Table.Page} WHERE {Query.whereByWebLog} AND {linkSql}" FROM {Table.Page}
addWebLogId cmd webLogId WHERE {Document.Query.whereByWebLog} AND {linkSql}"
cmd.Parameters.AddRange linkParams (webLogParam webLogId :: linkParams)
use! rdr = cmd.ExecuteReaderAsync() Map.toPermalink
let! isFound = rdr.ReadAsync() conn
return if isFound then Some (Map.toPermalink rdr) else None
}
/// 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 "Page.findFullByWebLog" log.LogTrace "Page.findFullByWebLog"
let! pages = Document.findByWebLog<Page> conn ser Table.Page webLogId let! pages = Document.findByWebLog<Page> Table.Page webLogId conn
let! withRevs = let! withRevs = pages |> List.map appendPageRevisions |> Task.WhenAll
pages
|> List.map (fun page -> backgroundTask { return! appendPageRevisions page })
|> Task.WhenAll
return List.ofArray withRevs return List.ofArray withRevs
} }
/// 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 = backgroundTask { let findListed webLogId =
log.LogTrace "Page.findListed" log.LogTrace "Page.findListed"
use cmd = conn.CreateCommand () Custom.list
cmd.CommandText <- $" $"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereFieldEquals pgListName "'true'"}
{Query.selectFromTable Table.Page} ORDER BY LOWER({titleField})"""
WHERE {Query.whereByWebLog} [ webLogParam webLogId ]
AND {pgListField} = 'true' (fun rdr -> { fromData<Page> rdr with Text = "" })
ORDER BY LOWER({titleField})" conn
addWebLogId cmd webLogId
let! pages = cmdToList<Page> cmd ser
return pages |> List.map withoutText
}
/// 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 "Page.findPageOfPages" log.LogTrace "Page.findPageOfPages"
use cmd = conn.CreateCommand() Custom.list
cmd.CommandText <- $" $"{Document.Query.selectByWebLog Table.Page} ORDER BY LOWER({titleField}) LIMIT @pageSize OFFSET @toSkip"
{Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} [ webLogParam webLogId; SqliteParameter("@pageSize", 26); SqliteParameter("@toSkip", (pageNbr - 1) * 25) ]
ORDER BY LOWER({titleField}) fromData<Page>
LIMIT @pageSize OFFSET @toSkip" conn
addWebLogId cmd webLogId
addParam cmd "@pageSize" 26 /// Save a page
addParam cmd "@toSkip" ((pageNbr - 1) * 25) let save (page: Page) = backgroundTask {
cmdToList<Page> cmd ser log.LogTrace "Page.update"
let! oldPage = findFullById page.Id page.WebLogId
do! save Table.Page { page with Revisions = [] } conn
do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions
}
/// Restore pages from a backup /// Restore pages from a backup
let restore pages = backgroundTask { let restore pages = backgroundTask {
log.LogTrace "Page.restore" log.LogTrace "Page.restore"
for page in pages do for page in pages do do! save page
do! add page
}
/// Update a page
let update (page: Page) = backgroundTask {
log.LogTrace "Page.update"
match! findFullById page.Id page.WebLogId with
| Some oldPage ->
do! Document.update conn ser Table.Page page.Id { page with Revisions = [] }
do! updatePageRevisions page.Id oldPage.Revisions page.Revisions
| None -> ()
} }
/// Update a page's prior permalinks /// Update a page's prior permalinks
@ -186,13 +155,13 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
log.LogTrace "Page.updatePriorPermalinks" log.LogTrace "Page.updatePriorPermalinks"
match! findById pageId webLogId with match! findById pageId webLogId with
| Some _ -> | Some _ ->
do! Document.updateField conn ser Table.Page pageId (nameof Page.Empty.PriorPermalinks) permalinks do! Update.partialById Table.Page pageId {| PriorPermalinks = permalinks |} conn
return true return true
| None -> return false | None -> return false
} }
interface IPageData with interface IPageData with
member _.Add page = add page member _.Add page = save page
member _.All webLogId = all webLogId member _.All webLogId = all webLogId
member _.CountAll webLogId = countAll webLogId member _.CountAll webLogId = countAll webLogId
member _.CountListed webLogId = countListed webLogId member _.CountListed webLogId = countListed webLogId
@ -205,5 +174,5 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
member _.FindListed webLogId = findListed webLogId member _.FindListed webLogId = findListed webLogId
member _.FindPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr member _.FindPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr
member _.Restore pages = restore pages member _.Restore pages = restore pages
member _.Update page = update page member _.Update page = save page
member _.UpdatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks member _.UpdatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks

View File

@ -1,84 +1,70 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open System.Threading.Tasks open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
open MyWebLog.Data open MyWebLog.Data
open Newtonsoft.Json
open NodaTime open NodaTime
/// SQLite myWebLog post data implementation /// SQLite myWebLog post data implementation
type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) = type SQLitePostData(conn: SqliteConnection, log: ILogger) =
/// The JSON field for the post's permalink /// The name of the JSON field for the post's permalink
let linkField = $"data ->> '{nameof Post.Empty.Permalink}'" let linkName = nameof Post.Empty.Permalink
/// The JSON field for when the post was published /// The JSON field for when the post was published
let publishField = $"data ->> '{nameof Post.Empty.PublishedOn}'" let publishField = $"data ->> '{nameof Post.Empty.PublishedOn}'"
/// The JSON field for post status /// The name of the JSON field for the post's status
let statField = $"data ->> '{nameof Post.Empty.Status}'" let statName = nameof Post.Empty.Status
// 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" log.LogTrace "Post.appendPostRevisions"
let! revisions = Revisions.findByEntityId conn Table.PostRevision Table.Post post.Id let! revisions = Revisions.findByEntityId Table.PostRevision Table.Post post.Id conn
return { post with Revisions = revisions } return { post with Revisions = revisions }
} }
/// The SELECT statement to retrieve posts with a web log ID parameter /// The SELECT statement to retrieve posts with a web log ID parameter
let postByWebLog = $"{Query.selectFromTable Table.Post} WHERE {Query.whereByWebLog}" let postByWebLog = Document.Query.selectByWebLog Table.Post
/// The SELECT statement to retrieve published posts with a web log ID parameter /// The SELECT statement to retrieve published posts with a web log ID parameter
let publishedPostByWebLog = $"{postByWebLog} AND {statField} = '{string Published}'" let publishedPostByWebLog = $"""{postByWebLog} AND {Query.whereFieldEquals statName $"'{string Published}'"}"""
/// Remove the text from a post
let withoutText (post: Post) =
{ post with Text = "" }
/// Update a post's revisions /// Update a post's revisions
let updatePostRevisions (postId: PostId) oldRevs newRevs = let updatePostRevisions (postId: PostId) oldRevs newRevs =
log.LogTrace "Post.updatePostRevisions" log.LogTrace "Post.updatePostRevisions"
Revisions.update conn Table.PostRevision Table.Post postId oldRevs newRevs Revisions.update Table.PostRevision Table.Post postId oldRevs newRevs conn
// IMPLEMENTATION FUNCTIONS // IMPLEMENTATION FUNCTIONS
/// Add a post
let add (post: Post) = backgroundTask {
log.LogTrace "Post.add"
do! Document.insert conn ser Table.Post { post with Revisions = [] }
do! updatePostRevisions post.Id [] post.Revisions
}
/// Count posts in a status for the given web log /// Count posts in a status for the given web log
let countByStatus (status: PostStatus) webLogId = backgroundTask { let countByStatus (status: PostStatus) webLogId =
log.LogTrace "Post.countByStatus" log.LogTrace "Post.countByStatus"
use cmd = conn.CreateCommand() Custom.scalar
cmd.CommandText <- $"{Query.countByWebLog Table.Post} AND {statField} = @status" $"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals statName "@status"}"""
addWebLogId cmd webLogId [ webLogParam webLogId; SqliteParameter("@status", string status) ]
addParam cmd "@status" (string status) (fun rdr -> int (rdr.GetInt64(0)))
return! count cmd conn
}
/// Find a post by its ID for the given web log (excluding revisions and prior permalinks /// 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" log.LogTrace "Post.findById"
Document.findByIdAndWebLog<PostId, Post> conn ser Table.Post postId webLogId Document.findByIdAndWebLog<PostId, Post> Table.Post postId webLogId conn
/// 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)
let findByPermalink (permalink: Permalink) webLogId = backgroundTask { let findByPermalink (permalink: Permalink) webLogId =
log.LogTrace "Post.findByPermalink" log.LogTrace "Post.findByPermalink"
use cmd = conn.CreateCommand() Custom.single
cmd.CommandText <- $"{Query.selectFromTable Table.Post} WHERE {Query.whereByWebLog} AND {linkField} = @link" $"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereFieldEquals linkName "@link"}"""
addWebLogId cmd webLogId [ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
addParam cmd "@link" (string permalink) fromData<Post>
use! rdr = cmd.ExecuteReaderAsync() conn
let! isFound = rdr.ReadAsync()
return if isFound then Some (Map.fromDoc<Post> ser rdr) else None
}
/// 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 {
@ -95,39 +81,34 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
log.LogTrace "Post.delete" log.LogTrace "Post.delete"
match! findById postId webLogId with match! findById postId webLogId with
| Some _ -> | Some _ ->
use cmd = conn.CreateCommand() do! Custom.nonQuery
cmd.CommandText <- $" $"""DELETE FROM {Table.PostRevision} WHERE post_id = @id;
DELETE FROM {Table.PostRevision} WHERE post_id = @id; DELETE FROM {Table.PostComment}
DELETE FROM {Table.PostComment} WHERE data ->> '{nameof Comment.Empty.PostId}' = @id; WHERE {Query.whereFieldEquals (nameof Comment.Empty.PostId) "@id"};
DELETE FROM {Table.Post} WHERE {Query.whereById}" {Query.Delete.byId Table.Post}"""
addDocId cmd postId [ idParam postId ]
do! write cmd conn
return true return true
| None -> return false | None -> return false
} }
/// 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: Permalink list) webLogId = backgroundTask { let findCurrentPermalink (permalinks: Permalink list) webLogId =
log.LogTrace "Post.findCurrentPermalink" log.LogTrace "Post.findCurrentPermalink"
let linkSql, linkParams = inJsonArray Table.Post (nameof Post.Empty.PriorPermalinks) "link" permalinks let linkSql, linkParams = inJsonArray Table.Post (nameof Post.Empty.PriorPermalinks) "link" permalinks
use cmd = conn.CreateCommand() Custom.single
cmd.CommandText <- $"SELECT data ->> '{linkName}'
$"SELECT {linkField} AS permalink FROM {Table.Post} WHERE {Query.whereByWebLog} AND {linkSql}" FROM {Table.Post}
addWebLogId cmd webLogId WHERE {Document.Query.whereByWebLog} AND {linkSql}"
cmd.Parameters.AddRange linkParams (webLogParam webLogId :: linkParams)
use! rdr = cmd.ExecuteReaderAsync() Map.toPermalink
let! isFound = rdr.ReadAsync() conn
return if isFound then Some (Map.toPermalink rdr) else None
}
/// 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" log.LogTrace "Post.findFullByWebLog"
let! posts = Document.findByWebLog<Post> conn ser Table.Post webLogId let! posts = Document.findByWebLog<Post> Table.Post webLogId conn
let! withRevs = let! withRevs = posts |> List.map appendPostRevisions |> Task.WhenAll
posts
|> List.map (fun post -> backgroundTask { return! appendPostRevisions post })
|> Task.WhenAll
return List.ofArray withRevs return List.ofArray withRevs
} }
@ -135,102 +116,91 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
let findPageOfCategorizedPosts webLogId (categoryIds: CategoryId list) pageNbr postsPerPage = let findPageOfCategorizedPosts webLogId (categoryIds: CategoryId list) pageNbr postsPerPage =
log.LogTrace "Post.findPageOfCategorizedPosts" log.LogTrace "Post.findPageOfCategorizedPosts"
let catSql, catParams = inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId" categoryIds let catSql, catParams = inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId" categoryIds
use cmd = conn.CreateCommand () Custom.list
cmd.CommandText <- $" $"{publishedPostByWebLog} AND {catSql}
{publishedPostByWebLog} AND {catSql}
ORDER BY {publishField} DESC ORDER BY {publishField} DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
addWebLogId cmd webLogId (webLogParam webLogId :: catParams)
cmd.Parameters.AddRange catParams fromData<Post>
cmdToList<Post> cmd ser conn
/// Get a page of posts for the given web log (excludes revisions) /// Get a page of posts for the given web log (excludes text and revisions)
let findPageOfPosts webLogId pageNbr postsPerPage = backgroundTask { let findPageOfPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPosts" log.LogTrace "Post.findPageOfPosts"
use cmd = conn.CreateCommand() Custom.list
cmd.CommandText <- $" $"{postByWebLog}
{postByWebLog}
ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}' ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}'
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
addWebLogId cmd webLogId [ webLogParam webLogId ]
let! posts = cmdToList<Post> cmd ser (fun rdr -> { fromData<Post> rdr with Text = "" })
return posts |> List.map withoutText conn
}
/// 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" log.LogTrace "Post.findPageOfPublishedPosts"
use cmd = conn.CreateCommand() Custom.list
cmd.CommandText <- $" $"{publishedPostByWebLog}
{publishedPostByWebLog}
ORDER BY {publishField} DESC ORDER BY {publishField} DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
addWebLogId cmd webLogId [ webLogParam webLogId ]
cmdToList<Post> cmd ser fromData<Post>
conn
/// Get a page of tagged posts for the given web log (excludes revisions) /// Get a page of tagged posts for the given web log (excludes revisions)
let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage = let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage =
log.LogTrace "Post.findPageOfTaggedPosts" log.LogTrace "Post.findPageOfTaggedPosts"
let tagSql, tagParams = inJsonArray Table.Post (nameof Post.Empty.Tags) "tag" [ tag ] let tagSql, tagParams = inJsonArray Table.Post (nameof Post.Empty.Tags) "tag" [ tag ]
use cmd = conn.CreateCommand() Custom.list
cmd.CommandText <- $" $"{publishedPostByWebLog} AND {tagSql}
{publishedPostByWebLog} AND {tagSql}
ORDER BY p.published_on DESC ORDER BY p.published_on DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
addWebLogId cmd webLogId (webLogParam webLogId :: tagParams)
cmd.Parameters.AddRange tagParams fromData<Post>
cmdToList<Post> cmd ser conn
/// 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 : Instant) = backgroundTask {
log.LogTrace "Post.findSurroundingPosts" log.LogTrace "Post.findSurroundingPosts"
use cmd = conn.CreateCommand () let! older =
addWebLogId cmd webLogId Custom.single
addParam cmd "@publishedOn" (instantParam publishedOn)
cmd.CommandText <-
$"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1" $"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1"
use! rdr = cmd.ExecuteReaderAsync() [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
let! isFound = rdr.ReadAsync() fromData<Post>
let older = if isFound then Some (Map.fromDoc<Post> ser rdr) else None conn
do! rdr.CloseAsync () let! newer =
Custom.single
cmd.CommandText <-
$"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1" $"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1"
use! rdr = cmd.ExecuteReaderAsync () [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
let! isFound = rdr.ReadAsync() fromData<Post>
let newer = if isFound then Some (Map.fromDoc<Post> ser rdr) else None conn
return older, newer return older, newer
} }
/// Save a post
let save (post: Post) = backgroundTask {
log.LogTrace "Post.save"
let! oldPost = findFullById post.Id post.WebLogId
do! save Table.Post { post with Revisions = [] } conn
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" log.LogTrace "Post.restore"
for post in posts do for post in posts do do! save post
do! add post
}
/// Update a post
let update (post: Post) = backgroundTask {
match! findFullById post.Id post.WebLogId with
| Some oldPost ->
do! Document.update conn ser Table.Post post.Id { post with Revisions = [] }
do! updatePostRevisions post.Id oldPost.Revisions post.Revisions
| None -> return ()
} }
/// Update prior permalinks for a post /// Update prior permalinks for a post
let updatePriorPermalinks postId webLogId (permalinks: Permalink list) = backgroundTask { let updatePriorPermalinks postId webLogId (permalinks: Permalink list) = backgroundTask {
match! findById postId webLogId with match! findById postId webLogId with
| Some _ -> | Some _ ->
do! Document.updateField conn ser Table.Post postId (nameof Post.Empty.PriorPermalinks) permalinks do! Update.partialById Table.Post postId {| PriorPermalinks = permalinks |} conn
return true return true
| None -> return false | None -> return false
} }
interface IPostData with interface IPostData with
member _.Add post = add post member _.Add post = save post
member _.CountByStatus status webLogId = countByStatus status webLogId member _.CountByStatus status webLogId = countByStatus status webLogId
member _.Delete postId webLogId = delete postId webLogId member _.Delete postId webLogId = delete postId webLogId
member _.FindById postId webLogId = findById postId webLogId member _.FindById postId webLogId = findById postId webLogId
@ -247,5 +217,5 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
findPageOfTaggedPosts webLogId tag pageNbr postsPerPage findPageOfTaggedPosts webLogId tag pageNbr postsPerPage
member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn
member _.Restore posts = restore posts member _.Restore posts = restore posts
member _.Update post = update post member _.Update post = save post
member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks

View File

@ -29,8 +29,8 @@ type SQLiteTagMapData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
log.LogTrace "TagMap.findByUrlValue" log.LogTrace "TagMap.findByUrlValue"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- $" cmd.CommandText <- $"
{Query.selectFromTable Table.TagMap} {QueryOld.selectFromTable Table.TagMap}
WHERE {Query.whereByWebLog} WHERE {QueryOld.whereByWebLog}
AND data ->> '{nameof TagMap.Empty.UrlValue}' = @urlValue" AND data ->> '{nameof TagMap.Empty.UrlValue}' = @urlValue"
addWebLogId cmd webLogId addWebLogId cmd webLogId
addParam cmd "@urlValue" urlValue addParam cmd "@urlValue" urlValue
@ -49,7 +49,7 @@ type SQLiteTagMapData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
log.LogTrace "TagMap.findMappingForTags" log.LogTrace "TagMap.findMappingForTags"
use cmd = conn.CreateCommand () use cmd = conn.CreateCommand ()
let mapSql, mapParams = inClause $"AND data ->> '{nameof TagMap.Empty.Tag}'" "tag" id tags let mapSql, mapParams = inClause $"AND data ->> '{nameof TagMap.Empty.Tag}'" "tag" id tags
cmd.CommandText <- $"{Query.selectFromTable Table.TagMap} WHERE {Query.whereByWebLog} {mapSql}" cmd.CommandText <- $"{QueryOld.selectFromTable Table.TagMap} WHERE {QueryOld.whereByWebLog} {mapSql}"
addWebLogId cmd webLogId addWebLogId cmd webLogId
cmd.Parameters.AddRange mapParams cmd.Parameters.AddRange mapParams
cmdToList<TagMap> cmd ser cmdToList<TagMap> cmd ser

View File

@ -20,7 +20,7 @@ type SQLiteThemeData(conn : SqliteConnection, ser: JsonSerializer, log: ILogger)
let all () = backgroundTask { let all () = backgroundTask {
log.LogTrace "Theme.all" log.LogTrace "Theme.all"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- $"{Query.selectFromTable Table.Theme} WHERE {idField} <> 'admin' ORDER BY {idField}" cmd.CommandText <- $"{QueryOld.selectFromTable Table.Theme} WHERE {idField} <> 'admin' ORDER BY {idField}"
let! themes = cmdToList<Theme> cmd ser let! themes = cmdToList<Theme> cmd ser
return themes |> List.map withoutTemplateText return themes |> List.map withoutTemplateText
} }
@ -55,7 +55,7 @@ type SQLiteThemeData(conn : SqliteConnection, ser: JsonSerializer, log: ILogger)
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- $" cmd.CommandText <- $"
DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id; DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id;
DELETE FROM {Table.Theme} WHERE {Query.whereById}" DELETE FROM {Table.Theme} WHERE {QueryOld.whereById}"
addDocId cmd themeId addDocId cmd themeId
do! write cmd do! write cmd
return true return true

View File

@ -19,7 +19,7 @@ type SQLiteWebLogData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
let all () = let all () =
log.LogTrace "WebLog.all" log.LogTrace "WebLog.all"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- Query.selectFromTable Table.WebLog cmd.CommandText <- QueryOld.selectFromTable Table.WebLog
cmdToList<WebLog> cmd ser cmdToList<WebLog> cmd ser
/// Delete a web log by its ID /// Delete a web log by its ID
@ -48,7 +48,7 @@ type SQLiteWebLogData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
log.LogTrace "WebLog.findByHost" log.LogTrace "WebLog.findByHost"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- cmd.CommandText <-
$"{Query.selectFromTable Table.WebLog} WHERE data ->> '{nameof WebLog.Empty.UrlBase}' = @urlBase" $"{QueryOld.selectFromTable Table.WebLog} WHERE data ->> '{nameof WebLog.Empty.UrlBase}' = @urlBase"
addParam cmd "@urlBase" url addParam cmd "@urlBase" url
use! rdr = cmd.ExecuteReaderAsync() use! rdr = cmd.ExecuteReaderAsync()
let! isFound = rdr.ReadAsync() let! isFound = rdr.ReadAsync()

View File

@ -62,8 +62,8 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog
log.LogTrace "WebLogUser.findByEmail" log.LogTrace "WebLogUser.findByEmail"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- $" cmd.CommandText <- $"
{Query.selectFromTable Table.WebLogUser} {QueryOld.selectFromTable Table.WebLogUser}
WHERE {Query.whereByWebLog} WHERE {QueryOld.whereByWebLog}
AND data ->> '{nameof WebLogUser.Empty.Email}' = @email" AND data ->> '{nameof WebLogUser.Empty.Email}' = @email"
addWebLogId cmd webLogId addWebLogId cmd webLogId
addParam cmd "@email" email addParam cmd "@email" email
@ -84,7 +84,7 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog
log.LogTrace "WebLogUser.findNames" log.LogTrace "WebLogUser.findNames"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
let nameSql, nameParams = inClause "AND data ->> 'Id'" "id" string userIds let nameSql, nameParams = inClause "AND data ->> 'Id'" "id" string userIds
cmd.CommandText <- $"{Query.selectFromTable Table.WebLogUser} WHERE {Query.whereByWebLog} {nameSql}" cmd.CommandText <- $"{QueryOld.selectFromTable Table.WebLogUser} WHERE {QueryOld.whereByWebLog} {nameSql}"
addWebLogId cmd webLogId addWebLogId cmd webLogId
cmd.Parameters.AddRange nameParams cmd.Parameters.AddRange nameParams
let! users = cmdToList<WebLogUser> cmd ser let! users = cmdToList<WebLogUser> cmd ser
@ -105,8 +105,8 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog
cmd.CommandText <- $" cmd.CommandText <- $"
UPDATE {Table.WebLogUser} UPDATE {Table.WebLogUser}
SET data = json_set(data, '$.{nameof WebLogUser.Empty.LastSeenOn}', @lastSeenOn) SET data = json_set(data, '$.{nameof WebLogUser.Empty.LastSeenOn}', @lastSeenOn)
WHERE {Query.whereById} WHERE {QueryOld.whereById}
AND {Query.whereByWebLog}" AND {QueryOld.whereByWebLog}"
addDocId cmd userId addDocId cmd userId
addWebLogId cmd webLogId addWebLogId cmd webLogId
addParam cmd "@lastSeenOn" (instantParam (Noda.now ())) addParam cmd "@lastSeenOn" (instantParam (Noda.now ()))

View File

@ -2,6 +2,7 @@ namespace MyWebLog.Data
open System.Threading.Tasks open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -12,9 +13,10 @@ open NodaTime
/// SQLite myWebLog data implementation /// SQLite myWebLog data implementation
type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSerializer) = type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSerializer) =
/// Create tables (and their associated indexes) if they do not exist
let ensureTables () = backgroundTask { let ensureTables () = backgroundTask {
let! tables = Custom.list<string> "SELECT name FROM sqlite_master WHERE type = 'table'" None _.GetString(0) let! tables = Custom.list<string> "SELECT name FROM sqlite_master WHERE type = 'table'" [] (_.GetString(0)) conn
let needsTable table = let needsTable table =
not (List.contains table tables) not (List.contains table tables)
@ -102,18 +104,15 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
} }
|> Seq.map (fun sql -> |> Seq.map (fun sql ->
log.LogInformation $"""Creating {(sql.Replace("IF NOT EXISTS ", "").Split ' ')[2]} table...""" log.LogInformation $"""Creating {(sql.Replace("IF NOT EXISTS ", "").Split ' ')[2]} table..."""
Custom.nonQuery sql None) Custom.nonQuery sql [] conn)
let! _ = Task.WhenAll tasks let! _ = Task.WhenAll tasks
() ()
} }
/// Set the database version to the specified version /// Set the database version to the specified version
let setDbVersion version = backgroundTask { let setDbVersion version =
use cmd = conn.CreateCommand() Custom.nonQuery $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" [] conn
cmd.CommandText <- $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')"
do! write cmd
}
/// Implement the changes between v2-rc1 and v2-rc2 /// Implement the changes between v2-rc1 and v2-rc2
let migrateV2Rc1ToV2Rc2 () = backgroundTask { let migrateV2Rc1ToV2Rc2 () = backgroundTask {
@ -418,6 +417,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
/// Migrate from v2 to v2.1 /// Migrate from v2 to v2.1
let migrateV2ToV2point1 () = backgroundTask { let migrateV2ToV2point1 () = backgroundTask {
// FIXME: This will be a backup/restore scenario, as we're changing to documents for most tables
Utils.logMigrationStep log "v2 to v2.1" "Adding redirect rules to web_log table" Utils.logMigrationStep log "v2 to v2.1" "Adding redirect rules to web_log table"
use cmd = conn.CreateCommand() use cmd = conn.CreateCommand()
cmd.CommandText <- "ALTER TABLE web_log ADD COLUMN redirect_rules TEXT NOT NULL DEFAULT '[]'" cmd.CommandText <- "ALTER TABLE web_log ADD COLUMN redirect_rules TEXT NOT NULL DEFAULT '[]'"
@ -454,8 +454,8 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
interface IData with interface IData with
member _.Category = SQLiteCategoryData (conn, ser, log) member _.Category = SQLiteCategoryData (conn, ser, log)
member _.Page = SQLitePageData (conn, ser, log) member _.Page = SQLitePageData (conn, log)
member _.Post = SQLitePostData (conn, ser, log) member _.Post = SQLitePostData (conn, log)
member _.TagMap = SQLiteTagMapData (conn, ser, log) member _.TagMap = SQLiteTagMapData (conn, ser, log)
member _.Theme = SQLiteThemeData (conn, ser, log) member _.Theme = SQLiteThemeData (conn, ser, log)
member _.ThemeAsset = SQLiteThemeAssetData (conn, log) member _.ThemeAsset = SQLiteThemeAssetData (conn, log)
@ -467,6 +467,6 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
member _.StartUp () = backgroundTask { member _.StartUp () = backgroundTask {
do! ensureTables () do! ensureTables ()
let! version = Custom.single<string> $"SELECT id FROM {Table.DbVersion}" None _.GetString(0) let! version = Custom.single<string> $"SELECT id FROM {Table.DbVersion}" [] (_.GetString(0)) conn
do! migrate version do! migrate version
} }