Version 2.1 #41

Merged
danieljsummers merged 123 commits from version-2.1 into main 2024-03-27 00:13:28 +00:00
12 changed files with 144 additions and 188 deletions
Showing only changes of commit 1a50c68668 - Show all commits

View File

@ -4,12 +4,9 @@
<ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" /> <ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BitBadger.Sqlite.Documents\src\BitBadger.Sqlite.FSharp.Documents\BitBadger.Sqlite.FSharp.Documents.fsproj" />
</ItemGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="BitBadger.Documents.Postgres" Version="3.0.0-rc-1" /> <PackageReference Include="BitBadger.Documents.Postgres" Version="3.0.0-rc-1" />
<PackageReference Include="BitBadger.Documents.Sqlite" Version="3.0.0-rc-1" />
<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" /> <PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Caching.Abstractions" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" /> <PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="8.0.0" />

View File

@ -1,8 +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.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -23,11 +23,10 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
/// Count all top-level categories for the given web log /// Count all top-level categories for the given web log
let countTopLevel webLogId = let countTopLevel webLogId =
log.LogTrace "Category.countTopLevel" log.LogTrace "Category.countTopLevel"
Custom.scalar conn.customScalar
$"{Document.Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL" $"{Document.Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL"
[ webLogParam webLogId ] [ webLogParam webLogId ]
(fun rdr -> int (rdr.GetInt64(0))) (toCount >> int)
conn
/// Find all categories for the given web log /// Find all categories for the given web log
let findByWebLog webLogId = let findByWebLog webLogId =
@ -54,9 +53,9 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}') SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}')
FROM {Table.Post} FROM {Table.Post}
WHERE {Document.Query.whereByWebLog} WHERE {Document.Query.whereByWebLog}
AND {Query.whereFieldEquals (nameof Post.Empty.Status) $"'{string Published}'"} AND {Query.whereByField (nameof Post.Empty.Status) EQ $"'{string Published}'"}
AND {catSql}""" AND {catSql}"""
let! postCount = Custom.scalar query (webLogParam webLogId :: catParams) (_.GetInt64(0)) conn let! postCount = conn.customScalar query (webLogParam webLogId :: catParams) toCount
return it.Id, int postCount return it.Id, int postCount
}) })
|> Task.WhenAll |> Task.WhenAll
@ -80,13 +79,13 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
match! findById catId webLogId with match! findById catId webLogId with
| Some cat -> | Some cat ->
// Reassign any children to the category's parent category // Reassign any children to the category's parent category
let! children = Count.byFieldEquals Table.Category parentIdField catId conn let! children = conn.countByField Table.Category parentIdField EQ catId
if children > 0 then if children > 0 then
do! Update.partialByFieldEquals Table.Category parentIdField catId {| ParentId = cat.ParentId |} conn do! conn.patchByField Table.Category parentIdField EQ catId {| ParentId = cat.ParentId |}
// 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
let! posts = let! posts =
Custom.list conn.customList
$"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}' $"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}'
FROM {Table.Post} FROM {Table.Post}
WHERE {Document.Query.whereByWebLog} WHERE {Document.Query.whereByWebLog}
@ -96,11 +95,10 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
WHERE json_each.value = @id)" WHERE json_each.value = @id)"
[ idParam catId; webLogParam webLogId ] [ idParam catId; webLogParam webLogId ]
(fun rdr -> rdr.GetString(0), Utils.deserialize<string list> ser (rdr.GetString(1))) (fun rdr -> rdr.GetString(0), Utils.deserialize<string list> ser (rdr.GetString(1)))
conn
for postId, cats in posts do for postId, cats in posts do
do! Update.partialById do! conn.patchById
Table.Post postId {| CategoryIds = cats |> List.filter (fun it -> it <> string catId) |} conn Table.Post postId {| CategoryIds = cats |> List.filter (fun it -> it <> string catId) |}
do! Delete.byId Table.Category catId conn do! conn.deleteById Table.Category catId
return if children = 0L then CategoryDeleted else ReassignedChildCategories return if children = 0L then CategoryDeleted else ReassignedChildCategories
| None -> return CategoryNotFound | None -> return CategoryNotFound
} }
@ -108,7 +106,7 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
/// Save a category /// Save a category
let save cat = let save cat =
log.LogTrace "Category.save" log.LogTrace "Category.save"
save<Category> Table.Category cat conn conn.save<Category> Table.Category cat
/// Restore categories from a backup /// Restore categories from a backup
let restore cats = backgroundTask { let restore cats = backgroundTask {

View File

@ -222,17 +222,14 @@ module Map =
let sqlParam name (value: obj) = let sqlParam name (value: obj) =
SqliteParameter(name, value) SqliteParameter(name, value)
/// Create a document ID parameter
let idParam (key: 'TKey) =
sqlParam "@id" (string key)
/// Create a web log ID parameter /// Create a web log ID parameter
let webLogParam (webLogId: WebLogId) = let webLogParam (webLogId: WebLogId) =
sqlParam "@webLogId" (string webLogId) sqlParam "@webLogId" (string webLogId)
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open BitBadger.Documents.Sqlite.WithConn
/// Functions for manipulating documents /// Functions for manipulating documents
module Document = module Document =
@ -242,7 +239,7 @@ module Document =
/// Fragment to add a web log ID condition to a WHERE clause (parameter @webLogId) /// Fragment to add a web log ID condition to a WHERE clause (parameter @webLogId)
let whereByWebLog = let whereByWebLog =
Query.whereFieldEquals "WebLogId" "@webLogId" Query.whereByField "WebLogId" EQ "@webLogId"
/// A SELECT query to count documents for a given web log ID /// A SELECT query to count documents for a given web log ID
let countByWebLog table = let countByWebLog table =
@ -250,7 +247,7 @@ module Document =
/// A query to select from a table by the document's ID and its web log ID /// A query to select from a table by the document's ID and its web log ID
let selectByIdAndWebLog table = let selectByIdAndWebLog table =
$"{Query.Find.byFieldEquals table} AND {whereByWebLog}" $"{Query.Find.byId table} AND {whereByWebLog}"
/// A query to select from a table by its web log ID /// A query to select from a table by its web log ID
let selectByWebLog table = let selectByWebLog table =
@ -258,7 +255,7 @@ module Document =
/// Count documents for the given web log ID /// Count documents for the given web log ID
let countByWebLog table (webLogId: WebLogId) conn = backgroundTask { let countByWebLog table (webLogId: WebLogId) conn = backgroundTask {
let! count = Count.byFieldEquals table "WebLogId" webLogId conn let! count = Count.byField table "WebLogId" EQ webLogId conn
return int count return int count
} }
@ -268,7 +265,7 @@ module Document =
/// Find documents for the given web log /// Find documents for the given web log
let findByWebLog<'TDoc> table (webLogId: WebLogId) conn = let findByWebLog<'TDoc> table (webLogId: WebLogId) conn =
Find.byFieldEquals<'TDoc> table "WebLogId" webLogId conn Find.byField<'TDoc> table "WebLogId" EQ webLogId conn
/// Functions to support revisions /// Functions to support revisions

View File

@ -1,8 +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.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -39,11 +39,10 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Get all pages for a web log (without text or revisions) /// Get all pages for a web log (without text or revisions)
let all webLogId = let all webLogId =
log.LogTrace "Page.all" log.LogTrace "Page.all"
Custom.list conn.customList
$"{Query.selectFromTable Table.Page} WHERE {Document.Query.whereByWebLog} ORDER BY LOWER({titleField})" $"{Query.selectFromTable Table.Page} WHERE {Document.Query.whereByWebLog} ORDER BY LOWER({titleField})"
[ webLogParam webLogId ] [ webLogParam webLogId ]
(fun rdr -> { fromData<Page> rdr with Text = "" }) (fun rdr -> { fromData<Page> rdr with Text = "" })
conn
/// Count all pages for the given web log /// Count all pages for the given web log
let countAll webLogId = let countAll webLogId =
@ -53,11 +52,10 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Count all pages shown in the page list for the given web log /// Count all pages shown in the page list for the given web log
let countListed webLogId = let countListed webLogId =
log.LogTrace "Page.countListed" log.LogTrace "Page.countListed"
Custom.scalar conn.customScalar
$"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals pgListName "'true'"}""" $"""{Document.Query.countByWebLog} AND {Query.whereByField pgListName EQ "'true'"}"""
[ webLogParam webLogId ] [ webLogParam webLogId ]
(fun rdr -> int (rdr.GetInt64(0))) (toCount >> int)
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 =
@ -80,10 +78,9 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "Page.delete" log.LogTrace "Page.delete"
match! findById pageId webLogId with match! findById pageId webLogId with
| Some _ -> | Some _ ->
do! Custom.nonQuery do! conn.customNonQuery
$"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.Delete.byId Table.Page}" $"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.Delete.byId Table.Page}"
[ idParam pageId ] [ idParam pageId ]
conn
return true return true
| None -> return false | None -> return false
} }
@ -91,23 +88,21 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Find a page by its permalink for the given web log /// Find a page by its permalink for the given web log
let findByPermalink (permalink: Permalink) webLogId = let findByPermalink (permalink: Permalink) webLogId =
log.LogTrace "Page.findByPermalink" log.LogTrace "Page.findByPermalink"
Custom.single conn.customSingle
$"""{Document.Query.selectByWebLog} AND {Query.whereFieldEquals linkName "@link"}""" $"""{Document.Query.selectByWebLog} AND {Query.whereByField linkName EQ "@link"}"""
[ webLogParam webLogId; SqliteParameter("@link", string permalink) ] [ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
fromData<Page> fromData<Page>
conn
/// 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 = 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
Custom.single conn.customSingle
$"SELECT data ->> '{linkName}' AS permalink $"SELECT data ->> '{linkName}' AS permalink
FROM {Table.Page} FROM {Table.Page}
WHERE {Document.Query.whereByWebLog} AND {linkSql}" WHERE {Document.Query.whereByWebLog} AND {linkSql}"
(webLogParam webLogId :: linkParams) (webLogParam webLogId :: linkParams)
Map.toPermalink Map.toPermalink
conn
/// 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 {
@ -120,27 +115,25 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Get all listed pages for the given web log (without revisions or text) /// Get all listed pages for the given web log (without revisions or text)
let findListed webLogId = let findListed webLogId =
log.LogTrace "Page.findListed" log.LogTrace "Page.findListed"
Custom.list conn.customList
$"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereFieldEquals pgListName "'true'"} $"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereByField pgListName EQ "'true'"}
ORDER BY LOWER({titleField})""" ORDER BY LOWER({titleField})"""
[ webLogParam webLogId ] [ webLogParam webLogId ]
(fun rdr -> { fromData<Page> rdr with Text = "" }) (fun rdr -> { fromData<Page> rdr with Text = "" })
conn
/// 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"
Custom.list conn.customList
$"{Document.Query.selectByWebLog Table.Page} ORDER BY LOWER({titleField}) LIMIT @pageSize OFFSET @toSkip" $"{Document.Query.selectByWebLog Table.Page} ORDER BY LOWER({titleField}) LIMIT @pageSize OFFSET @toSkip"
[ webLogParam webLogId; SqliteParameter("@pageSize", 26); SqliteParameter("@toSkip", (pageNbr - 1) * 25) ] [ webLogParam webLogId; SqliteParameter("@pageSize", 26); SqliteParameter("@toSkip", (pageNbr - 1) * 25) ]
fromData<Page> fromData<Page>
conn
/// Save a page /// Save a page
let save (page: Page) = backgroundTask { let save (page: Page) = backgroundTask {
log.LogTrace "Page.update" log.LogTrace "Page.update"
let! oldPage = findFullById page.Id page.WebLogId let! oldPage = findFullById page.Id page.WebLogId
do! save Table.Page { page with Revisions = [] } conn do! conn.save Table.Page { page with Revisions = [] }
do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions
} }
@ -155,7 +148,7 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "Page.updatePriorPermalinks" log.LogTrace "Page.updatePriorPermalinks"
match! findById pageId webLogId with match! findById pageId webLogId with
| Some _ -> | Some _ ->
do! Update.partialById Table.Page pageId {| PriorPermalinks = permalinks |} conn do! conn.patchById Table.Page pageId {| PriorPermalinks = permalinks |}
return true return true
| None -> return false | None -> return false
} }

View File

@ -1,8 +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.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -34,7 +34,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
let postByWebLog = Document.Query.selectByWebLog Table.Post 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 {Query.whereFieldEquals statName $"'{string Published}'"}""" let publishedPostByWebLog = $"""{postByWebLog} AND {Query.whereByField statName EQ $"'{string Published}'"}"""
/// Update a post's revisions /// Update a post's revisions
let updatePostRevisions (postId: PostId) oldRevs newRevs = let updatePostRevisions (postId: PostId) oldRevs newRevs =
@ -46,11 +46,10 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
/// 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 = let countByStatus (status: PostStatus) webLogId =
log.LogTrace "Post.countByStatus" log.LogTrace "Post.countByStatus"
Custom.scalar conn.customScalar
$"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals statName "@status"}""" $"""{Document.Query.countByWebLog} AND {Query.whereByField statName EQ "@status"}"""
[ webLogParam webLogId; SqliteParameter("@status", string status) ] [ webLogParam webLogId; SqliteParameter("@status", string status) ]
(fun rdr -> int (rdr.GetInt64(0))) (toCount >> int)
conn
/// Find a post by its ID for the given web log (excluding revisions) /// Find a post by its ID for the given web log (excluding revisions)
let findById postId webLogId = let findById postId webLogId =
@ -60,11 +59,10 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
/// Find a post by its permalink for the given web log (excluding revisions) /// Find a post by its permalink for the given web log (excluding revisions)
let findByPermalink (permalink: Permalink) webLogId = let findByPermalink (permalink: Permalink) webLogId =
log.LogTrace "Post.findByPermalink" log.LogTrace "Post.findByPermalink"
Custom.single conn.customSingle
$"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereFieldEquals linkName "@link"}""" $"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereByField linkName EQ "@link"}"""
[ webLogParam webLogId; SqliteParameter("@link", string permalink) ] [ webLogParam webLogId; SqliteParameter("@link", string permalink) ]
fromData<Post> fromData<Post>
conn
/// 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 {
@ -81,13 +79,12 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "Post.delete" log.LogTrace "Post.delete"
match! findById postId webLogId with match! findById postId webLogId with
| Some _ -> | Some _ ->
do! Custom.nonQuery do! conn.customNonQuery
$"""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 {Query.whereFieldEquals (nameof Comment.Empty.PostId) "@id"}; WHERE {Query.whereByField (nameof Comment.Empty.PostId) EQ "@id"};
{Query.Delete.byId Table.Post}""" {Query.Delete.byId Table.Post}"""
[ idParam postId ] [ idParam postId ]
conn
return true return true
| None -> return false | None -> return false
} }
@ -96,13 +93,12 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
let findCurrentPermalink (permalinks: Permalink list) webLogId = 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
Custom.single conn.customSingle
$"SELECT data ->> '{linkName}' AS permalink $"SELECT data ->> '{linkName}' AS permalink
FROM {Table.Post} FROM {Table.Post}
WHERE {Document.Query.whereByWebLog} AND {linkSql}" WHERE {Document.Query.whereByWebLog} AND {linkSql}"
(webLogParam webLogId :: linkParams) (webLogParam webLogId :: linkParams)
Map.toPermalink Map.toPermalink
conn
/// 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 {
@ -116,63 +112,57 @@ type SQLitePostData(conn: SqliteConnection, 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
Custom.list conn.customList
$"{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}"
(webLogParam webLogId :: catParams) (webLogParam webLogId :: catParams)
fromData<Post> fromData<Post>
conn
/// Get a page of posts for the given web log (excludes text and revisions) /// Get a page of posts for the given web log (excludes text and revisions)
let findPageOfPosts webLogId pageNbr postsPerPage = let findPageOfPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPosts" log.LogTrace "Post.findPageOfPosts"
Custom.list conn.customList
$"{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}"
[ webLogParam webLogId ] [ webLogParam webLogId ]
(fun rdr -> { fromData<Post> rdr with Text = "" }) (fun rdr -> { fromData<Post> rdr with Text = "" })
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"
Custom.list conn.customList
$"{publishedPostByWebLog} $"{publishedPostByWebLog}
ORDER BY {publishField} DESC ORDER BY {publishField} DESC
LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"
[ webLogParam webLogId ] [ webLogParam webLogId ]
fromData<Post> 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 ]
Custom.list conn.customList
$"{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}"
(webLogParam webLogId :: tagParams) (webLogParam webLogId :: tagParams)
fromData<Post> fromData<Post>
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"
let! older = let! older =
Custom.single conn.customSingle
$"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1" $"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1"
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
fromData<Post> fromData<Post>
conn
let! newer = let! newer =
Custom.single conn.customSingle
$"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1" $"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1"
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
fromData<Post> fromData<Post>
conn
return older, newer return older, newer
} }
@ -180,7 +170,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
let save (post: Post) = backgroundTask { let save (post: Post) = backgroundTask {
log.LogTrace "Post.save" log.LogTrace "Post.save"
let! oldPost = findFullById post.Id post.WebLogId let! oldPost = findFullById post.Id post.WebLogId
do! save Table.Post { post with Revisions = [] } conn do! conn.save Table.Post { post with Revisions = [] }
do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions
} }
@ -194,7 +184,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
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! Update.partialById Table.Post postId {| PriorPermalinks = permalinks |} conn do! conn.patchById Table.Post postId {| PriorPermalinks = permalinks |}
return true return true
| None -> return false | None -> return false
} }

View File

@ -1,7 +1,7 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -20,7 +20,7 @@ type SQLiteTagMapData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "TagMap.delete" log.LogTrace "TagMap.delete"
match! findById tagMapId webLogId with match! findById tagMapId webLogId with
| Some _ -> | Some _ ->
do! Delete.byId Table.TagMap tagMapId conn do! conn.deleteById Table.TagMap tagMapId
return true return true
| None -> return false | None -> return false
} }
@ -28,12 +28,11 @@ type SQLiteTagMapData(conn: SqliteConnection, log: ILogger) =
/// Find a tag mapping by its URL value for the given web log /// Find a tag mapping by its URL value for the given web log
let findByUrlValue (urlValue: string) webLogId = let findByUrlValue (urlValue: string) webLogId =
log.LogTrace "TagMap.findByUrlValue" log.LogTrace "TagMap.findByUrlValue"
Custom.single conn.customSingle
$"""{Document.Query.selectByWebLog Table.TagMap} $"""{Document.Query.selectByWebLog Table.TagMap}
AND {Query.whereFieldEquals (nameof TagMap.Empty.UrlValue) "@urlValue"}""" AND {Query.whereByField (nameof TagMap.Empty.UrlValue) EQ "@urlValue"}"""
[ webLogParam webLogId; SqliteParameter("@urlValue", urlValue) ] [ webLogParam webLogId; SqliteParameter("@urlValue", urlValue) ]
fromData<TagMap> fromData<TagMap>
conn
/// Get all tag mappings for the given web log /// Get all tag mappings for the given web log
let findByWebLog webLogId = let findByWebLog webLogId =
@ -44,16 +43,15 @@ type SQLiteTagMapData(conn: SqliteConnection, log: ILogger) =
let findMappingForTags (tags: string list) webLogId = let findMappingForTags (tags: string list) webLogId =
log.LogTrace "TagMap.findMappingForTags" log.LogTrace "TagMap.findMappingForTags"
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
Custom.list conn.customList
$"{Document.Query.selectByWebLog Table.TagMap} {mapSql}" $"{Document.Query.selectByWebLog Table.TagMap} {mapSql}"
(webLogParam webLogId :: mapParams) (webLogParam webLogId :: mapParams)
fromData<TagMap> fromData<TagMap>
conn
/// Save a tag mapping /// Save a tag mapping
let save (tagMap: TagMap) = let save (tagMap: TagMap) =
log.LogTrace "TagMap.save" log.LogTrace "TagMap.save"
save Table.TagMap tagMap conn conn.save Table.TagMap tagMap
/// Restore tag mappings from a backup /// Restore tag mappings from a backup
let restore tagMaps = backgroundTask { let restore tagMaps = backgroundTask {

View File

@ -1,7 +1,7 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -25,36 +25,34 @@ type SQLiteThemeData(conn : SqliteConnection, log: ILogger) =
/// Retrieve all themes (except 'admin'; excludes template text) /// Retrieve all themes (except 'admin'; excludes template text)
let all () = let all () =
log.LogTrace "Theme.all" log.LogTrace "Theme.all"
Custom.list conn.customList
$"{Query.selectFromTable Table.Theme} WHERE {idField} <> 'admin' ORDER BY {idField}" $"{Query.selectFromTable Table.Theme} WHERE {idField} <> 'admin' ORDER BY {idField}"
[] []
withoutTemplateText withoutTemplateText
conn
/// Does a given theme exist? /// Does a given theme exist?
let exists (themeId: ThemeId) = let exists (themeId: ThemeId) =
log.LogTrace "Theme.exists" log.LogTrace "Theme.exists"
Exists.byId Table.Theme themeId conn conn.existsById Table.Theme themeId
/// Find a theme by its ID /// Find a theme by its ID
let findById themeId = let findById themeId =
log.LogTrace "Theme.findById" log.LogTrace "Theme.findById"
Find.byId<ThemeId, Theme> Table.Theme themeId conn conn.findById<ThemeId, Theme> Table.Theme themeId
/// Find a theme by its ID (excludes the text of templates) /// Find a theme by its ID (excludes the text of templates)
let findByIdWithoutText (themeId: ThemeId) = let findByIdWithoutText (themeId: ThemeId) =
log.LogTrace "Theme.findByIdWithoutText" log.LogTrace "Theme.findByIdWithoutText"
Custom.single (Query.Find.byId Table.Theme) [ idParam themeId ] withoutTemplateText conn conn.customSingle (Query.Find.byId Table.Theme) [ idParam themeId ] withoutTemplateText
/// Delete a theme by its ID /// Delete a theme by its ID
let delete themeId = backgroundTask { let delete themeId = backgroundTask {
log.LogTrace "Theme.delete" log.LogTrace "Theme.delete"
match! findByIdWithoutText themeId with match! findByIdWithoutText themeId with
| Some _ -> | Some _ ->
do! Custom.nonQuery do! conn.customNonQuery
$"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id; {Query.Delete.byId Table.Theme}" $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id; {Query.Delete.byId Table.Theme}"
[ idParam themeId ] [ idParam themeId ]
conn
return true return true
| None -> return false | None -> return false
} }
@ -62,7 +60,7 @@ type SQLiteThemeData(conn : SqliteConnection, log: ILogger) =
/// Save a theme /// Save a theme
let save (theme: Theme) = let save (theme: Theme) =
log.LogTrace "Theme.save" log.LogTrace "Theme.save"
save Table.Theme theme conn conn.save Table.Theme theme
interface IThemeData with interface IThemeData with
member _.All() = all () member _.All() = all ()
@ -86,44 +84,41 @@ type SQLiteThemeAssetData(conn : SqliteConnection, log: ILogger) =
/// Get all theme assets (excludes data) /// Get all theme assets (excludes data)
let all () = let all () =
log.LogTrace "ThemeAsset.all" log.LogTrace "ThemeAsset.all"
Custom.list $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}" [] (Map.toThemeAsset false) conn conn.customList $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset}" [] (Map.toThemeAsset false)
/// Delete all assets for the given theme /// Delete all assets for the given theme
let deleteByTheme (themeId: ThemeId) = let deleteByTheme (themeId: ThemeId) =
log.LogTrace "ThemeAsset.deleteByTheme" log.LogTrace "ThemeAsset.deleteByTheme"
Custom.nonQuery $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id" [ idParam themeId ] conn conn.customNonQuery $"DELETE FROM {Table.ThemeAsset} WHERE theme_id = @id" [ idParam themeId ]
/// Find a theme asset by its ID /// Find a theme asset by its ID
let findById assetId = let findById assetId =
log.LogTrace "ThemeAsset.findById" log.LogTrace "ThemeAsset.findById"
Custom.single conn.customSingle
$"SELECT *, ROWID FROM {Table.ThemeAsset} WHERE theme_id = @id AND path = @path" $"SELECT *, ROWID FROM {Table.ThemeAsset} WHERE theme_id = @id AND path = @path"
(assetIdParams assetId) (assetIdParams assetId)
(Map.toThemeAsset true) (Map.toThemeAsset true)
conn
/// Get theme assets for the given theme (excludes data) /// Get theme assets for the given theme (excludes data)
let findByTheme (themeId: ThemeId) = let findByTheme (themeId: ThemeId) =
log.LogTrace "ThemeAsset.findByTheme" log.LogTrace "ThemeAsset.findByTheme"
Custom.list conn.customList
$"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset} WHERE theme_id = @id" $"SELECT theme_id, path, updated_on FROM {Table.ThemeAsset} WHERE theme_id = @id"
[ idParam themeId ] [ idParam themeId ]
(Map.toThemeAsset false) (Map.toThemeAsset false)
conn
/// Get theme assets for the given theme /// Get theme assets for the given theme
let findByThemeWithData (themeId: ThemeId) = let findByThemeWithData (themeId: ThemeId) =
log.LogTrace "ThemeAsset.findByThemeWithData" log.LogTrace "ThemeAsset.findByThemeWithData"
Custom.list conn.customList
$"SELECT *, ROWID FROM {Table.ThemeAsset} WHERE theme_id = @id" $"SELECT *, ROWID FROM {Table.ThemeAsset} WHERE theme_id = @id"
[ idParam themeId ] [ idParam themeId ]
(Map.toThemeAsset true) (Map.toThemeAsset true)
conn
/// Save a theme asset /// Save a theme asset
let save (asset: ThemeAsset) = backgroundTask { let save (asset: ThemeAsset) = backgroundTask {
log.LogTrace "ThemeAsset.save" log.LogTrace "ThemeAsset.save"
do! Custom.nonQuery do! conn.customNonQuery
$"INSERT INTO {Table.ThemeAsset} ( $"INSERT INTO {Table.ThemeAsset} (
theme_id, path, updated_on, data theme_id, path, updated_on, data
) VALUES ( ) VALUES (
@ -134,14 +129,12 @@ type SQLiteThemeAssetData(conn : SqliteConnection, log: ILogger) =
[ sqlParam "@updatedOn" (instantParam asset.UpdatedOn) [ sqlParam "@updatedOn" (instantParam asset.UpdatedOn)
sqlParam "@dataLength" asset.Data.Length sqlParam "@dataLength" asset.Data.Length
yield! (assetIdParams asset.Id) ] yield! (assetIdParams asset.Id) ]
conn
let! rowId = let! rowId =
Custom.scalar conn.customScalar
$"SELECT ROWID FROM {Table.ThemeAsset} WHERE theme_id = @id AND path = @path" $"SELECT ROWID FROM {Table.ThemeAsset} WHERE theme_id = @id AND path = @path"
(assetIdParams asset.Id) (assetIdParams asset.Id)
(_.GetInt64(0)) _.GetInt64(0)
conn
use dataStream = new MemoryStream(asset.Data) use dataStream = new MemoryStream(asset.Data)
use blobStream = new SqliteBlob(conn, Table.ThemeAsset, "data", rowId) use blobStream = new SqliteBlob(conn, Table.ThemeAsset, "data", rowId)
do! dataStream.CopyToAsync blobStream do! dataStream.CopyToAsync blobStream

View File

@ -1,7 +1,7 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open System.IO open System.IO
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -13,7 +13,7 @@ type SQLiteUploadData(conn: SqliteConnection, log: ILogger) =
/// Save an uploaded file /// Save an uploaded file
let add (upload: Upload) = backgroundTask { let add (upload: Upload) = backgroundTask {
log.LogTrace "Upload.add" log.LogTrace "Upload.add"
do! Custom.nonQuery do! conn.customNonQuery
$"INSERT INTO {Table.Upload} ( $"INSERT INTO {Table.Upload} (
id, web_log_id, path, updated_on, data id, web_log_id, path, updated_on, data
) VALUES ( ) VALUES (
@ -24,9 +24,8 @@ type SQLiteUploadData(conn: SqliteConnection, log: ILogger) =
sqlParam "@path" (string upload.Path) sqlParam "@path" (string upload.Path)
sqlParam "@updatedOn" (instantParam upload.UpdatedOn) sqlParam "@updatedOn" (instantParam upload.UpdatedOn)
sqlParam "@dataLength" upload.Data.Length ] sqlParam "@dataLength" upload.Data.Length ]
conn
let! rowId = let! rowId =
Custom.scalar $"SELECT ROWID FROM {Table.Upload} WHERE id = @id" [ idParam upload.Id ] (_.GetInt64(0)) conn conn.customScalar $"SELECT ROWID FROM {Table.Upload} WHERE id = @id" [ idParam upload.Id ] _.GetInt64(0)
use dataStream = new MemoryStream(upload.Data) use dataStream = new MemoryStream(upload.Data)
use blobStream = new SqliteBlob(conn, Table.Upload, "data", rowId) use blobStream = new SqliteBlob(conn, Table.Upload, "data", rowId)
do! dataStream.CopyToAsync blobStream do! dataStream.CopyToAsync blobStream
@ -36,14 +35,13 @@ type SQLiteUploadData(conn: SqliteConnection, log: ILogger) =
let delete (uploadId: UploadId) webLogId = backgroundTask { let delete (uploadId: UploadId) webLogId = backgroundTask {
log.LogTrace "Upload.delete" log.LogTrace "Upload.delete"
let! upload = let! upload =
Custom.single conn.customSingle
$"SELECT id, web_log_id, path, updated_on FROM {Table.Upload} WHERE id = @id AND web_log_id = @webLogId" $"SELECT id, web_log_id, path, updated_on FROM {Table.Upload} WHERE id = @id AND web_log_id = @webLogId"
[ idParam uploadId; webLogParam webLogId ] [ idParam uploadId; webLogParam webLogId ]
(Map.toUpload false) (Map.toUpload false)
conn
match upload with match upload with
| Some up -> | Some up ->
do! Custom.nonQuery $"DELETE FROM {Table.Upload} WHERE id = @id" [ idParam up.Id ] conn do! conn.customNonQuery $"DELETE FROM {Table.Upload} WHERE id = @id" [ idParam up.Id ]
return Ok (string up.Path) return Ok (string up.Path)
| None -> return Error $"Upload ID {string uploadId} not found" | None -> return Error $"Upload ID {string uploadId} not found"
} }
@ -51,29 +49,26 @@ type SQLiteUploadData(conn: SqliteConnection, log: ILogger) =
/// Find an uploaded file by its path for the given web log /// Find an uploaded file by its path for the given web log
let findByPath (path: string) webLogId = let findByPath (path: string) webLogId =
log.LogTrace "Upload.findByPath" log.LogTrace "Upload.findByPath"
Custom.single conn.customSingle
$"SELECT *, ROWID FROM {Table.Upload} WHERE web_log_id = @webLogId AND path = @path" $"SELECT *, ROWID FROM {Table.Upload} WHERE web_log_id = @webLogId AND path = @path"
[ webLogParam webLogId; sqlParam "@path" path ] [ webLogParam webLogId; sqlParam "@path" path ]
(Map.toUpload true) (Map.toUpload true)
conn
/// Find all uploaded files for the given web log (excludes data) /// Find all uploaded files for the given web log (excludes data)
let findByWebLog webLogId = let findByWebLog webLogId =
log.LogTrace "Upload.findByWebLog" log.LogTrace "Upload.findByWebLog"
Custom.list conn.customList
$"SELECT id, web_log_id, path, updated_on FROM {Table.Upload} WHERE web_log_id = @webLogId" $"SELECT id, web_log_id, path, updated_on FROM {Table.Upload} WHERE web_log_id = @webLogId"
[ webLogParam webLogId ] [ webLogParam webLogId ]
(Map.toUpload false) (Map.toUpload false)
conn
/// Find all uploaded files for the given web log /// Find all uploaded files for the given web log
let findByWebLogWithData webLogId = let findByWebLogWithData webLogId =
log.LogTrace "Upload.findByWebLogWithData" log.LogTrace "Upload.findByWebLogWithData"
Custom.list conn.customList
$"SELECT *, ROWID FROM {Table.Upload} WHERE web_log_id = @webLogId" $"SELECT *, ROWID FROM {Table.Upload} WHERE web_log_id = @webLogId"
[ webLogParam webLogId ] [ webLogParam webLogId ]
(Map.toUpload true) (Map.toUpload true)
conn
/// Restore uploads from a backup /// Restore uploads from a backup
let restore uploads = backgroundTask { let restore uploads = backgroundTask {

View File

@ -1,7 +1,7 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -13,56 +13,55 @@ type SQLiteWebLogData(conn: SqliteConnection, log: ILogger) =
/// Add a web log /// Add a web log
let add webLog = let add webLog =
log.LogTrace "WebLog.add" log.LogTrace "WebLog.add"
insert<WebLog> Table.WebLog webLog conn conn.insert<WebLog> Table.WebLog webLog
/// Retrieve all web logs /// Retrieve all web logs
let all () = let all () =
log.LogTrace "WebLog.all" log.LogTrace "WebLog.all"
Find.all<WebLog> Table.WebLog conn conn.findAll<WebLog> Table.WebLog
/// Delete a web log by its ID /// Delete a web log by its ID
let delete webLogId = let delete webLogId =
log.LogTrace "WebLog.delete" log.LogTrace "WebLog.delete"
let subQuery table = let subQuery table =
$"""(SELECT data ->> 'Id' FROM {table} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"}""" $"""(SELECT data ->> 'Id' FROM {table} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"}"""
Custom.nonQuery Custom.nonQuery
$"""DELETE FROM {Table.PostComment} WHERE data ->> 'PostId' IN {subQuery Table.Post}; $"""DELETE FROM {Table.PostComment} WHERE data ->> 'PostId' IN {subQuery Table.Post};
DELETE FROM {Table.PostRevision} WHERE post_id IN {subQuery Table.Post}; DELETE FROM {Table.PostRevision} WHERE post_id IN {subQuery Table.Post};
DELETE FROM {Table.PageRevision} WHERE page_id IN {subQuery Table.Page}; DELETE FROM {Table.PageRevision} WHERE page_id IN {subQuery Table.Page};
DELETE FROM {Table.Post} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"}; DELETE FROM {Table.Post} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.Page} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"}; DELETE FROM {Table.Page} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.Category} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"}; DELETE FROM {Table.Category} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.TagMap} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"}; DELETE FROM {Table.TagMap} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.Upload} WHERE web_log_id = @id; DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId;
DELETE FROM {Table.WebLogUser} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"}; DELETE FROM {Table.WebLogUser} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.WebLog} WHERE {Query.whereById "@webLogId"}""" DELETE FROM {Table.WebLog} WHERE {Query.whereById "@webLogId"}"""
[ webLogParam webLogId ] [ webLogParam webLogId ]
conn
/// Find a web log by its host (URL base) /// Find a web log by its host (URL base)
let findByHost (url: string) = let findByHost (url: string) =
log.LogTrace "WebLog.findByHost" log.LogTrace "WebLog.findByHost"
Find.firstByFieldEquals<WebLog> Table.WebLog (nameof WebLog.Empty.UrlBase) url conn conn.findFirstByField<WebLog> Table.WebLog (nameof WebLog.Empty.UrlBase) EQ url
/// Find a web log by its ID /// Find a web log by its ID
let findById webLogId = let findById webLogId =
log.LogTrace "WebLog.findById" log.LogTrace "WebLog.findById"
Find.byId<WebLogId, WebLog> Table.WebLog webLogId conn conn.findById<WebLogId, WebLog> Table.WebLog webLogId
/// Update redirect rules for a web log /// Update redirect rules for a web log
let updateRedirectRules (webLog: WebLog) = let updateRedirectRules (webLog: WebLog) =
log.LogTrace "WebLog.updateRedirectRules" log.LogTrace "WebLog.updateRedirectRules"
Update.partialById Table.WebLog webLog.Id {| RedirectRules = webLog.RedirectRules |} conn conn.patchById Table.WebLog webLog.Id {| RedirectRules = webLog.RedirectRules |}
/// Update RSS options for a web log /// Update RSS options for a web log
let updateRssOptions (webLog: WebLog) = let updateRssOptions (webLog: WebLog) =
log.LogTrace "WebLog.updateRssOptions" log.LogTrace "WebLog.updateRssOptions"
Update.partialById Table.WebLog webLog.Id {| Rss = webLog.Rss |} conn conn.patchById Table.WebLog webLog.Id {| Rss = webLog.Rss |}
/// Update settings for a web log /// Update settings for a web log
let updateSettings (webLog: WebLog) = let updateSettings (webLog: WebLog) =
log.LogTrace "WebLog.updateSettings" log.LogTrace "WebLog.updateSettings"
Update.full Table.WebLog webLog.Id webLog conn conn.updateById Table.WebLog webLog.Id webLog
interface IWebLogData with interface IWebLogData with
member _.Add webLog = add webLog member _.Add webLog = add webLog

View File

@ -1,7 +1,7 @@
namespace MyWebLog.Data.SQLite namespace MyWebLog.Data.SQLite
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -20,12 +20,12 @@ type SQLiteWebLogUserData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "WebLogUser.delete" log.LogTrace "WebLogUser.delete"
match! findById userId webLogId with match! findById userId webLogId with
| Some _ -> | Some _ ->
let! pageCount = Count.byFieldEquals Table.Page (nameof Page.Empty.AuthorId) (string userId) conn let! pageCount = conn.countByField Table.Page (nameof Page.Empty.AuthorId) EQ (string userId)
let! postCount = Count.byFieldEquals Table.Post (nameof Post.Empty.AuthorId) (string userId) conn let! postCount = conn.countByField Table.Post (nameof Post.Empty.AuthorId) EQ (string userId)
if pageCount + postCount > 0 then if pageCount + postCount > 0 then
return Error "User has pages or posts; cannot delete" return Error "User has pages or posts; cannot delete"
else else
do! Delete.byId Table.WebLogUser userId conn do! conn.deleteById Table.WebLogUser userId
return Ok true return Ok true
| None -> return Error "User does not exist" | None -> return Error "User does not exist"
} }
@ -33,12 +33,11 @@ type SQLiteWebLogUserData(conn: SqliteConnection, log: ILogger) =
/// Find a user by their e-mail address for the given web log /// Find a user by their e-mail address for the given web log
let findByEmail (email: string) webLogId = let findByEmail (email: string) webLogId =
log.LogTrace "WebLogUser.findByEmail" log.LogTrace "WebLogUser.findByEmail"
Custom.single conn.customSingle
$"""{Document.Query.selectByWebLog Table.WebLogUser} $"""{Document.Query.selectByWebLog Table.WebLogUser}
AND {Query.whereFieldEquals (nameof WebLogUser.Empty.Email) "@email"}""" AND {Query.whereByField (nameof WebLogUser.Empty.Email) EQ "@email"}"""
[ webLogParam webLogId; sqlParam "@email" email ] [ webLogParam webLogId; sqlParam "@email" email ]
fromData<WebLogUser> fromData<WebLogUser>
conn
/// Get all users for the given web log /// Get all users for the given web log
let findByWebLog webLogId = backgroundTask { let findByWebLog webLogId = backgroundTask {
@ -51,18 +50,17 @@ type SQLiteWebLogUserData(conn: SqliteConnection, log: ILogger) =
let findNames webLogId (userIds: WebLogUserId list) = let findNames webLogId (userIds: WebLogUserId list) =
log.LogTrace "WebLogUser.findNames" log.LogTrace "WebLogUser.findNames"
let nameSql, nameParams = inClause "AND data ->> 'Id'" "id" string userIds let nameSql, nameParams = inClause "AND data ->> 'Id'" "id" string userIds
Custom.list conn.customList
$"{Document.Query.selectByWebLog Table.WebLogUser} {nameSql}" $"{Document.Query.selectByWebLog Table.WebLogUser} {nameSql}"
(webLogParam webLogId :: nameParams) (webLogParam webLogId :: nameParams)
(fun rdr -> (fun rdr ->
let user = fromData<WebLogUser> rdr let user = fromData<WebLogUser> rdr
{ Name = string user.Id; Value = user.DisplayName }) { Name = string user.Id; Value = user.DisplayName })
conn
/// Save a user /// Save a user
let save user = let save user =
log.LogTrace "WebLogUser.update" log.LogTrace "WebLogUser.update"
save<WebLogUser> Table.WebLogUser user conn conn.save<WebLogUser> Table.WebLogUser user
/// Restore users from a backup /// Restore users from a backup
let restore users = backgroundTask { let restore users = backgroundTask {
@ -74,7 +72,7 @@ type SQLiteWebLogUserData(conn: SqliteConnection, log: ILogger) =
let setLastSeen userId webLogId = backgroundTask { let setLastSeen userId webLogId = backgroundTask {
log.LogTrace "WebLogUser.setLastSeen" log.LogTrace "WebLogUser.setLastSeen"
match! findById userId webLogId with match! findById userId webLogId with
| Some _ -> do! Update.partialById Table.WebLogUser userId {| LastSeenOn = Noda.now () |} conn | Some _ -> do! conn.patchById Table.WebLogUser userId {| LastSeenOn = Noda.now () |}
| None -> () | None -> ()
} }

View File

@ -1,8 +1,8 @@
namespace MyWebLog.Data namespace MyWebLog.Data
open System.Threading.Tasks open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents open BitBadger.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging open Microsoft.Extensions.Logging
open MyWebLog open MyWebLog
@ -16,13 +16,13 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
/// Create tables (and their associated indexes) if they do not exist /// 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'" [] (_.GetString(0)) conn let! tables = conn.customList<string> "SELECT name FROM sqlite_master WHERE type = 'table'" [] _.GetString(0)
let needsTable table = let needsTable table =
not (List.contains table tables) not (List.contains table tables)
let jsonTable table = let jsonTable table =
$"{Definition.createTable table}; {Definition.createKey table}" $"{Query.Definition.ensureTable table}; {Query.Definition.ensureKey table}"
let tasks = let tasks =
seq { seq {
@ -41,21 +41,23 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
// Category table // Category table
if needsTable Table.Category then if needsTable Table.Category then
$"{jsonTable Table.Category}; $"""{jsonTable Table.Category};
CREATE INDEX idx_{Table.Category}_web_log ON {Table.Category} ((data ->> 'WebLogId'))" {Query.Definition.ensureIndexOn Table.Category "web_log" [ nameof Category.Empty.WebLogId ]}"""
// Web log user table // Web log user table
if needsTable Table.WebLogUser then if needsTable Table.WebLogUser then
$"{jsonTable Table.WebLogUser}; $"""{jsonTable Table.WebLogUser};
CREATE INDEX idx_{Table.WebLogUser}_email {Query.Definition.ensureIndexOn
ON {Table.WebLogUser} ((data ->> 'WebLogId'), (data ->> 'Email'))" Table.WebLogUser
"email"
[ nameof WebLogUser.Empty.WebLogId; nameof WebLogUser.Empty.Email ]}"""
// Page tables // Page tables
if needsTable Table.Page then if needsTable Table.Page then
$"{jsonTable Table.Page}; $"""{jsonTable Table.Page};
CREATE INDEX idx_{Table.Page}_author ON {Table.Page} ((data ->> 'AuthorId')); {Query.Definition.ensureIndexOn Table.Page "author" [ nameof Page.Empty.AuthorId ]};
CREATE INDEX idx_{Table.Page}_permalink {Query.Definition.ensureIndexOn
ON {Table.Page} ((data ->> 'WebLogId'), (data ->> 'Permalink'))" Table.Page "permalink" [ nameof Page.Empty.WebLogId; nameof Page.Empty.Permalink ]}"""
if needsTable Table.PageRevision then if needsTable Table.PageRevision then
$"CREATE TABLE {Table.PageRevision} ( $"CREATE TABLE {Table.PageRevision} (
page_id TEXT NOT NULL, page_id TEXT NOT NULL,
@ -65,12 +67,14 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
// Post tables // Post tables
if needsTable Table.Post then if needsTable Table.Post then
$"{jsonTable Table.Post}; $"""{jsonTable Table.Post};
CREATE INDEX idx_{Table.Post}_author ON {Table.Post} ((data ->> 'AuthorId')); {Query.Definition.ensureIndexOn Table.Post "author" [ nameof Post.Empty.AuthorId ]};
CREATE INDEX idx_{Table.Post}_status {Query.Definition.ensureIndexOn
ON {Table.Post} ((data ->> 'WebLogId'), (data ->> 'Status'), (data ->> 'UpdatedOn')); Table.Post "permalink" [ nameof Post.Empty.WebLogId; nameof Post.Empty.Permalink ]};
CREATE INDEX idx_{Table.Post}_permalink {Query.Definition.ensureIndexOn
ON {Table.Post} ((data ->> 'WebLogId'), (data ->> 'Permalink'))" Table.Post
"status"
[ nameof Post.Empty.WebLogId; nameof Post.Empty.Status; nameof Post.Empty.UpdatedOn ]}"""
// TODO: index categories by post? // TODO: index categories by post?
if needsTable Table.PostRevision then if needsTable Table.PostRevision then
$"CREATE TABLE {Table.PostRevision} ( $"CREATE TABLE {Table.PostRevision} (
@ -79,13 +83,14 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
revision_text TEXT NOT NULL, revision_text TEXT NOT NULL,
PRIMARY KEY (post_id, as_of))" PRIMARY KEY (post_id, as_of))"
if needsTable Table.PostComment then if needsTable Table.PostComment then
$"{jsonTable Table.PostComment}; $"""{jsonTable Table.PostComment};
CREATE INDEX idx_{Table.PostComment}_post ON {Table.PostComment} ((data ->> 'PostId'))" {Query.Definition.ensureIndexOn Table.PostComment "post" [ nameof Comment.Empty.PostId ]}"""
// Tag map table // Tag map table
if needsTable Table.TagMap then if needsTable Table.TagMap then
$"{jsonTable Table.TagMap}; $"""{jsonTable Table.TagMap};
CREATE INDEX idx_{Table.TagMap}_tag ON {Table.TagMap} ((data ->> 'WebLogId'), (data ->> 'UrlValue'))" {Query.Definition.ensureIndexOn
Table.TagMap "url" [ nameof TagMap.Empty.WebLogId; nameof TagMap.Empty.UrlValue ]}"""
// Uploaded file table // Uploaded file table
if needsTable Table.Upload then if needsTable Table.Upload then
@ -104,7 +109,7 @@ 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 [] conn) conn.customNonQuery sql [])
let! _ = Task.WhenAll tasks let! _ = Task.WhenAll tasks
() ()
@ -112,7 +117,7 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria
/// Set the database version to the specified version /// Set the database version to the specified version
let setDbVersion version = let setDbVersion version =
Custom.nonQuery $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" [] conn conn.customNonQuery $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" []
/// Implement the changes between v2-rc1 and v2-rc2 /// Implement the changes between v2-rc1 and v2-rc2
let migrateV2Rc1ToV2Rc2 () = backgroundTask { let migrateV2Rc1ToV2Rc2 () = backgroundTask {
@ -467,6 +472,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}" [] (_.GetString(0)) conn let! version = conn.customSingle<string> $"SELECT id FROM {Table.DbVersion}" [] _.GetString(0)
do! migrate version do! migrate version
} }

View File

@ -56,9 +56,6 @@ open MyWebLog.Data
open Newtonsoft.Json open Newtonsoft.Json
open Npgsql open Npgsql
// The SQLite document library
module Sqlite = BitBadger.Sqlite.FSharp.Documents
/// Logic to obtain a data connection and implementation based on configured values /// Logic to obtain a data connection and implementation based on configured values
module DataImplementation = module DataImplementation =
@ -176,11 +173,7 @@ let main args =
| :? SQLiteData -> | :? SQLiteData ->
// ADO.NET connections are designed to work as per-request instantiation // ADO.NET connections are designed to work as per-request instantiation
let cfg = sp.GetRequiredService<IConfiguration>() let cfg = sp.GetRequiredService<IConfiguration>()
let _ = let _ = builder.Services.AddScoped<SqliteConnection>(fun sp -> Sqlite.Configuration.dbConn ())
builder.Services.AddScoped<SqliteConnection>(fun sp ->
let conn = Sqlite.Configuration.dbConn ()
conn.OpenAsync() |> Async.AwaitTask |> Async.RunSynchronously
conn)
let _ = builder.Services.AddScoped<IData, SQLiteData>() let _ = builder.Services.AddScoped<IData, SQLiteData>()
// Use SQLite for caching as well // Use SQLite for caching as well
let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db" let cachePath = defaultArg (Option.ofObj (cfg.GetConnectionString "SQLiteCachePath")) "./session.db"