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" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\..\BitBadger.Sqlite.Documents\src\BitBadger.Sqlite.FSharp.Documents\BitBadger.Sqlite.FSharp.Documents.fsproj" />
</ItemGroup>
<ItemGroup>
<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.Extensions.Caching.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
open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging
open MyWebLog
@ -23,11 +23,10 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
/// Count all top-level categories for the given web log
let countTopLevel webLogId =
log.LogTrace "Category.countTopLevel"
Custom.scalar
conn.customScalar
$"{Document.Query.countByWebLog} AND data ->> '{parentIdField}' IS NULL"
[ webLogParam webLogId ]
(fun rdr -> int (rdr.GetInt64(0)))
conn
(toCount >> int)
/// Find all categories for the given web log
let findByWebLog webLogId =
@ -54,9 +53,9 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
SELECT COUNT(DISTINCT data ->> '{nameof Post.Empty.Id}')
FROM {Table.Post}
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}"""
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
})
|> Task.WhenAll
@ -80,13 +79,13 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
match! findById catId webLogId with
| Some cat ->
// 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
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
let catIdField = Post.Empty.CategoryIds
let! posts =
Custom.list
conn.customList
$"SELECT data ->> '{Post.Empty.Id}', data -> '{catIdField}'
FROM {Table.Post}
WHERE {Document.Query.whereByWebLog}
@ -96,11 +95,10 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
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! Delete.byId Table.Category catId conn
do! conn.patchById
Table.Post postId {| CategoryIds = cats |> List.filter (fun it -> it <> string catId) |}
do! conn.deleteById Table.Category catId
return if children = 0L then CategoryDeleted else ReassignedChildCategories
| None -> return CategoryNotFound
}
@ -108,7 +106,7 @@ type SQLiteCategoryData(conn: SqliteConnection, ser: JsonSerializer, log: ILogge
/// Save a category
let save cat =
log.LogTrace "Category.save"
save<Category> Table.Category cat conn
conn.save<Category> Table.Category cat
/// Restore categories from a backup
let restore cats = backgroundTask {

View File

@ -222,17 +222,14 @@ module Map =
let sqlParam name (value: obj) =
SqliteParameter(name, value)
/// Create a document ID parameter
let idParam (key: 'TKey) =
sqlParam "@id" (string key)
/// Create a web log ID parameter
let webLogParam (webLogId: WebLogId) =
sqlParam "@webLogId" (string webLogId)
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open BitBadger.Documents.Sqlite.WithConn
/// Functions for manipulating documents
module Document =
@ -242,7 +239,7 @@ module Document =
/// Fragment to add a web log ID condition to a WHERE clause (parameter @webLogId)
let whereByWebLog =
Query.whereFieldEquals "WebLogId" "@webLogId"
Query.whereByField "WebLogId" EQ "@webLogId"
/// A SELECT query to count documents for a given web log ID
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
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
let selectByWebLog table =
@ -258,7 +255,7 @@ module Document =
/// Count documents for the given web log ID
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
}
@ -268,7 +265,7 @@ module Document =
/// Find documents for the given web log
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

View File

@ -1,8 +1,8 @@
namespace MyWebLog.Data.SQLite
open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging
open MyWebLog
@ -39,11 +39,10 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Get all pages for a web log (without text or revisions)
let all webLogId =
log.LogTrace "Page.all"
Custom.list
conn.customList
$"{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 =
@ -53,11 +52,10 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Count all pages shown in the page list for the given web log
let countListed webLogId =
log.LogTrace "Page.countListed"
Custom.scalar
$"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals pgListName "'true'"}"""
conn.customScalar
$"""{Document.Query.countByWebLog} AND {Query.whereByField pgListName EQ "'true'"}"""
[ webLogParam webLogId ]
(fun rdr -> int (rdr.GetInt64(0)))
conn
(toCount >> int)
/// Find a page by its ID (without revisions)
let findById pageId webLogId =
@ -80,10 +78,9 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "Page.delete"
match! findById pageId webLogId with
| Some _ ->
do! Custom.nonQuery
do! conn.customNonQuery
$"DELETE FROM {Table.PageRevision} WHERE page_id = @id; {Query.Delete.byId Table.Page}"
[ idParam pageId ]
conn
return true
| None -> return false
}
@ -91,23 +88,21 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Find a page by its permalink for the given web log
let findByPermalink (permalink: Permalink) webLogId =
log.LogTrace "Page.findByPermalink"
Custom.single
$"""{Document.Query.selectByWebLog} AND {Query.whereFieldEquals linkName "@link"}"""
conn.customSingle
$"""{Document.Query.selectByWebLog} AND {Query.whereByField linkName EQ "@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 =
log.LogTrace "Page.findCurrentPermalink"
let linkSql, linkParams = inJsonArray Table.Page (nameof Page.Empty.PriorPermalinks) "link" permalinks
Custom.single
conn.customSingle
$"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 {
@ -120,27 +115,25 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) =
/// Get all listed pages for the given web log (without revisions or text)
let findListed webLogId =
log.LogTrace "Page.findListed"
Custom.list
$"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereFieldEquals pgListName "'true'"}
conn.customList
$"""{Document.Query.selectByWebLog Table.Page} AND {Query.whereByField pgListName EQ "'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"
Custom.list
conn.customList
$"{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! conn.save Table.Page { page with 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"
match! findById pageId webLogId with
| Some _ ->
do! Update.partialById Table.Page pageId {| PriorPermalinks = permalinks |} conn
do! conn.patchById Table.Page pageId {| PriorPermalinks = permalinks |}
return true
| None -> return false
}

View File

@ -1,8 +1,8 @@
namespace MyWebLog.Data.SQLite
open System.Threading.Tasks
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging
open MyWebLog
@ -34,7 +34,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
let postByWebLog = Document.Query.selectByWebLog Table.Post
/// 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
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
let countByStatus (status: PostStatus) webLogId =
log.LogTrace "Post.countByStatus"
Custom.scalar
$"""{Document.Query.countByWebLog} AND {Query.whereFieldEquals statName "@status"}"""
conn.customScalar
$"""{Document.Query.countByWebLog} AND {Query.whereByField statName EQ "@status"}"""
[ webLogParam webLogId; SqliteParameter("@status", string status) ]
(fun rdr -> int (rdr.GetInt64(0)))
conn
(toCount >> int)
/// Find a post by its ID for the given web log (excluding revisions)
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)
let findByPermalink (permalink: Permalink) webLogId =
log.LogTrace "Post.findByPermalink"
Custom.single
$"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereFieldEquals linkName "@link"}"""
conn.customSingle
$"""{Document.Query.selectByWebLog Table.Post} AND {Query.whereByField linkName EQ "@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 {
@ -81,13 +79,12 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
log.LogTrace "Post.delete"
match! findById postId webLogId with
| Some _ ->
do! Custom.nonQuery
do! conn.customNonQuery
$"""DELETE FROM {Table.PostRevision} WHERE post_id = @id;
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}"""
[ idParam postId ]
conn
return true
| None -> return false
}
@ -96,13 +93,12 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
let findCurrentPermalink (permalinks: Permalink list) webLogId =
log.LogTrace "Post.findCurrentPermalink"
let linkSql, linkParams = inJsonArray Table.Post (nameof Post.Empty.PriorPermalinks) "link" permalinks
Custom.single
conn.customSingle
$"SELECT data ->> '{linkName}' AS permalink
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 {
@ -116,63 +112,57 @@ type SQLitePostData(conn: SqliteConnection, 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
Custom.list
conn.customList
$"{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 text and revisions)
let findPageOfPosts webLogId pageNbr postsPerPage =
log.LogTrace "Post.findPageOfPosts"
Custom.list
conn.customList
$"{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"
Custom.list
conn.customList
$"{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 ]
Custom.list
conn.customList
$"{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"
let! older =
Custom.single
conn.customSingle
$"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1"
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
fromData<Post>
conn
let! newer =
Custom.single
conn.customSingle
$"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1"
[ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ]
fromData<Post>
conn
return older, newer
}
@ -180,7 +170,7 @@ type SQLitePostData(conn: SqliteConnection, log: ILogger) =
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! conn.save Table.Post { post with 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 {
match! findById postId webLogId with
| Some _ ->
do! Update.partialById Table.Post postId {| PriorPermalinks = permalinks |} conn
do! conn.patchById Table.Post postId {| PriorPermalinks = permalinks |}
return true
| None -> return false
}

View File

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

View File

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

View File

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

View File

@ -1,7 +1,7 @@
namespace MyWebLog.Data.SQLite
open BitBadger.Sqlite.FSharp.Documents
open BitBadger.Sqlite.FSharp.Documents.WithConn
open BitBadger.Documents
open BitBadger.Documents.Sqlite
open Microsoft.Data.Sqlite
open Microsoft.Extensions.Logging
open MyWebLog
@ -13,56 +13,55 @@ type SQLiteWebLogData(conn: SqliteConnection, log: ILogger) =
/// Add a web log
let add webLog =
log.LogTrace "WebLog.add"
insert<WebLog> Table.WebLog webLog conn
conn.insert<WebLog> Table.WebLog webLog
/// Retrieve all web logs
let all () =
log.LogTrace "WebLog.all"
Find.all<WebLog> Table.WebLog conn
conn.findAll<WebLog> Table.WebLog
/// Delete a web log by its ID
let delete webLogId =
log.LogTrace "WebLog.delete"
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
$"""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.PageRevision} WHERE page_id IN {subQuery Table.Page};
DELETE FROM {Table.Post} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"};
DELETE FROM {Table.Page} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"};
DELETE FROM {Table.Category} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"};
DELETE FROM {Table.TagMap} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"};
DELETE FROM {Table.Upload} WHERE web_log_id = @id;
DELETE FROM {Table.WebLogUser} WHERE {Query.whereFieldEquals "WebLogId" "@webLogId"};
DELETE FROM {Table.Post} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.Page} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.Category} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.TagMap} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.Upload} WHERE web_log_id = @webLogId;
DELETE FROM {Table.WebLogUser} WHERE {Query.whereByField "WebLogId" EQ "@webLogId"};
DELETE FROM {Table.WebLog} WHERE {Query.whereById "@webLogId"}"""
[ webLogParam webLogId ]
conn
/// Find a web log by its host (URL base)
let findByHost (url: string) =
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
let findById webLogId =
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
let updateRedirectRules (webLog: WebLog) =
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
let updateRssOptions (webLog: WebLog) =
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
let updateSettings (webLog: WebLog) =
log.LogTrace "WebLog.updateSettings"
Update.full Table.WebLog webLog.Id webLog conn
conn.updateById Table.WebLog webLog.Id webLog
interface IWebLogData with
member _.Add webLog = add webLog

View File

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

View File

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

View File

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