WIP on SQLite doc library
This commit is contained in:
parent
d330c97d9f
commit
bb4252f3c1
@ -76,13 +76,6 @@ let toList<'T> (it: SqliteDataReader -> 'T) (rdr: SqliteDataReader) =
|
||||
seq { while rdr.Read () do it rdr }
|
||||
|> 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
|
||||
let write (cmd: SqliteCommand) = backgroundTask {
|
||||
let! _ = cmd.ExecuteNonQueryAsync()
|
||||
@ -90,7 +83,7 @@ let write (cmd: SqliteCommand) = backgroundTask {
|
||||
}
|
||||
|
||||
/// 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
|
||||
let durationParam =
|
||||
@ -261,7 +254,8 @@ let cmdToList<'TDoc> (cmd: SqliteCommand) ser = backgroundTask {
|
||||
}
|
||||
|
||||
/// 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)
|
||||
let whereById =
|
||||
@ -292,6 +286,14 @@ module Query =
|
||||
$"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) =
|
||||
cmd.Parameters.AddWithValue(name, value) |> ignore
|
||||
|
||||
@ -307,18 +309,39 @@ let addDocParam<'TDoc> (cmd: SqliteCommand) (doc: 'TDoc) ser =
|
||||
let addWebLogId (cmd: SqliteCommand) (webLogId: WebLogId) =
|
||||
addParam cmd "@webLogId" (string webLogId)
|
||||
|
||||
open BitBadger.Sqlite.FSharp.Documents
|
||||
open BitBadger.Sqlite.FSharp.Documents.WithConn
|
||||
|
||||
/// Functions for manipulating documents
|
||||
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
|
||||
let countByWebLog (conn: SqliteConnection) table webLogId = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- Query.countByWebLog table
|
||||
addWebLogId cmd webLogId
|
||||
return! count cmd
|
||||
let countByWebLog table (webLogId: WebLogId) conn = backgroundTask {
|
||||
let! count = Count.byFieldEquals table "WebLogId" webLogId conn
|
||||
return int count
|
||||
}
|
||||
|
||||
/// Find a document by its ID
|
||||
[<Obsolete("replace this")>]
|
||||
let findById<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereById}"
|
||||
@ -329,55 +352,49 @@ module Document =
|
||||
}
|
||||
|
||||
/// Find a document by its ID and web log ID
|
||||
let findByIdAndWebLog<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) webLogId = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
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
|
||||
}
|
||||
let findByIdAndWebLog<'TKey, 'TDoc> table (key: 'TKey) webLogId conn =
|
||||
Custom.single (Query.selectByIdAndWebLog table) [ idParam key; webLogParam webLogId ] fromData<'TDoc> conn
|
||||
|
||||
/// Find documents for the given web log
|
||||
let findByWebLog<'TDoc> (conn: SqliteConnection) ser table webLogId =
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"{Query.selectFromTable table} WHERE {Query.whereByWebLog}"
|
||||
addWebLogId cmd webLogId
|
||||
cmdToList<'TDoc> cmd ser
|
||||
let findByWebLog<'TDoc> table (webLogId: WebLogId) conn =
|
||||
Find.byFieldEquals<'TDoc> table "WebLogId" webLogId conn
|
||||
|
||||
/// Insert a document
|
||||
[<Obsolete("replace this")>]
|
||||
let insert<'TDoc> (conn: SqliteConnection) ser table (doc: 'TDoc) = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- Query.insert table
|
||||
cmd.CommandText <- QueryOld.insert table
|
||||
addDocParam<'TDoc> cmd doc ser
|
||||
do! write cmd
|
||||
}
|
||||
|
||||
/// Update (replace) a document by its ID
|
||||
[<Obsolete("replace this")>]
|
||||
let update<'TKey, 'TDoc> (conn: SqliteConnection) ser table (key: 'TKey) (doc: 'TDoc) = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- Query.updateById table
|
||||
cmd.CommandText <- QueryOld.updateById table
|
||||
addDocId cmd key
|
||||
addDocParam<'TDoc> cmd doc ser
|
||||
do! write cmd
|
||||
}
|
||||
|
||||
/// Update a field in a document by its ID
|
||||
[<Obsolete("replace this")>]
|
||||
let updateField<'TKey, 'TValue> (conn: SqliteConnection) ser table (key: 'TKey) jsonField
|
||||
(value: 'TValue) = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
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
|
||||
addParam cmd "@it" (Utils.serialize ser value)
|
||||
do! write cmd
|
||||
}
|
||||
|
||||
/// Delete a document by its ID
|
||||
[<Obsolete("replace this")>]
|
||||
let delete<'TKey> (conn: SqliteConnection) table (key: 'TKey) = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- Query.deleteById table
|
||||
cmd.CommandText <- QueryOld.deleteById table
|
||||
addDocId cmd key
|
||||
do! write cmd
|
||||
}
|
||||
@ -386,29 +403,24 @@ module Document =
|
||||
module Revisions =
|
||||
|
||||
/// Find all revisions for the given entity
|
||||
let findByEntityId<'TKey> (conn: SqliteConnection) revTable entityTable (key: 'TKey) = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <-
|
||||
let findByEntityId<'TKey> revTable entityTable (key: 'TKey) conn =
|
||||
Custom.list
|
||||
$"SELECT as_of, revision_text FROM %s{revTable} WHERE %s{entityTable}_id = @id ORDER BY as_of DESC"
|
||||
addDocId cmd key
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
return toList Map.toRevision rdr
|
||||
}
|
||||
[ idParam key ]
|
||||
Map.toRevision
|
||||
conn
|
||||
|
||||
/// Find all revisions for all posts for the given web log
|
||||
let findByWebLog<'TKey> (conn: SqliteConnection) revTable entityTable (keyFunc: string -> 'TKey)
|
||||
webLogId = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <-
|
||||
let findByWebLog<'TKey> revTable entityTable (keyFunc: string -> 'TKey) webLogId conn =
|
||||
Custom.list
|
||||
$"SELECT pr.*
|
||||
FROM %s{revTable} pr
|
||||
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"
|
||||
addWebLogId cmd webLogId
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
return toList (fun rdr -> keyFunc (Map.getString $"{entityTable}_id" rdr), Map.toRevision rdr) rdr
|
||||
}
|
||||
[ webLogParam webLogId ]
|
||||
(fun rdr -> keyFunc (Map.getString $"{entityTable}_id" rdr), Map.toRevision rdr)
|
||||
conn
|
||||
|
||||
/// Parameters for a revision INSERT statement
|
||||
let revParams<'TKey> (key: 'TKey) rev =
|
||||
@ -416,26 +428,15 @@ module Revisions =
|
||||
SqliteParameter("@id", string key)
|
||||
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
|
||||
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
|
||||
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
|
||||
cmd.Parameters.Clear()
|
||||
addDocId cmd key
|
||||
addParam cmd "@asOf" delRev.AsOf
|
||||
do! write cmd
|
||||
if not (List.isEmpty toAdd) then
|
||||
cmd.CommandText <- insertSql revTable
|
||||
for addRev in toAdd do
|
||||
cmd.Parameters.Clear()
|
||||
cmd.Parameters.AddRange(revParams key addRev)
|
||||
do! write cmd
|
||||
for delRev in toDelete do
|
||||
do! Custom.nonQuery
|
||||
$"DELETE FROM %s{revTable} WHERE %s{entityTable}_id = @id AND as_of = @asOf"
|
||||
[ idParam key; SqliteParameter("@asOf", instantParam delRev.AsOf) ]
|
||||
conn
|
||||
for addRev in toAdd do
|
||||
do! Custom.nonQuery $"INSERT INTO {revTable} VALUES (@id, @asOf, @text)" (revParams key addRev) conn
|
||||
}
|
||||
|
@ -1,6 +1,8 @@
|
||||
namespace MyWebLog.Data.SQLite
|
||||
|
||||
open System.Threading.Tasks
|
||||
open BitBadger.Sqlite.FSharp.Documents
|
||||
open BitBadger.Sqlite.FSharp.Documents.WithConn
|
||||
open Microsoft.Data.Sqlite
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
@ -13,29 +15,24 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
|
||||
/// The name of the parent ID field
|
||||
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
|
||||
let countAll webLogId =
|
||||
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
|
||||
let countTopLevel webLogId = backgroundTask {
|
||||
let countTopLevel webLogId =
|
||||
log.LogTrace "Category.countTopLevel"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"{Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL"
|
||||
addWebLogId cmd webLogId
|
||||
return! count cmd
|
||||
}
|
||||
Custom.scalar
|
||||
$"{Document.Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL"
|
||||
[ webLogParam webLogId ]
|
||||
(fun rdr -> int (rdr.GetInt64(0)))
|
||||
conn
|
||||
|
||||
/// Find all categories for the given web log
|
||||
let findByWebLog webLogId =
|
||||
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
|
||||
let findAllForView webLogId = backgroundTask {
|
||||
@ -53,104 +50,74 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
|
||||
|> Seq.append (Seq.singleton it.Id)
|
||||
|> List.ofSeq
|
||||
|> inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
let query = $"""
|
||||
SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}')
|
||||
FROM {Table.Post}
|
||||
WHERE {Query.whereByWebLog}
|
||||
AND data ->> '{nameof Post.Empty.Status}' = '{string Published}'
|
||||
AND {catSql}"
|
||||
addWebLogId cmd webLogId
|
||||
cmd.Parameters.AddRange catParams
|
||||
let! postCount = count cmd
|
||||
return it.Id, postCount
|
||||
WHERE {Document.Query.whereByWebLog}
|
||||
AND {Query.whereFieldEquals (nameof Post.Empty.Status) $"'{string Published}'"}
|
||||
AND {catSql}"""
|
||||
let! postCount = Custom.scalar query (webLogParam webLogId :: catParams) (_.GetInt64(0)) conn
|
||||
return it.Id, int postCount
|
||||
})
|
||||
|> Task.WhenAll
|
||||
return
|
||||
ordered
|
||||
|> Seq.map (fun cat ->
|
||||
{ cat with
|
||||
PostCount =
|
||||
counts
|
||||
|> Array.tryFind (fun c -> fst c = cat.Id)
|
||||
|> Option.map snd
|
||||
|> Option.defaultValue 0 })
|
||||
PostCount = defaultArg (counts |> Array.tryFind (fun c -> fst c = cat.Id) |> Option.map snd) 0
|
||||
})
|
||||
|> Array.ofSeq
|
||||
}
|
||||
|
||||
/// Find a category by its ID for the given web log
|
||||
let findById catId webLogId =
|
||||
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
|
||||
let delete catId webLogId = backgroundTask {
|
||||
log.LogTrace "Category.delete"
|
||||
match! findById catId webLogId with
|
||||
| Some cat ->
|
||||
use cmd = conn.CreateCommand()
|
||||
// Reassign any children to the category's parent category
|
||||
cmd.CommandText <- $"SELECT COUNT(*) FROM {Table.Category} WHERE data ->> '{parentIdField}' = @parentId"
|
||||
addParam cmd "@parentId" (string catId)
|
||||
let! children = count cmd
|
||||
let! children = Count.byFieldEquals Table.Category parentIdField catId conn
|
||||
if children > 0 then
|
||||
cmd.CommandText <- $"
|
||||
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
|
||||
do! Update.partialByFieldEquals Table.Category parentIdField catId {| ParentId = cat.ParentId |} conn
|
||||
// Delete the category off all posts where it is assigned, and the category itself
|
||||
let catIdField = Post.Empty.CategoryIds
|
||||
cmd.CommandText <- $"
|
||||
SELECT data ->> '{Post.Empty.Id}' AS id, data -> '{catIdField}' AS cat_ids
|
||||
FROM {Table.Post}
|
||||
WHERE {Query.whereByWebLog}
|
||||
AND EXISTS
|
||||
(SELECT 1 FROM json_each({Table.Post}.data -> '{catIdField}') WHERE json_each.value = @id)"
|
||||
cmd.Parameters.Clear()
|
||||
addDocId cmd catId
|
||||
addWebLogId cmd webLogId
|
||||
use! postRdr = cmd.ExecuteReaderAsync()
|
||||
if postRdr.HasRows then
|
||||
let postIdAndCats =
|
||||
toList
|
||||
(fun rdr ->
|
||||
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
|
||||
let! posts =
|
||||
Custom.list
|
||||
$"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}'
|
||||
FROM {Table.Post}
|
||||
WHERE {Document.Query.whereByWebLog}
|
||||
AND EXISTS
|
||||
(SELECT 1
|
||||
FROM json_each({Table.Post}.data -> '{catIdField}')
|
||||
WHERE json_each.value = @id)"
|
||||
[ idParam catId; webLogParam webLogId ]
|
||||
(fun rdr -> rdr.GetString(0), Utils.deserialize<string list> ser (rdr.GetString(1)))
|
||||
conn
|
||||
for postId, cats in posts do
|
||||
do! Update.partialById
|
||||
Table.Post postId {| CategoryIds = cats |> List.filter (fun it -> it <> string catId) |} conn
|
||||
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
|
||||
}
|
||||
|
||||
/// Save a category
|
||||
let save cat =
|
||||
log.LogTrace "Category.save"
|
||||
save<Category> Table.Category cat conn
|
||||
|
||||
/// Restore categories from a backup
|
||||
let restore cats = backgroundTask {
|
||||
for cat in cats do
|
||||
do! add 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
|
||||
log.LogTrace "Category.restore"
|
||||
for cat in cats do do! save cat
|
||||
}
|
||||
|
||||
interface ICategoryData with
|
||||
member _.Add cat = add cat
|
||||
member _.Add cat = save cat
|
||||
member _.CountAll webLogId = countAll webLogId
|
||||
member _.CountTopLevel webLogId = countTopLevel webLogId
|
||||
member _.FindAllForView webLogId = findAllForView webLogId
|
||||
@ -158,4 +125,4 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
|
||||
member _.FindByWebLog webLogId = findByWebLog webLogId
|
||||
member _.Delete catId webLogId = delete catId webLogId
|
||||
member _.Restore cats = restore cats
|
||||
member _.Update cat = update cat
|
||||
member _.Update cat = save cat
|
||||
|
@ -1,20 +1,21 @@
|
||||
namespace MyWebLog.Data.SQLite
|
||||
|
||||
open System.Threading.Tasks
|
||||
open BitBadger.Sqlite.FSharp.Documents
|
||||
open BitBadger.Sqlite.FSharp.Documents.WithConn
|
||||
open Microsoft.Data.Sqlite
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
open MyWebLog.Data
|
||||
open Newtonsoft.Json
|
||||
|
||||
/// 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
|
||||
let linkField = $"data ->> '{nameof Page.Empty.Permalink}'"
|
||||
/// The JSON field name for the permalink
|
||||
let linkName = nameof Page.Empty.Permalink
|
||||
|
||||
/// The JSON field for the "is in page list" flag
|
||||
let pgListField = $"data ->> '{nameof Page.Empty.IsInPageList}'"
|
||||
/// The JSON field name for the "is in page list" flag
|
||||
let pgListName = nameof Page.Empty.IsInPageList
|
||||
|
||||
/// The JSON field for the title of the page
|
||||
let titleField = $"data ->> '{nameof Page.Empty.Title}'"
|
||||
@ -24,57 +25,44 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
|
||||
/// Append revisions to a page
|
||||
let appendPageRevisions (page : Page) = backgroundTask {
|
||||
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 a page with no text
|
||||
let withoutText (page: Page) =
|
||||
{ page with Text = "" }
|
||||
|
||||
/// Update a page's revisions
|
||||
let updatePageRevisions (pageId: PageId) oldRevs newRevs =
|
||||
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
|
||||
|
||||
/// 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)
|
||||
let all webLogId = backgroundTask {
|
||||
let all webLogId =
|
||||
log.LogTrace "Page.all"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <-
|
||||
$"{Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} ORDER BY LOWER({titleField})"
|
||||
addWebLogId cmd webLogId
|
||||
let! pages = cmdToList<Page> cmd ser
|
||||
return pages |> List.map withoutText
|
||||
}
|
||||
Custom.list
|
||||
$"{Query.selectFromTable Table.Page} WHERE {Document.Query.whereByWebLog} ORDER BY LOWER({titleField})"
|
||||
[ webLogParam webLogId ]
|
||||
(fun rdr -> { fromData<Page> rdr with Text = "" })
|
||||
conn
|
||||
|
||||
/// Count all pages for the given web log
|
||||
let countAll webLogId =
|
||||
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
|
||||
let countListed webLogId = backgroundTask {
|
||||
let countListed webLogId =
|
||||
log.LogTrace "Page.countListed"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"{Query.countByWebLog} AND {pgListField} = 'true'"
|
||||
addWebLogId cmd webLogId
|
||||
return! count cmd
|
||||
}
|
||||
Custom.scalar
|
||||
$"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals pgListName "'true'"}"""
|
||||
[ webLogParam webLogId ]
|
||||
(fun rdr -> int (rdr.GetInt64(0)))
|
||||
conn
|
||||
|
||||
/// Find a page by its ID (without revisions)
|
||||
let findById pageId webLogId =
|
||||
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
|
||||
let findFullById pageId webLogId = backgroundTask {
|
||||
@ -92,93 +80,74 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
|
||||
log.LogTrace "Page.delete"
|
||||
match! findById pageId webLogId with
|
||||
| Some _ ->
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.deleteById}"
|
||||
addDocId cmd pageId
|
||||
do! write cmd
|
||||
do! Custom.nonQuery
|
||||
$"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.Delete.byId Table.Page}"
|
||||
[ idParam pageId ]
|
||||
conn
|
||||
return true
|
||||
| None -> return false
|
||||
}
|
||||
|
||||
/// 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"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $" {Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog} AND {linkField} = @link"
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@link" (string permalink)
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
return if isFound then Some (Map.fromDoc<Page> ser rdr) else None
|
||||
}
|
||||
Custom.single
|
||||
$"""{Document.Query.selectByWebLog} AND {Query.whereFieldEquals linkName "@link"}"""
|
||||
[ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
|
||||
fromData<Page>
|
||||
conn
|
||||
|
||||
/// 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"
|
||||
let linkSql, linkParams = inJsonArray Table.Page (nameof Page.Empty.PriorPermalinks) "link" permalinks
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <-
|
||||
$"SELECT {linkField} AS permalink FROM {Table.Page} WHERE {Query.whereByWebLog} AND {linkSql}"
|
||||
addWebLogId cmd webLogId
|
||||
cmd.Parameters.AddRange linkParams
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
return if isFound then Some (Map.toPermalink rdr) else None
|
||||
}
|
||||
Custom.single
|
||||
$"SELECT data ->> '{linkName}' AS permalink
|
||||
FROM {Table.Page}
|
||||
WHERE {Document.Query.whereByWebLog} AND {linkSql}"
|
||||
(webLogParam webLogId :: linkParams)
|
||||
Map.toPermalink
|
||||
conn
|
||||
|
||||
/// Get all complete pages for the given web log
|
||||
let findFullByWebLog webLogId = backgroundTask {
|
||||
log.LogTrace "Page.findFullByWebLog"
|
||||
let! pages = Document.findByWebLog<Page> conn ser Table.Page webLogId
|
||||
let! withRevs =
|
||||
pages
|
||||
|> List.map (fun page -> backgroundTask { return! appendPageRevisions page })
|
||||
|> Task.WhenAll
|
||||
let! pages = Document.findByWebLog<Page> Table.Page webLogId conn
|
||||
let! withRevs = pages |> List.map appendPageRevisions |> Task.WhenAll
|
||||
return List.ofArray withRevs
|
||||
}
|
||||
|
||||
/// Get all listed pages for the given web log (without revisions or text)
|
||||
let findListed webLogId = backgroundTask {
|
||||
let findListed webLogId =
|
||||
log.LogTrace "Page.findListed"
|
||||
use cmd = conn.CreateCommand ()
|
||||
cmd.CommandText <- $"
|
||||
{Query.selectFromTable Table.Page}
|
||||
WHERE {Query.whereByWebLog}
|
||||
AND {pgListField} = 'true'
|
||||
ORDER BY LOWER({titleField})"
|
||||
addWebLogId cmd webLogId
|
||||
let! pages = cmdToList<Page> cmd ser
|
||||
return pages |> List.map withoutText
|
||||
}
|
||||
Custom.list
|
||||
$"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereFieldEquals pgListName "'true'"}
|
||||
ORDER BY LOWER({titleField})"""
|
||||
[ webLogParam webLogId ]
|
||||
(fun rdr -> { fromData<Page> rdr with Text = "" })
|
||||
conn
|
||||
|
||||
/// Get a page of pages for the given web log (without revisions)
|
||||
let findPageOfPages webLogId pageNbr =
|
||||
log.LogTrace "Page.findPageOfPages"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
{Query.selectFromTable Table.Page} WHERE {Query.whereByWebLog}
|
||||
ORDER BY LOWER({titleField})
|
||||
LIMIT @pageSize OFFSET @toSkip"
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@pageSize" 26
|
||||
addParam cmd "@toSkip" ((pageNbr - 1) * 25)
|
||||
cmdToList<Page> cmd ser
|
||||
Custom.list
|
||||
$"{Document.Query.selectByWebLog Table.Page} ORDER BY LOWER({titleField}) LIMIT @pageSize OFFSET @toSkip"
|
||||
[ webLogParam webLogId; SqliteParameter("@pageSize", 26); SqliteParameter("@toSkip", (pageNbr - 1) * 25) ]
|
||||
fromData<Page>
|
||||
conn
|
||||
|
||||
/// Save a page
|
||||
let save (page: Page) = backgroundTask {
|
||||
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
|
||||
let restore pages = backgroundTask {
|
||||
log.LogTrace "Page.restore"
|
||||
for page in pages do
|
||||
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 -> ()
|
||||
for page in pages do do! save page
|
||||
}
|
||||
|
||||
/// Update a page's prior permalinks
|
||||
@ -186,13 +155,13 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
|
||||
log.LogTrace "Page.updatePriorPermalinks"
|
||||
match! findById pageId webLogId with
|
||||
| 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
|
||||
| None -> return false
|
||||
| None -> return false
|
||||
}
|
||||
|
||||
interface IPageData with
|
||||
member _.Add page = add page
|
||||
member _.Add page = save page
|
||||
member _.All webLogId = all webLogId
|
||||
member _.CountAll webLogId = countAll webLogId
|
||||
member _.CountListed webLogId = countListed webLogId
|
||||
@ -205,5 +174,5 @@ type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
|
||||
member _.FindListed webLogId = findListed webLogId
|
||||
member _.FindPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr
|
||||
member _.Restore pages = restore pages
|
||||
member _.Update page = update page
|
||||
member _.Update page = save page
|
||||
member _.UpdatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks
|
||||
|
@ -1,84 +1,70 @@
|
||||
namespace MyWebLog.Data.SQLite
|
||||
|
||||
open System.Threading.Tasks
|
||||
open BitBadger.Sqlite.FSharp.Documents
|
||||
open BitBadger.Sqlite.FSharp.Documents.WithConn
|
||||
open Microsoft.Data.Sqlite
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
open MyWebLog.Data
|
||||
open Newtonsoft.Json
|
||||
open NodaTime
|
||||
|
||||
/// 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
|
||||
let linkField = $"data ->> '{nameof Post.Empty.Permalink}'"
|
||||
/// The name of the JSON field for the post's permalink
|
||||
let linkName = nameof Post.Empty.Permalink
|
||||
|
||||
/// The JSON field for when the post was published
|
||||
let publishField = $"data ->> '{nameof Post.Empty.PublishedOn}'"
|
||||
|
||||
/// The JSON field for post status
|
||||
let statField = $"data ->> '{nameof Post.Empty.Status}'"
|
||||
/// The name of the JSON field for the post's status
|
||||
let statName = nameof Post.Empty.Status
|
||||
|
||||
// SUPPORT FUNCTIONS
|
||||
|
||||
/// Append revisions to a post
|
||||
let appendPostRevisions (post: Post) = backgroundTask {
|
||||
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 }
|
||||
}
|
||||
|
||||
/// 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
|
||||
let publishedPostByWebLog = $"{postByWebLog} AND {statField} = '{string Published}'"
|
||||
|
||||
/// Remove the text from a post
|
||||
let withoutText (post: Post) =
|
||||
{ post with Text = "" }
|
||||
let publishedPostByWebLog = $"""{postByWebLog} AND {Query.whereFieldEquals statName $"'{string Published}'"}"""
|
||||
|
||||
/// Update a post's revisions
|
||||
let updatePostRevisions (postId: PostId) oldRevs newRevs =
|
||||
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
|
||||
|
||||
/// 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
|
||||
let countByStatus (status: PostStatus) webLogId = backgroundTask {
|
||||
let countByStatus (status: PostStatus) webLogId =
|
||||
log.LogTrace "Post.countByStatus"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"{Query.countByWebLog Table.Post} AND {statField} = @status"
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@status" (string status)
|
||||
return! count cmd
|
||||
}
|
||||
Custom.scalar
|
||||
$"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals statName "@status"}"""
|
||||
[ webLogParam webLogId; SqliteParameter("@status", string status) ]
|
||||
(fun rdr -> int (rdr.GetInt64(0)))
|
||||
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 =
|
||||
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)
|
||||
let findByPermalink (permalink: Permalink) webLogId = backgroundTask {
|
||||
/// Find a post by its permalink for the given web log (excluding revisions)
|
||||
let findByPermalink (permalink: Permalink) webLogId =
|
||||
log.LogTrace "Post.findByPermalink"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"{Query.selectFromTable Table.Post} WHERE {Query.whereByWebLog} AND {linkField} = @link"
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@link" (string permalink)
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
return if isFound then Some (Map.fromDoc<Post> ser rdr) else None
|
||||
}
|
||||
Custom.single
|
||||
$"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereFieldEquals linkName "@link"}"""
|
||||
[ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
|
||||
fromData<Post>
|
||||
conn
|
||||
|
||||
/// Find a complete post by its ID for the given web log
|
||||
let findFullById postId webLogId = backgroundTask {
|
||||
@ -95,39 +81,34 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
|
||||
log.LogTrace "Post.delete"
|
||||
match! findById postId webLogId with
|
||||
| Some _ ->
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
DELETE FROM {Table.PostRevision} WHERE post_id = @id;
|
||||
DELETE FROM {Table.PostComment} WHERE data ->> '{nameof Comment.Empty.PostId}' = @id;
|
||||
DELETE FROM {Table.Post} WHERE {Query.whereById}"
|
||||
addDocId cmd postId
|
||||
do! write cmd
|
||||
do! Custom.nonQuery
|
||||
$"""DELETE FROM {Table.PostRevision} WHERE post_id = @id;
|
||||
DELETE FROM {Table.PostComment}
|
||||
WHERE {Query.whereFieldEquals (nameof Comment.Empty.PostId) "@id"};
|
||||
{Query.Delete.byId Table.Post}"""
|
||||
[ idParam postId ]
|
||||
conn
|
||||
return true
|
||||
| None -> return false
|
||||
}
|
||||
|
||||
/// 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"
|
||||
let linkSql, linkParams = inJsonArray Table.Post (nameof Post.Empty.PriorPermalinks) "link" permalinks
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <-
|
||||
$"SELECT {linkField} AS permalink FROM {Table.Post} WHERE {Query.whereByWebLog} AND {linkSql}"
|
||||
addWebLogId cmd webLogId
|
||||
cmd.Parameters.AddRange linkParams
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
return if isFound then Some (Map.toPermalink rdr) else None
|
||||
}
|
||||
Custom.single
|
||||
$"SELECT data ->> '{linkName}'
|
||||
FROM {Table.Post}
|
||||
WHERE {Document.Query.whereByWebLog} AND {linkSql}"
|
||||
(webLogParam webLogId :: linkParams)
|
||||
Map.toPermalink
|
||||
conn
|
||||
|
||||
/// Get all complete posts for the given web log
|
||||
let findFullByWebLog webLogId = backgroundTask {
|
||||
log.LogTrace "Post.findFullByWebLog"
|
||||
let! posts = Document.findByWebLog<Post> conn ser Table.Post webLogId
|
||||
let! withRevs =
|
||||
posts
|
||||
|> List.map (fun post -> backgroundTask { return! appendPostRevisions post })
|
||||
|> Task.WhenAll
|
||||
let! posts = Document.findByWebLog<Post> Table.Post webLogId conn
|
||||
let! withRevs = posts |> List.map appendPostRevisions |> Task.WhenAll
|
||||
return List.ofArray withRevs
|
||||
}
|
||||
|
||||
@ -135,102 +116,91 @@ type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger) =
|
||||
let findPageOfCategorizedPosts webLogId (categoryIds: CategoryId list) pageNbr postsPerPage =
|
||||
log.LogTrace "Post.findPageOfCategorizedPosts"
|
||||
let catSql, catParams = inJsonArray Table.Post (nameof Post.Empty.CategoryIds) "catId" categoryIds
|
||||
use cmd = conn.CreateCommand ()
|
||||
cmd.CommandText <- $"
|
||||
{publishedPostByWebLog} AND {catSql}
|
||||
ORDER BY {publishField} DESC
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
addWebLogId cmd webLogId
|
||||
cmd.Parameters.AddRange catParams
|
||||
cmdToList<Post> cmd ser
|
||||
Custom.list
|
||||
$"{publishedPostByWebLog} AND {catSql}
|
||||
ORDER BY {publishField} DESC
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
(webLogParam webLogId :: catParams)
|
||||
fromData<Post>
|
||||
conn
|
||||
|
||||
/// Get a page of posts for the given web log (excludes revisions)
|
||||
let findPageOfPosts webLogId pageNbr postsPerPage = backgroundTask {
|
||||
/// Get a page of posts for the given web log (excludes text and revisions)
|
||||
let findPageOfPosts webLogId pageNbr postsPerPage =
|
||||
log.LogTrace "Post.findPageOfPosts"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
{postByWebLog}
|
||||
ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}'
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
addWebLogId cmd webLogId
|
||||
let! posts = cmdToList<Post> cmd ser
|
||||
return posts |> List.map withoutText
|
||||
}
|
||||
Custom.list
|
||||
$"{postByWebLog}
|
||||
ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}'
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
[ webLogParam webLogId ]
|
||||
(fun rdr -> { fromData<Post> rdr with Text = "" })
|
||||
conn
|
||||
|
||||
/// Get a page of published posts for the given web log (excludes revisions)
|
||||
let findPageOfPublishedPosts webLogId pageNbr postsPerPage =
|
||||
log.LogTrace "Post.findPageOfPublishedPosts"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
{publishedPostByWebLog}
|
||||
ORDER BY {publishField} DESC
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
addWebLogId cmd webLogId
|
||||
cmdToList<Post> cmd ser
|
||||
Custom.list
|
||||
$"{publishedPostByWebLog}
|
||||
ORDER BY {publishField} DESC
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
[ webLogParam webLogId ]
|
||||
fromData<Post>
|
||||
conn
|
||||
|
||||
/// Get a page of tagged posts for the given web log (excludes revisions)
|
||||
let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage =
|
||||
log.LogTrace "Post.findPageOfTaggedPosts"
|
||||
let tagSql, tagParams = inJsonArray Table.Post (nameof Post.Empty.Tags) "tag" [ tag ]
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
{publishedPostByWebLog} AND {tagSql}
|
||||
ORDER BY p.published_on DESC
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
addWebLogId cmd webLogId
|
||||
cmd.Parameters.AddRange tagParams
|
||||
cmdToList<Post> cmd ser
|
||||
Custom.list
|
||||
$"{publishedPostByWebLog} AND {tagSql}
|
||||
ORDER BY p.published_on DESC
|
||||
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
|
||||
(webLogParam webLogId :: tagParams)
|
||||
fromData<Post>
|
||||
conn
|
||||
|
||||
/// Find the next newest and oldest post from a publish date for the given web log
|
||||
let findSurroundingPosts webLogId (publishedOn : Instant) = backgroundTask {
|
||||
log.LogTrace "Post.findSurroundingPosts"
|
||||
use cmd = conn.CreateCommand ()
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@publishedOn" (instantParam publishedOn)
|
||||
|
||||
cmd.CommandText <-
|
||||
$"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1"
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
let older = if isFound then Some (Map.fromDoc<Post> ser rdr) else None
|
||||
do! rdr.CloseAsync ()
|
||||
|
||||
cmd.CommandText <-
|
||||
$"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1"
|
||||
use! rdr = cmd.ExecuteReaderAsync ()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
let newer = if isFound then Some (Map.fromDoc<Post> ser rdr) else None
|
||||
|
||||
let! older =
|
||||
Custom.single
|
||||
$"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1"
|
||||
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
|
||||
fromData<Post>
|
||||
conn
|
||||
let! newer =
|
||||
Custom.single
|
||||
$"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1"
|
||||
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
|
||||
fromData<Post>
|
||||
conn
|
||||
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
|
||||
let restore posts = backgroundTask {
|
||||
log.LogTrace "Post.restore"
|
||||
for post in posts do
|
||||
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 ()
|
||||
for post in posts do do! save post
|
||||
}
|
||||
|
||||
/// Update prior permalinks for a post
|
||||
let updatePriorPermalinks postId webLogId (permalinks: Permalink list) = backgroundTask {
|
||||
match! findById postId webLogId with
|
||||
| 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
|
||||
| None -> return false
|
||||
| None -> return false
|
||||
}
|
||||
|
||||
interface IPostData with
|
||||
member _.Add post = add post
|
||||
member _.Add post = save post
|
||||
member _.CountByStatus status webLogId = countByStatus status webLogId
|
||||
member _.Delete postId webLogId = delete 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
|
||||
member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn
|
||||
member _.Restore posts = restore posts
|
||||
member _.Update post = update post
|
||||
member _.Update post = save post
|
||||
member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks
|
||||
|
@ -29,8 +29,8 @@ type SQLiteTagMapData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
|
||||
log.LogTrace "TagMap.findByUrlValue"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
{Query.selectFromTable Table.TagMap}
|
||||
WHERE {Query.whereByWebLog}
|
||||
{QueryOld.selectFromTable Table.TagMap}
|
||||
WHERE {QueryOld.whereByWebLog}
|
||||
AND data ->> '{nameof TagMap.Empty.UrlValue}' = @urlValue"
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@urlValue" urlValue
|
||||
@ -49,7 +49,7 @@ type SQLiteTagMapData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
|
||||
log.LogTrace "TagMap.findMappingForTags"
|
||||
use cmd = conn.CreateCommand ()
|
||||
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
|
||||
cmd.Parameters.AddRange mapParams
|
||||
cmdToList<TagMap> cmd ser
|
||||
|
@ -20,7 +20,7 @@ type SQLiteThemeData(conn : SqliteConnection, ser: JsonSerializer, log: ILogger)
|
||||
let all () = backgroundTask {
|
||||
log.LogTrace "Theme.all"
|
||||
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
|
||||
return themes |> List.map withoutTemplateText
|
||||
}
|
||||
@ -55,7 +55,7 @@ type SQLiteThemeData(conn : SqliteConnection, ser: JsonSerializer, log: ILogger)
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
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
|
||||
do! write cmd
|
||||
return true
|
||||
|
@ -19,7 +19,7 @@ type SQLiteWebLogData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
|
||||
let all () =
|
||||
log.LogTrace "WebLog.all"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- Query.selectFromTable Table.WebLog
|
||||
cmd.CommandText <- QueryOld.selectFromTable Table.WebLog
|
||||
cmdToList<WebLog> cmd ser
|
||||
|
||||
/// Delete a web log by its ID
|
||||
@ -48,7 +48,7 @@ type SQLiteWebLogData(conn: SqliteConnection, ser: JsonSerializer, log: ILogger)
|
||||
log.LogTrace "WebLog.findByHost"
|
||||
use cmd = conn.CreateCommand()
|
||||
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
|
||||
use! rdr = cmd.ExecuteReaderAsync()
|
||||
let! isFound = rdr.ReadAsync()
|
||||
|
@ -62,8 +62,8 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog
|
||||
log.LogTrace "WebLogUser.findByEmail"
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"
|
||||
{Query.selectFromTable Table.WebLogUser}
|
||||
WHERE {Query.whereByWebLog}
|
||||
{QueryOld.selectFromTable Table.WebLogUser}
|
||||
WHERE {QueryOld.whereByWebLog}
|
||||
AND data ->> '{nameof WebLogUser.Empty.Email}' = @email"
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@email" email
|
||||
@ -84,7 +84,7 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog
|
||||
log.LogTrace "WebLogUser.findNames"
|
||||
use cmd = conn.CreateCommand()
|
||||
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
|
||||
cmd.Parameters.AddRange nameParams
|
||||
let! users = cmdToList<WebLogUser> cmd ser
|
||||
@ -105,8 +105,8 @@ type SQLiteWebLogUserData(conn: SqliteConnection, ser: JsonSerializer, log: ILog
|
||||
cmd.CommandText <- $"
|
||||
UPDATE {Table.WebLogUser}
|
||||
SET data = json_set(data, '$.{nameof WebLogUser.Empty.LastSeenOn}', @lastSeenOn)
|
||||
WHERE {Query.whereById}
|
||||
AND {Query.whereByWebLog}"
|
||||
WHERE {QueryOld.whereById}
|
||||
AND {QueryOld.whereByWebLog}"
|
||||
addDocId cmd userId
|
||||
addWebLogId cmd webLogId
|
||||
addParam cmd "@lastSeenOn" (instantParam (Noda.now ()))
|
||||
|
@ -2,6 +2,7 @@ namespace MyWebLog.Data
|
||||
|
||||
open System.Threading.Tasks
|
||||
open BitBadger.Sqlite.FSharp.Documents
|
||||
open BitBadger.Sqlite.FSharp.Documents.WithConn
|
||||
open Microsoft.Data.Sqlite
|
||||
open Microsoft.Extensions.Logging
|
||||
open MyWebLog
|
||||
@ -12,9 +13,10 @@ open NodaTime
|
||||
/// SQLite myWebLog data implementation
|
||||
type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSerializer) =
|
||||
|
||||
/// Create tables (and their associated indexes) if they do not exist
|
||||
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 =
|
||||
not (List.contains table tables)
|
||||
@ -102,18 +104,15 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
||||
}
|
||||
|> Seq.map (fun sql ->
|
||||
log.LogInformation $"""Creating {(sql.Replace("IF NOT EXISTS ", "").Split ' ')[2]} table..."""
|
||||
Custom.nonQuery sql None)
|
||||
Custom.nonQuery sql [] conn)
|
||||
|
||||
let! _ = Task.WhenAll tasks
|
||||
()
|
||||
}
|
||||
|
||||
/// Set the database version to the specified version
|
||||
let setDbVersion version = backgroundTask {
|
||||
use cmd = conn.CreateCommand()
|
||||
cmd.CommandText <- $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')"
|
||||
do! write cmd
|
||||
}
|
||||
let setDbVersion version =
|
||||
Custom.nonQuery $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" [] conn
|
||||
|
||||
/// Implement the changes between v2-rc1 and v2-rc2
|
||||
let migrateV2Rc1ToV2Rc2 () = backgroundTask {
|
||||
@ -418,6 +417,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
||||
|
||||
/// Migrate from v2 to v2.1
|
||||
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"
|
||||
use cmd = conn.CreateCommand()
|
||||
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
|
||||
|
||||
member _.Category = SQLiteCategoryData (conn, ser, log)
|
||||
member _.Page = SQLitePageData (conn, ser, log)
|
||||
member _.Post = SQLitePostData (conn, ser, log)
|
||||
member _.Page = SQLitePageData (conn, log)
|
||||
member _.Post = SQLitePostData (conn, log)
|
||||
member _.TagMap = SQLiteTagMapData (conn, ser, log)
|
||||
member _.Theme = SQLiteThemeData (conn, ser, log)
|
||||
member _.ThemeAsset = SQLiteThemeAssetData (conn, log)
|
||||
@ -467,6 +467,6 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
|
||||
|
||||
member _.StartUp () = backgroundTask {
|
||||
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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user