namespace MyWebLog.Data.SQLite open System.Threading.Tasks open BitBadger.Sqlite.FSharp.Documents open BitBadger.Sqlite.FSharp.Documents.WithConn open Microsoft.Data.Sqlite open Microsoft.Extensions.Logging open MyWebLog open MyWebLog.Data open NodaTime /// SQLite myWebLog post data implementation type SQLitePostData(conn: SqliteConnection, log: ILogger) = /// The name of the JSON field for the post's permalink let linkName = nameof Post.Empty.Permalink /// The JSON field for when the post was published let publishField = $"data ->> '{nameof Post.Empty.PublishedOn}'" /// The name of the JSON field for the post's status let statName = nameof Post.Empty.Status // SUPPORT FUNCTIONS /// Append revisions to a post let appendPostRevisions (post: Post) = backgroundTask { log.LogTrace "Post.appendPostRevisions" let! revisions = Revisions.findByEntityId Table.PostRevision Table.Post post.Id conn return { post with Revisions = revisions } } /// The SELECT statement to retrieve posts with a web log ID parameter let postByWebLog = 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}'"}""" /// Update a post's revisions let updatePostRevisions (postId: PostId) oldRevs newRevs = log.LogTrace "Post.updatePostRevisions" Revisions.update Table.PostRevision Table.Post postId oldRevs newRevs conn // IMPLEMENTATION FUNCTIONS /// 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"}""" [ webLogParam webLogId; SqliteParameter("@status", string status) ] (fun rdr -> int (rdr.GetInt64(0))) conn /// Find a post by its ID for the given web log (excluding revisions) let findById postId webLogId = log.LogTrace "Post.findById" Document.findByIdAndWebLog Table.Post postId webLogId conn /// 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"}""" [ webLogParam webLogId; SqliteParameter("@link", string permalink) ] fromData conn /// Find a complete post by its ID for the given web log let findFullById postId webLogId = backgroundTask { log.LogTrace "Post.findFullById" match! findById postId webLogId with | Some post -> let! post = appendPostRevisions post return Some post | None -> return None } /// Delete a post by its ID for the given web log let delete postId webLogId = backgroundTask { log.LogTrace "Post.delete" match! findById postId webLogId with | Some _ -> do! Custom.nonQuery $"""DELETE FROM {Table.PostRevision} WHERE post_id = @id; DELETE FROM {Table.PostComment} WHERE {Query.whereFieldEquals (nameof Comment.Empty.PostId) "@id"}; {Query.Delete.byId Table.Post}""" [ idParam postId ] conn return true | None -> return false } /// Find the current permalink from a list of potential prior permalinks for the given web log let findCurrentPermalink (permalinks: Permalink list) webLogId = log.LogTrace "Post.findCurrentPermalink" let linkSql, linkParams = inJsonArray Table.Post (nameof Post.Empty.PriorPermalinks) "link" permalinks Custom.single $"SELECT data ->> '{linkName}' FROM {Table.Post} WHERE {Document.Query.whereByWebLog} AND {linkSql}" (webLogParam webLogId :: linkParams) Map.toPermalink conn /// Get all complete posts for the given web log let findFullByWebLog webLogId = backgroundTask { log.LogTrace "Post.findFullByWebLog" let! posts = Document.findByWebLog Table.Post webLogId conn let! withRevs = posts |> List.map appendPostRevisions |> Task.WhenAll return List.ofArray withRevs } /// Get a page of categorized posts for the given web log (excludes revisions) 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 $"{publishedPostByWebLog} AND {catSql} ORDER BY {publishField} DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" (webLogParam webLogId :: catParams) fromData 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 $"{postByWebLog} ORDER BY {publishField} DESC NULLS FIRST, data ->> '{nameof Post.Empty.UpdatedOn}' LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ webLogParam webLogId ] (fun rdr -> { fromData 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 $"{publishedPostByWebLog} ORDER BY {publishField} DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" [ webLogParam webLogId ] fromData 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 $"{publishedPostByWebLog} AND {tagSql} ORDER BY p.published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" (webLogParam webLogId :: tagParams) fromData 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 $"{publishedPostByWebLog} AND {publishField} < @publishedOn ORDER BY {publishField} DESC LIMIT 1" [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] fromData conn let! newer = Custom.single $"{publishedPostByWebLog} AND {publishField} > @publishedOn ORDER BY {publishField} LIMIT 1" [ webLogParam webLogId; SqliteParameter("@publishedOn", instantParam publishedOn) ] fromData conn return older, newer } /// Save a post let save (post: Post) = backgroundTask { log.LogTrace "Post.save" let! oldPost = findFullById post.Id post.WebLogId do! save Table.Post { post with Revisions = [] } conn do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions } /// Restore posts from a backup let restore posts = backgroundTask { log.LogTrace "Post.restore" for post in posts do do! save post } /// Update prior permalinks for a post let updatePriorPermalinks postId webLogId (permalinks: Permalink list) = backgroundTask { match! findById postId webLogId with | Some _ -> do! Update.partialById Table.Post postId {| PriorPermalinks = permalinks |} conn return true | None -> return false } interface IPostData with member _.Add post = save post member _.CountByStatus status webLogId = countByStatus status webLogId member _.Delete postId webLogId = delete postId webLogId member _.FindById postId webLogId = findById postId webLogId member _.FindByPermalink permalink webLogId = findByPermalink permalink webLogId member _.FindCurrentPermalink permalinks webLogId = findCurrentPermalink permalinks webLogId member _.FindFullById postId webLogId = findFullById postId webLogId member _.FindFullByWebLog webLogId = findFullByWebLog webLogId member _.FindPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage member _.FindPageOfPosts webLogId pageNbr postsPerPage = findPageOfPosts webLogId pageNbr postsPerPage member _.FindPageOfPublishedPosts webLogId pageNbr postsPerPage = findPageOfPublishedPosts webLogId pageNbr postsPerPage member _.FindPageOfTaggedPosts webLogId tag pageNbr postsPerPage = findPageOfTaggedPosts webLogId tag pageNbr postsPerPage member _.FindSurroundingPosts webLogId publishedOn = findSurroundingPosts webLogId publishedOn member _.Restore posts = restore posts member _.Update post = save post member _.UpdatePriorPermalinks postId webLogId permalinks = updatePriorPermalinks postId webLogId permalinks