diff --git a/src/MyWebLog.Data/MyWebLog.Data.fsproj b/src/MyWebLog.Data/MyWebLog.Data.fsproj
index 3014d6d..090e70e 100644
--- a/src/MyWebLog.Data/MyWebLog.Data.fsproj
+++ b/src/MyWebLog.Data/MyWebLog.Data.fsproj
@@ -33,6 +33,9 @@
+
+
+
diff --git a/src/MyWebLog.Data/PostgreSql/PostgreSqlCategoryData.fs b/src/MyWebLog.Data/PostgreSql/PostgreSqlCategoryData.fs
index 8783c9d..e0d7c9c 100644
--- a/src/MyWebLog.Data/PostgreSql/PostgreSqlCategoryData.fs
+++ b/src/MyWebLog.Data/PostgreSql/PostgreSqlCategoryData.fs
@@ -7,32 +7,6 @@ open Npgsql.FSharp
type PostgreSqlCategoryData (conn : NpgsqlConnection) =
- /// Add parameters for category INSERT or UPDATE statements
- let addCategoryParameters (cat : Category) =
- Sql.parameters [
- webLogIdParam cat.WebLogId
- "@id", Sql.string (CategoryId.toString cat.Id)
- "@name", Sql.string cat.Name
- "@slug", Sql.string cat.Slug
- "@description", Sql.stringOrNone cat.Description
- "@parentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString)
- ]
-
- /// Add a category
- let add cat = backgroundTask {
- let! _ =
- Sql.existingConnection conn
- |> Sql.query """
- INSERT INTO category (
- id, web_log_id, name, slug, description, parent_id
- ) VALUES (
- @id, @webLogId, @name, @slug, @description, @parentId
- )"""
- |> addCategoryParameters cat
- |> Sql.executeNonQueryAsync
- ()
- }
-
/// Count all categories for the given web log
let countAll webLogId =
Sql.existingConnection conn
@@ -54,39 +28,38 @@ type PostgreSqlCategoryData (conn : NpgsqlConnection) =
|> Sql.query "SELECT * FROM category WHERE web_log_id = @webLogId ORDER BY LOWER(name)"
|> Sql.parameters [ webLogIdParam webLogId ]
|> Sql.executeAsync Map.toCategory
- let ordered = Utils.orderByHierarchy cats None None []
+ let ordered = Utils.orderByHierarchy cats None None []
let counts =
ordered
- // |> Seq.map (fun it -> backgroundTask {
- // // Parent category post counts include posts in subcategories
- // cmd.Parameters.Clear ()
- // addWebLogId cmd webLogId
- // cmd.CommandText <- """
- // SELECT COUNT(DISTINCT p.id)
- // FROM post p
- // INNER JOIN post_category pc ON pc.post_id = p.id
- // WHERE p.web_log_id = @webLogId
- // AND p.status = 'Published'
- // AND pc.category_id IN ("""
- // ordered
- // |> Seq.filter (fun cat -> cat.ParentNames |> Array.contains it.Name)
- // |> Seq.map (fun cat -> cat.Id)
- // |> Seq.append (Seq.singleton it.Id)
- // |> Seq.iteri (fun idx item ->
- // if idx > 0 then cmd.CommandText <- $"{cmd.CommandText}, "
- // cmd.CommandText <- $"{cmd.CommandText}@catId{idx}"
- // cmd.Parameters.AddWithValue ($"@catId{idx}", item) |> ignore)
- // cmd.CommandText <- $"{cmd.CommandText})"
- // let! postCount = count cmd
- // return it.Id, postCount
- // })
- // |> Task.WhenAll
+ |> Seq.map (fun it ->
+ // Parent category post counts include posts in subcategories
+ let catIdSql, catIdParams =
+ ordered
+ |> Seq.filter (fun cat -> cat.ParentNames |> Array.contains it.Name)
+ |> Seq.map (fun cat -> cat.Id)
+ |> List.ofSeq
+ |> inClause "id" id
+ let postCount =
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ SELECT COUNT(DISTINCT p.id) AS the_count
+ FROM post p
+ INNER JOIN post_category pc ON pc.post_id = p.id
+ WHERE p.web_log_id = @webLogId
+ AND p.status = 'Published'
+ AND pc.category_id IN ({catIdSql})"""
+ |> Sql.parameters (webLogIdParam webLogId :: catIdParams)
+ |> Sql.executeRowAsync Map.toCount
+ |> Async.AwaitTask
+ |> Async.RunSynchronously
+ it.Id, postCount)
+ |> List.ofSeq
return
ordered
|> Seq.map (fun cat ->
{ cat with
PostCount = counts
- |> Array.tryFind (fun c -> fst c = cat.Id)
+ |> List.tryFind (fun c -> fst c = cat.Id)
|> Option.map snd
|> Option.defaultValue 0
})
@@ -130,51 +103,53 @@ type PostgreSqlCategoryData (conn : NpgsqlConnection) =
"@newParentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString) ]
|> Sql.executeNonQueryAsync
()
- // Delete the category off all posts where it is assigned
- let catIdParam = "@id", Sql.string (CategoryId.toString catId)
+ // Delete the category off all posts where it is assigned, and the category itself
let! _ =
Sql.existingConnection conn
|> Sql.query """
DELETE FROM post_category
WHERE category_id = @id
- AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId)"""
- |> Sql.parameters [ catIdParam; webLogIdParam webLogId ]
- |> Sql.executeNonQueryAsync
- // Delete the category itself
- let! _ =
- Sql.existingConnection conn
- |> Sql.query "DELETE FROM category WHERE id = @id"
- |> Sql.parameters [ catIdParam ]
+ AND post_id IN (SELECT id FROM post WHERE web_log_id = @webLogId);
+ DELETE FROM category WHERE id = @id"""
+ |> Sql.parameters [ "@id", Sql.string (CategoryId.toString catId); webLogIdParam webLogId ]
|> Sql.executeNonQueryAsync
return if children = 0 then CategoryDeleted else ReassignedChildCategories
| None -> return CategoryNotFound
}
- /// Restore categories from a backup
- let restore cats = backgroundTask {
- for cat in cats do
- do! add cat
- }
-
/// Update a category
- let update cat = backgroundTask {
+ let save (cat : Category) = backgroundTask {
let! _ =
Sql.existingConnection conn
|> Sql.query """
- UPDATE category
- SET name = @name,
- slug = @slug,
- description = @description,
- parent_id = @parentId
- WHERE id = @id
- AND web_log_id = @webLogId"""
- |> addCategoryParameters cat
+ INSERT INTO category (
+ id, web_log_id, name, slug, description, parent_id
+ ) VALUES (
+ @id, @webLogId, @name, @slug, @description, @parentId
+ ) ON CONFLICT (id) DO UPDATE
+ SET name = EXCLUDED.name,
+ slug = EXCLUDED.slug,
+ description = EXCLUDED.description,
+ parent_id = EXCLUDED.parent_id"""
+ |> Sql.parameters
+ [ webLogIdParam cat.WebLogId
+ "@id", Sql.string (CategoryId.toString cat.Id)
+ "@name", Sql.string cat.Name
+ "@slug", Sql.string cat.Slug
+ "@description", Sql.stringOrNone cat.Description
+ "@parentId", Sql.stringOrNone (cat.ParentId |> Option.map CategoryId.toString) ]
|> Sql.executeNonQueryAsync
()
}
+ /// Restore categories from a backup
+ let restore cats = backgroundTask {
+ for cat in cats do
+ do! save cat
+ }
+
interface ICategoryData with
- member _.Add cat = add cat
+ member _.Add cat = save cat
member _.CountAll webLogId = countAll webLogId
member _.CountTopLevel webLogId = countTopLevel webLogId
member _.FindAllForView webLogId = findAllForView webLogId
@@ -182,4 +157,4 @@ type PostgreSqlCategoryData (conn : NpgsqlConnection) =
member _.FindByWebLog webLogId = findByWebLog webLogId
member _.Delete catId webLogId = delete catId webLogId
member _.Restore cats = restore cats
- member _.Update cat = update cat
+ member _.Update cat = save cat
diff --git a/src/MyWebLog.Data/PostgreSql/PostgreSqlHelpers.fs b/src/MyWebLog.Data/PostgreSql/PostgreSqlHelpers.fs
index ed8ee91..8004f45 100644
--- a/src/MyWebLog.Data/PostgreSql/PostgreSqlHelpers.fs
+++ b/src/MyWebLog.Data/PostgreSql/PostgreSqlHelpers.fs
@@ -3,18 +3,49 @@
module MyWebLog.Data.PostgreSql.PostgreSqlHelpers
open MyWebLog
+open Newtonsoft.Json
open Npgsql.FSharp
/// Create a SQL parameter for the web log ID
let webLogIdParam webLogId =
"@webLogId", Sql.string (WebLogId.toString webLogId)
+/// Create the SQL and parameters to find a page or post by one or more prior permalinks
+let priorPermalinkSql permalinks =
+ let mutable idx = 0
+ permalinks
+ |> List.skip 1
+ |> List.fold (fun (linkSql, linkParams) it ->
+ idx <- idx + 1
+ $"{linkSql} OR prior_permalinks && ARRAY[@link{idx}]",
+ ($"@link{idx}", Sql.string (Permalink.toString it)) :: linkParams)
+ (Seq.ofList permalinks
+ |> Seq.map (fun it ->
+ "prior_permalinks && ARRAY[@link0]", [ "@link0", Sql.string (Permalink.toString it) ])
+ |> Seq.head)
+
+/// Create the SQL and parameters for an IN clause
+let inClause<'T> name (valueFunc: 'T -> string) (items : 'T list) =
+ let mutable idx = 0
+ items
+ |> List.skip 1
+ |> List.fold (fun (itemS, itemP) it ->
+ idx <- idx + 1
+ $"{itemS}, @%s{name}{idx}", ($"@%s{name}{idx}", Sql.string (valueFunc it)) :: itemP)
+ (Seq.ofList items
+ |> Seq.map (fun it -> $"@%s{name}0", [ $"@%s{name}0", Sql.string (valueFunc it) ])
+ |> Seq.head)
+
/// Mapping functions for SQL queries
module Map =
- /// Create a category from the current row in the given data reader
+ /// Map an id field to a category ID
+ let toCategoryId (row : RowReader) =
+ CategoryId (row.string "id")
+
+ /// Create a category from the current row
let toCategory (row : RowReader) : Category =
- { Id = row.string "id" |> CategoryId
+ { Id = toCategoryId row
WebLogId = row.string "web_log_id" |> WebLogId
Name = row.string "name"
Slug = row.string "slug"
@@ -25,3 +56,72 @@ module Map =
/// Get a count from a row
let toCount (row : RowReader) =
row.int "the_count"
+
+ /// Create a meta item from the current row
+ let toMetaItem (row : RowReader) : MetaItem =
+ { Name = row.string "name"
+ Value = row.string "value"
+ }
+
+ /// Create a permalink from the current row
+ let toPermalink (row : RowReader) =
+ Permalink (row.string "permalink")
+
+ /// Create a page from the current row
+ let toPage (row : RowReader) : Page =
+ { Page.empty with
+ Id = row.string "id" |> PageId
+ WebLogId = row.string "web_log_id" |> WebLogId
+ AuthorId = row.string "author_id" |> WebLogUserId
+ Title = row.string "title"
+ Permalink = toPermalink row
+ PriorPermalinks = row.stringArray "prior_permalinks" |> Array.map Permalink |> List.ofArray
+ PublishedOn = row.dateTime "published_on"
+ UpdatedOn = row.dateTime "updated_on"
+ IsInPageList = row.bool "is_in_page_list"
+ Template = row.stringOrNone "template"
+ Text = row.string "page_text"
+ Metadata = row.stringOrNone "meta_items"
+ |> Option.map JsonConvert.DeserializeObject
+ |> Option.defaultValue []
+ }
+
+ /// Create a post from the current row
+ let toPost (row : RowReader) : Post =
+ { Post.empty with
+ Id = row.string "id" |> PostId
+ WebLogId = row.string "web_log_id" |> WebLogId
+ AuthorId = row.string "author_id" |> WebLogUserId
+ Status = row.string "status" |> PostStatus.parse
+ Title = row.string "title"
+ Permalink = toPermalink row
+ PriorPermalinks = row.stringArray "prior_permalinks" |> Array.map Permalink |> List.ofArray
+ PublishedOn = row.dateTimeOrNone "published_on"
+ UpdatedOn = row.dateTime "updated_on"
+ Template = row.stringOrNone "template"
+ Text = row.string "post_text"
+ CategoryIds = row.stringArrayOrNone "category_ids"
+ |> Option.map (Array.map CategoryId >> List.ofArray)
+ |> Option.defaultValue []
+ Tags = row.stringArrayOrNone "tags"
+ |> Option.map List.ofArray
+ |> Option.defaultValue []
+ Metadata = row.stringOrNone "meta_items"
+ |> Option.map JsonConvert.DeserializeObject
+ |> Option.defaultValue []
+ Episode = row.stringOrNone "episode" |> Option.map JsonConvert.DeserializeObject
+ }
+
+ /// Create a revision from the current row
+ let toRevision (row : RowReader) : Revision =
+ { AsOf = row.dateTime "as_of"
+ Text = row.string "revision_text" |> MarkupText.parse
+ }
+
+ /// Create a tag mapping from the current row in the given data reader
+ let toTagMap (row : RowReader) : TagMap =
+ { Id = row.string "id" |> TagMapId
+ WebLogId = row.string "web_log_id" |> WebLogId
+ Tag = row.string "tag"
+ UrlValue = row.string "url_value"
+ }
diff --git a/src/MyWebLog.Data/PostgreSql/PostgreSqlPageData.fs b/src/MyWebLog.Data/PostgreSql/PostgreSqlPageData.fs
new file mode 100644
index 0000000..0fd42ee
--- /dev/null
+++ b/src/MyWebLog.Data/PostgreSql/PostgreSqlPageData.fs
@@ -0,0 +1,254 @@
+namespace MyWebLog.Data.PostgreSql
+
+open MyWebLog
+open MyWebLog.Data
+open Newtonsoft.Json
+open Npgsql
+open Npgsql.FSharp
+
+/// PostgreSQL myWebLog page data implementation
+type PostgreSqlPageData (conn : NpgsqlConnection) =
+
+ // SUPPORT FUNCTIONS
+
+ /// Append revisions and permalinks to a page
+ let appendPageRevisions (page : Page) = backgroundTask {
+ let! revisions =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT as_of, revision_text FROM page_revision WHERE page_id = @pageId ORDER BY as_of DESC"
+ |> Sql.parameters [ "@pageId", Sql.string (PageId.toString page.Id) ]
+ |> Sql.executeAsync Map.toRevision
+ return { page with Revisions = revisions }
+ }
+
+ /// Return a page with no text or revisions
+ let pageWithoutText row =
+ { Map.toPage row with Text = "" }
+
+ /// Update a page's revisions
+ let updatePageRevisions pageId oldRevs newRevs = backgroundTask {
+ let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs
+ if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.executeTransactionAsync [
+ if not (List.isEmpty toDelete) then
+ "DELETE FROM page_revision WHERE page_id = @pageId AND as_of = @asOf",
+ toDelete
+ |> List.map (fun it -> [
+ "@pageId", Sql.string (PageId.toString pageId)
+ "@asOf", Sql.timestamptz it.AsOf
+ ])
+ if not (List.isEmpty toAdd) then
+ "INSERT INTO page_revision VALUES (@pageId, @asOf, @text)",
+ toAdd
+ |> List.map (fun it -> [
+ "@pageId", Sql.string (PageId.toString pageId)
+ "@asOf", Sql.timestamptz it.AsOf
+ "@text", Sql.string (MarkupText.toString it.Text)
+ ])
+ ]
+ ()
+ }
+
+ // IMPLEMENTATION FUNCTIONS
+
+ /// Get all pages for a web log (without text, revisions, prior permalinks, or metadata)
+ let all webLogId =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId ORDER BY LOWER(title)"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync pageWithoutText
+
+ /// Count all pages for the given web log
+ let countAll webLogId =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT COUNT(id) AS the_count FROM page WHERE web_log_id = @webLogId"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeRowAsync Map.toCount
+
+ /// Count all pages shown in the page list for the given web log
+ let countListed webLogId =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT COUNT(id) AS the_count FROM page WHERE web_log_id = @webLogId AND is_in_page_list = TRUE"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeRowAsync Map.toCount
+
+ /// Find a page by its ID (without revisions)
+ let findById pageId webLogId = backgroundTask {
+ let! page =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM page WHERE id = @id AND web_log_id = @webLogId"
+ |> Sql.parameters [ "@id", Sql.string (PageId.toString pageId); webLogIdParam webLogId ]
+ |> Sql.executeAsync Map.toPage
+ return List.tryHead page
+ }
+
+ /// Find a complete page by its ID
+ let findFullById pageId webLogId = backgroundTask {
+ match! findById pageId webLogId with
+ | Some page ->
+ let! withMore = appendPageRevisions page
+ return Some withMore
+ | None -> return None
+ }
+
+ /// Delete a page by its ID
+ let delete pageId webLogId = backgroundTask {
+ match! findById pageId webLogId with
+ | Some _ ->
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query """
+ DELETE FROM page_revision WHERE page_id = @id;
+ DELETE FROM page WHERE id = @id"""
+ |> Sql.parameters [ "@id", Sql.string (PageId.toString pageId) ]
+ |> Sql.executeNonQueryAsync
+ return true
+ | None -> return false
+ }
+
+ /// Find a page by its permalink for the given web log
+ let findByPermalink permalink webLogId = backgroundTask {
+ let! page =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId AND permalink = @link"
+ |> Sql.parameters [ webLogIdParam webLogId; "@link", Sql.string (Permalink.toString permalink) ]
+ |> Sql.executeAsync Map.toPage
+ return List.tryHead page
+ }
+
+ /// Find the current permalink within a set of potential prior permalinks for the given web log
+ let findCurrentPermalink permalinks webLogId = backgroundTask {
+ if List.isEmpty permalinks then return None
+ else
+ let linkSql, linkParams = priorPermalinkSql permalinks
+ let! links =
+ Sql.existingConnection conn
+ |> Sql.query $"SELECT permalink FROM page WHERE web_log_id = @webLogId AND ({linkSql})"
+ |> Sql.parameters (webLogIdParam webLogId :: linkParams)
+ |> Sql.executeAsync Map.toPermalink
+ return List.tryHead links
+ }
+
+ /// Get all complete pages for the given web log
+ let findFullByWebLog webLogId = backgroundTask {
+ let! pages =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync Map.toPage
+ let! revisions =
+ Sql.existingConnection conn
+ |> Sql.query """
+ SELECT *
+ FROM page_revision pr
+ INNER JOIN page p ON p.id = pr.page_id
+ WHERE p.web_log_id = @webLogId
+ ORDER BY pr.as_of DESC"""
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync (fun row -> PageId (row.string "page_id"), Map.toRevision row)
+ return
+ pages
+ |> List.map (fun it ->
+ { it with Revisions = revisions |> List.filter (fun r -> fst r = it.Id) |> List.map snd })
+ }
+
+ /// Get all listed pages for the given web log (without revisions or text)
+ let findListed webLogId =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM page WHERE web_log_id = @webLogId AND is_in_page_list = TRUE ORDER BY LOWER(title)"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync pageWithoutText
+
+ /// Get a page of pages for the given web log (without revisions)
+ let findPageOfPages webLogId pageNbr =
+ Sql.existingConnection conn
+ |> Sql.query"""
+ SELECT *
+ FROM page
+ WHERE web_log_id = @webLogId
+ ORDER BY LOWER(title)
+ LIMIT @pageSize OFFSET @toSkip"""
+ |> Sql.parameters [ webLogIdParam webLogId; "@pageSize", Sql.int 26; "@toSkip", Sql.int ((pageNbr - 1) * 25) ]
+ |> Sql.executeAsync Map.toPage
+
+ /// Save a page
+ let save (page : Page) = backgroundTask {
+ let! oldPage = findFullById page.Id page.WebLogId
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query """
+ INSERT INTO page (
+ id, web_log_id, author_id, title, permalink, prior_permalinks, published_on, updated_on,
+ is_in_page_list, template, page_text, meta_items
+ ) VALUES (
+ @id, @webLogId, @authorId, @title, @permalink, @priorPermalinks, @publishedOn, @updatedOn,
+ @isInPageList, @template, @text, @metaItems
+ ) ON CONFLICT (id) DO UPDATE
+ SET author_id = EXCLUDED.author_id,
+ title = EXCLUDED.title,
+ permalink = EXCLUDED.permalink,
+ prior_permalinks = EXCLUDED.prior_permalinks,
+ published_on = EXCLUDED.published_on,
+ updated_on = EXCLUDED.updated_on,
+ is_in_page_list = EXCLUDED.is_in_page_list,
+ template = EXCLUDED.template,
+ page_text = EXCLUDED.text,
+ meta_items = EXCLUDED.meta_items"""
+ |> Sql.parameters
+ [ webLogIdParam page.WebLogId
+ "@id", Sql.string (PageId.toString page.Id)
+ "@authorId", Sql.string (WebLogUserId.toString page.AuthorId)
+ "@title", Sql.string page.Title
+ "@permalink", Sql.string (Permalink.toString page.Permalink)
+ "@publishedOn", Sql.timestamptz page.PublishedOn
+ "@updatedOn", Sql.timestamptz page.UpdatedOn
+ "@isInPageList", Sql.bool page.IsInPageList
+ "@template", Sql.stringOrNone page.Template
+ "@text", Sql.string page.Text
+ "@metaItems", Sql.jsonb (JsonConvert.SerializeObject page.Metadata)
+ "@priorPermalinks",
+ Sql.stringArray (page.PriorPermalinks |> List.map Permalink.toString |> Array.ofList) ]
+ |> Sql.executeNonQueryAsync
+ do! updatePageRevisions page.Id (match oldPage with Some p -> p.Revisions | None -> []) page.Revisions
+ ()
+ }
+
+ /// Restore pages from a backup
+ let restore pages = backgroundTask {
+ for page in pages do
+ do! save page
+ }
+
+ /// Update a page's prior permalinks
+ let updatePriorPermalinks pageId webLogId permalinks = backgroundTask {
+ match! findById pageId webLogId with
+ | Some _ ->
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query "UPDATE page SET prior_permalinks = @prior WHERE id = @id"
+ |> Sql.parameters
+ [ "@id", Sql.string (PageId.toString pageId)
+ "@prior", Sql.stringArray (permalinks |> List.map Permalink.toString |> Array.ofList) ]
+ |> Sql.executeNonQueryAsync
+ return true
+ | None -> return false
+ }
+
+ interface IPageData with
+ member _.Add page = save page
+ member _.All webLogId = all webLogId
+ member _.CountAll webLogId = countAll webLogId
+ member _.CountListed webLogId = countListed webLogId
+ member _.Delete pageId webLogId = delete pageId webLogId
+ member _.FindById pageId webLogId = findById pageId webLogId
+ member _.FindByPermalink permalink webLogId = findByPermalink permalink webLogId
+ member _.FindCurrentPermalink permalinks webLogId = findCurrentPermalink permalinks webLogId
+ member _.FindFullById pageId webLogId = findFullById pageId webLogId
+ member _.FindFullByWebLog webLogId = findFullByWebLog webLogId
+ member _.FindListed webLogId = findListed webLogId
+ member _.FindPageOfPages webLogId pageNbr = findPageOfPages webLogId pageNbr
+ member _.Restore pages = restore pages
+ member _.Update page = save page
+ member _.UpdatePriorPermalinks pageId webLogId permalinks = updatePriorPermalinks pageId webLogId permalinks
diff --git a/src/MyWebLog.Data/PostgreSql/PostgreSqlPostData.fs b/src/MyWebLog.Data/PostgreSql/PostgreSqlPostData.fs
new file mode 100644
index 0000000..4679ab9
--- /dev/null
+++ b/src/MyWebLog.Data/PostgreSql/PostgreSqlPostData.fs
@@ -0,0 +1,352 @@
+namespace MyWebLog.Data.PostgreSql
+
+open System
+open MyWebLog
+open MyWebLog.Data
+open Newtonsoft.Json
+open Npgsql
+open Npgsql.FSharp
+
+/// PostgreSQL myWebLog post data implementation
+type PostgreSqlPostData (conn : NpgsqlConnection) =
+
+ // SUPPORT FUNCTIONS
+
+ /// Append revisions to a post
+ let appendPostRevisions (post : Post) = backgroundTask {
+ let! revisions =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT as_of, revision_text FROM post_revision WHERE post_id = @id ORDER BY as_of DESC"
+ |> Sql.parameters [ "@id", Sql.string (PostId.toString post.Id) ]
+ |> Sql.executeAsync Map.toRevision
+ return { post with Revisions = revisions }
+ }
+
+ /// The SELECT statement for a post that will include category IDs
+ let selectPost =
+ """SELECT *, ARRAY(SELECT cat.category_id FROM post_category cat WHERE cat.post_id = p.id) AS category_ids
+ FROM post"""
+
+ /// Return a post with no revisions, prior permalinks, or text
+ let postWithoutText row =
+ { Map.toPost row with Text = "" }
+
+ /// Update a post's assigned categories
+ let updatePostCategories postId oldCats newCats = backgroundTask {
+ let toDelete, toAdd = Utils.diffLists oldCats newCats CategoryId.toString
+ if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then
+ let catParams cats =
+ cats
+ |> List.map (fun it -> [
+ "@postId", Sql.string (PostId.toString postId)
+ "categoryId", Sql.string (CategoryId.toString it)
+ ])
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.executeTransactionAsync [
+ if not (List.isEmpty toDelete) then
+ "DELETE FROM post_category WHERE post_id = @postId AND category_id = @categoryId",
+ catParams toDelete
+ if not (List.isEmpty toAdd) then
+ "INSERT INTO post_category VALUES (@postId, @categoryId)", catParams toAdd
+ ]
+ ()
+ }
+
+ /// Update a post's revisions
+ let updatePostRevisions postId oldRevs newRevs = backgroundTask {
+ let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs
+ if not (List.isEmpty toDelete) || not (List.isEmpty toAdd) then
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.executeTransactionAsync [
+ if not (List.isEmpty toDelete) then
+ "DELETE FROM post_revision WHERE post_id = @postId AND as_of = @asOf",
+ toDelete
+ |> List.map (fun it -> [
+ "@postId", Sql.string (PostId.toString postId)
+ "@asOf", Sql.timestamptz it.AsOf
+ ])
+ if not (List.isEmpty toAdd) then
+ "INSERT INTO post_revision VALUES (@postId, @asOf, @text)",
+ toAdd
+ |> List.map (fun it -> [
+ "@postId", Sql.string (PostId.toString postId)
+ "@asOf", Sql.timestamptz it.AsOf
+ "@text", Sql.string (MarkupText.toString it.Text)
+ ])
+ ]
+ ()
+ }
+
+ // IMPLEMENTATION FUNCTIONS
+
+ /// Count posts in a status for the given web log
+ let countByStatus status webLogId =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT COUNT(id) AS the_count FROM post WHERE web_log_id = @webLogId AND status = @status"
+ |> Sql.parameters [ webLogIdParam webLogId; "@status", Sql.string (PostStatus.toString status) ]
+ |> Sql.executeRowAsync Map.toCount
+
+ /// Find a post by its ID for the given web log (excluding revisions)
+ let findById postId webLogId = backgroundTask {
+ let! post =
+ Sql.existingConnection conn
+ |> Sql.query $"{selectPost} WHERE id = @id AND web_log_id = @webLogId"
+ |> Sql.parameters [ "@id", Sql.string (PostId.toString postId); webLogIdParam webLogId ]
+ |> Sql.executeAsync Map.toPost
+ return List.tryHead post
+ }
+
+ /// Find a post by its permalink for the given web log (excluding revisions and prior permalinks)
+ let findByPermalink permalink webLogId = backgroundTask {
+ let! post =
+ Sql.existingConnection conn
+ |> Sql.query $"{selectPost} WHERE web_log_id = @webLogId AND permalink = @link"
+ |> Sql.parameters [ webLogIdParam webLogId; "@link", Sql.string (Permalink.toString permalink) ]
+ |> Sql.executeAsync Map.toPost
+ return List.tryHead post
+ }
+
+ /// Find a complete post by its ID for the given web log
+ let findFullById postId webLogId = backgroundTask {
+ match! findById postId webLogId with
+ | Some post ->
+ let! withRevisions = appendPostRevisions post
+ return Some withRevisions
+ | None -> return None
+ }
+
+ /// Delete a post by its ID for the given web log
+ let delete postId webLogId = backgroundTask {
+ match! findById postId webLogId with
+ | Some _ ->
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query """
+ DELETE FROM post_revision WHERE post_id = @id;
+ DELETE FROM post_category WHERE post_id = @id;
+ DELETE FROM post WHERE id = @id"""
+ |> Sql.parameters [ "@id", Sql.string (PostId.toString postId) ]
+ |> Sql.executeNonQueryAsync
+ return true
+ | None -> return false
+ }
+
+ /// Find the current permalink from a list of potential prior permalinks for the given web log
+ let findCurrentPermalink permalinks webLogId = backgroundTask {
+ if List.isEmpty permalinks then return None
+ else
+ let linkSql, linkParams = priorPermalinkSql permalinks
+ let! links =
+ Sql.existingConnection conn
+ |> Sql.query $"SELECT permalink FROM post WHERE web_log_id = @webLogId AND ({linkSql}"
+ |> Sql.parameters (webLogIdParam webLogId :: linkParams)
+ |> Sql.executeAsync Map.toPermalink
+ return List.tryHead links
+ }
+
+ /// Get all complete posts for the given web log
+ let findFullByWebLog webLogId = backgroundTask {
+ let! posts =
+ Sql.existingConnection conn
+ |> Sql.query $"{selectPost} WHERE web_log_id = @webLogId"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync Map.toPost
+ let! revisions =
+ Sql.existingConnection conn
+ |> Sql.query """
+ SELECT *
+ FROM post_revision pr
+ INNER JOIN post p ON p.id = pr.post_id
+ WHERE p.web_log_id = @webLogId
+ ORDER BY as_of DESC"""
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync (fun row -> PostId (row.string "post_id"), Map.toRevision row)
+ return
+ posts
+ |> List.map (fun it ->
+ { it with Revisions = revisions |> List.filter (fun r -> fst r = it.Id) |> List.map snd })
+ }
+
+ /// Get a page of categorized posts for the given web log (excludes revisions)
+ let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage =
+ let catSql, catParams = inClause "catId" CategoryId.toString categoryIds
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ {selectPost} p
+ INNER JOIN post_category pc ON pc.post_id = p.id
+ WHERE p.web_log_id = @webLogId
+ AND p.status = @status
+ AND pc.category_id IN ({catSql})
+ ORDER BY published_on DESC
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"""
+ |> Sql.parameters
+ [ webLogIdParam webLogId
+ "@status", Sql.string (PostStatus.toString Published)
+ yield! catParams ]
+ |> Sql.executeAsync Map.toPost
+
+ /// Get a page of posts for the given web log (excludes text and revisions)
+ let findPageOfPosts webLogId pageNbr postsPerPage =
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ {selectPost}
+ WHERE web_log_id = @webLogId
+ ORDER BY published_on DESC NULLS FIRST, updated_on
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"""
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync postWithoutText
+
+ /// Get a page of published posts for the given web log (excludes revisions)
+ let findPageOfPublishedPosts webLogId pageNbr postsPerPage =
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ {selectPost}
+ WHERE web_log_id = @webLogId
+ AND status = @status
+ ORDER BY published_on DESC
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"""
+ |> Sql.parameters [ webLogIdParam webLogId; "@status", Sql.string (PostStatus.toString Published) ]
+ |> Sql.executeAsync Map.toPost
+
+ /// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks)
+ let findPageOfTaggedPosts webLogId (tag : string) pageNbr postsPerPage =
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ {selectPost}
+ WHERE web_log_id = @webLogId
+ AND status = @status
+ AND tag && ARRAY[@tag]
+ ORDER BY published_on DESC
+ LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}"""
+ |> Sql.parameters
+ [ webLogIdParam webLogId
+ "@status", Sql.string (PostStatus.toString Published)
+ "@tag", Sql.string tag
+ ]
+ |> Sql.executeAsync Map.toPost
+
+ /// Find the next newest and oldest post from a publish date for the given web log
+ let findSurroundingPosts webLogId (publishedOn : DateTime) = backgroundTask {
+ let queryParams = Sql.parameters [
+ webLogIdParam webLogId
+ "@status", Sql.string (PostStatus.toString Published)
+ "@publishedOn", Sql.timestamptz publishedOn
+ ]
+ let! older =
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ {selectPost}
+ WHERE web_log_id = @webLogId
+ AND status = @status
+ AND published_on < @publishedOn
+ ORDER BY published_on DESC
+ LIMIT 1"""
+ |> queryParams
+ |> Sql.executeAsync Map.toPost
+ let! newer =
+ Sql.existingConnection conn
+ |> Sql.query $"""
+ {selectPost}
+ WHERE web_log_id = @webLogId
+ AND status = @status
+ AND published_on > @publishedOn
+ ORDER BY published_on
+ LIMIT 1"""
+ |> queryParams
+ |> Sql.executeAsync Map.toPost
+ return List.tryHead older, List.tryHead newer
+ }
+
+ /// Save a post
+ let save (post : Post) = backgroundTask {
+ let! oldPost = findFullById post.Id post.WebLogId
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query """
+ INSERT INTO post (
+ id, web_log_id, author_id, status, title, permalink, prior_permalinks, published_on, updated_on,
+ template, post_text, tags, meta_items, episode
+ ) VALUES (
+ @id, @webLogId, @authorId, @status, @title, @permalink, @priorPermalinks, @publishedOn, @updatedOn,
+ @template, @text, @tags, @metaItems, @episode
+ ) ON CONFLICT (id) DO UPDATE
+ SET author_id = EXCLUDED.author_id,
+ status = EXCLUDED.status,
+ title = EXCLUDED.title,
+ permalink = EXCLUDED.permalink,
+ prior_permalinks = EXCLUDED.prior_permalinks,
+ published_on = EXCLUDED.published_on,
+ updated_on = EXCLUDED.updated_on,
+ template = EXCLUDED.template,
+ post_text = EXCLUDED.text,
+ tags = EXCLUDED.tags,
+ meta_items = EXCLUDED.meta_items,
+ episode = EXCLUDED.episode"""
+ |> Sql.parameters
+ [ webLogIdParam post.WebLogId
+ "@id", Sql.string (PostId.toString post.Id)
+ "@authorId", Sql.string (WebLogUserId.toString post.AuthorId)
+ "@status", Sql.string (PostStatus.toString post.Status)
+ "@title", Sql.string post.Title
+ "@permalink", Sql.string (Permalink.toString post.Permalink)
+ "@publishedOn", Sql.timestamptzOrNone post.PublishedOn
+ "@updatedOn", Sql.timestamptz post.UpdatedOn
+ "@template", Sql.stringOrNone post.Template
+ "@text", Sql.string post.Text
+ "@episode", Sql.jsonbOrNone (post.Episode |> Option.map JsonConvert.SerializeObject)
+ "@priorPermalinks",
+ Sql.stringArray (post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList)
+ "@tags",
+ Sql.stringArrayOrNone (if List.isEmpty post.Tags then None else Some (Array.ofList post.Tags))
+ "@metaItems",
+ if List.isEmpty post.Metadata then None else Some (JsonConvert.SerializeObject post.Metadata)
+ |> Sql.jsonbOrNone
+ ]
+ |> Sql.executeNonQueryAsync
+ do! updatePostCategories post.Id (match oldPost with Some p -> p.CategoryIds | None -> []) post.CategoryIds
+ do! updatePostRevisions post.Id (match oldPost with Some p -> p.Revisions | None -> []) post.Revisions
+ }
+
+ /// Restore posts from a backup
+ let restore posts = backgroundTask {
+ for post in posts do
+ do! save post
+ }
+
+ /// Update prior permalinks for a post
+ let updatePriorPermalinks postId webLogId permalinks = backgroundTask {
+ match! findById postId webLogId with
+ | Some _ ->
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query "UPDATE post SET prior_permalinks = @prior WHERE id = @id"
+ |> Sql.parameters
+ [ "@id", Sql.string (PostId.toString postId)
+ "@prior", Sql.stringArray (permalinks |> List.map Permalink.toString |> Array.ofList) ]
+ |> Sql.executeNonQueryAsync
+ 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
diff --git a/src/MyWebLog.Data/PostgreSql/PostgreSqlTagMapData.fs b/src/MyWebLog.Data/PostgreSql/PostgreSqlTagMapData.fs
new file mode 100644
index 0000000..52e9cb6
--- /dev/null
+++ b/src/MyWebLog.Data/PostgreSql/PostgreSqlTagMapData.fs
@@ -0,0 +1,94 @@
+namespace MyWebLog.Data.PostgreSql
+
+open MyWebLog
+open MyWebLog.Data
+open Npgsql
+open Npgsql.FSharp
+
+/// PostgreSQL myWebLog tag mapping data implementation
+type PostgreSqlTagMapData (conn : NpgsqlConnection) =
+
+ /// Find a tag mapping by its ID for the given web log
+ let findById tagMapId webLogId = backgroundTask {
+ let! tagMap =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM tag_map WHERE id = @id AND web_log_id = @webLogId"
+ |> Sql.parameters [ "@id", Sql.string (TagMapId.toString tagMapId); webLogIdParam webLogId ]
+ |> Sql.executeAsync Map.toTagMap
+ return List.tryHead tagMap
+ }
+
+ /// Delete a tag mapping for the given web log
+ let delete tagMapId webLogId = backgroundTask {
+ match! findById tagMapId webLogId with
+ | Some _ ->
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query "DELETE FROM tag_map WHERE id = @id"
+ |> Sql.parameters [ "@id", Sql.string (TagMapId.toString tagMapId) ]
+ |> Sql.executeNonQueryAsync
+ return true
+ | None -> return false
+ }
+
+ /// Find a tag mapping by its URL value for the given web log
+ let findByUrlValue urlValue webLogId = backgroundTask {
+ let! tagMap =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM tag_map WHERE web_log_id = @webLogId AND url_value = @urlValue"
+ |> Sql.parameters [ webLogIdParam webLogId; "@urlValue", Sql.string urlValue ]
+ |> Sql.executeAsync Map.toTagMap
+ return List.tryHead tagMap
+ }
+
+ /// Get all tag mappings for the given web log
+ let findByWebLog webLogId =
+ Sql.existingConnection conn
+ |> Sql.query "SELECT * FROM tag_map WHERE web_log_id = @webLogId ORDER BY tag"
+ |> Sql.parameters [ webLogIdParam webLogId ]
+ |> Sql.executeAsync Map.toTagMap
+
+ /// Find any tag mappings in a list of tags for the given web log
+ let findMappingForTags tags webLogId =
+ let tagSql, tagParams = inClause "tag" id tags
+ Sql.existingConnection conn
+ |> Sql.query $"SELECT * FROM tag_map WHERE web_log_id = @webLogId AND tag IN ({tagSql}"
+ |> Sql.parameters (webLogIdParam webLogId :: tagParams)
+ |> Sql.executeAsync Map.toTagMap
+
+ /// Save a tag mapping
+ let save (tagMap : TagMap) = backgroundTask {
+ let! _ =
+ Sql.existingConnection conn
+ |> Sql.query """
+ INSERT INTO tag_map (
+ id, web_log_id, tag, url_value
+ ) VALUES (
+ @id, @webLogId, @tag, @urlValue
+ ) ON CONFLICT (id) DO UPDATE
+ SET tag = EXCLUDED.tag,
+ url_value = EXCLUDED.url_value"""
+ |> Sql.parameters
+ [ webLogIdParam tagMap.WebLogId
+ "@id", Sql.string (TagMapId.toString tagMap.Id)
+ "@tag", Sql.string tagMap.Tag
+ "@urlValue", Sql.string tagMap.UrlValue
+ ]
+ |> Sql.executeNonQueryAsync
+ ()
+ }
+
+ /// Restore tag mappings from a backup
+ let restore tagMaps = backgroundTask {
+ for tagMap in tagMaps do
+ do! save tagMap
+ }
+
+ interface ITagMapData with
+ member _.Delete tagMapId webLogId = delete tagMapId webLogId
+ member _.FindById tagMapId webLogId = findById tagMapId webLogId
+ member _.FindByUrlValue urlValue webLogId = findByUrlValue urlValue webLogId
+ member _.FindByWebLog webLogId = findByWebLog webLogId
+ member _.FindMappingForTags tags webLogId = findMappingForTags tags webLogId
+ member _.Save tagMap = save tagMap
+ member _.Restore tagMaps = restore tagMaps
diff --git a/src/MyWebLog.Data/PostgreSqlData.fs b/src/MyWebLog.Data/PostgreSqlData.fs
index 230966d..a121728 100644
--- a/src/MyWebLog.Data/PostgreSqlData.fs
+++ b/src/MyWebLog.Data/PostgreSqlData.fs
@@ -12,6 +12,8 @@ type PostgreSqlData (conn : NpgsqlConnection, log : ILogger) =
interface IData with
member _.Category = PostgreSqlCategoryData conn
+ member _.Page = PostgreSqlPageData conn
+ member _.Post = PostgreSqlPostData conn
member _.StartUp () = backgroundTask {
@@ -127,25 +129,16 @@ type PostgreSqlData (conn : NpgsqlConnection, log : ILogger) =
author_id TEXT NOT NULL REFERENCES web_log_user (id),
title TEXT NOT NULL,
permalink TEXT NOT NULL,
+ prior_permalinks TEXT[] NOT NULL DEFAULT '{}',
published_on TIMESTAMPTZ NOT NULL,
updated_on TIMESTAMPTZ NOT NULL,
is_in_page_list BOOLEAN NOT NULL DEFAULT FALSE,
template TEXT,
- page_text TEXT NOT NULL);
+ page_text TEXT NOT NULL
+ meta_items JSONB);
CREATE INDEX page_web_log_idx ON page (web_log_id);
CREATE INDEX page_author_idx ON page (author_id);
CREATE INDEX page_permalink_idx ON page (web_log_id, permalink)"""
- if needsTable "page_meta" then
- """CREATE TABLE page_meta (
- page_id TEXT NOT NULL REFERENCES page (id),
- name TEXT NOT NULL,
- value TEXT NOT NULL,
- PRIMARY KEY (page_id, name, value))"""
- if needsTable "page_permalink" then
- """CREATE TABLE page_permalink (
- page_id TEXT NOT NULL REFERENCES page (id),
- permalink TEXT NOT NULL,
- PRIMARY KEY (page_id, permalink))"""
if needsTable "page_revision" then
"""CREATE TABLE page_revision (
page_id TEXT NOT NULL REFERENCES page (id),
@@ -156,16 +149,20 @@ type PostgreSqlData (conn : NpgsqlConnection, log : ILogger) =
// Post tables
if needsTable "post" then
"""CREATE TABLE post (
- id TEXT NOT NULL PRIMARY KEY,
- web_log_id TEXT NOT NULL REFERENCES web_log (id),
- author_id TEXT NOT NULL REFERENCES web_log_user (id),
- status TEXT NOT NULL,
- title TEXT NOT NULL,
- permalink TEXT NOT NULL,
- published_on TIMESTAMPTZ,
- updated_on TIMESTAMPTZ NOT NULL,
- template TEXT,
- post_text TEXT NOT NULL);
+ id TEXT NOT NULL PRIMARY KEY,
+ web_log_id TEXT NOT NULL REFERENCES web_log (id),
+ author_id TEXT NOT NULL REFERENCES web_log_user (id),
+ status TEXT NOT NULL,
+ title TEXT NOT NULL,
+ permalink TEXT NOT NULL,
+ prior_permalinks TEXT[] NOT NULL DEFAULT '{}',
+ published_on TIMESTAMPTZ,
+ updated_on TIMESTAMPTZ NOT NULL,
+ template TEXT,
+ post_text TEXT NOT NULL,
+ tags TEXT[],
+ meta_items JSONB,
+ episode JSONB);
CREATE INDEX post_web_log_idx ON post (web_log_id);
CREATE INDEX post_author_idx ON post (author_id);
CREATE INDEX post_status_idx ON post (web_log_id, status, updated_on);
@@ -176,42 +173,6 @@ type PostgreSqlData (conn : NpgsqlConnection, log : ILogger) =
category_id TEXT NOT NULL REFERENCES category (id),
PRIMARY KEY (post_id, category_id));
CREATE INDEX post_category_category_idx ON post_category (category_id)"""
- if needsTable "post_episode" then
- """CREATE TABLE post_episode (
- post_id TEXT NOT NULL PRIMARY KEY REFERENCES post(id),
- media TEXT NOT NULL,
- length INTEGER NOT NULL,
- duration TEXT,
- media_type TEXT,
- image_url TEXT,
- subtitle TEXT,
- explicit TEXT,
- chapter_file TEXT,
- chapter_type TEXT,
- transcript_url TEXT,
- transcript_type TEXT,
- transcript_lang TEXT,
- transcript_captions INTEGER,
- season_number INTEGER,
- season_description TEXT,
- episode_number TEXT,
- episode_description TEXT)"""
- if needsTable "post_tag" then
- """CREATE TABLE post_tag (
- post_id TEXT NOT NULL REFERENCES post (id),
- tag TEXT NOT NULL,
- PRIMARY KEY (post_id, tag))"""
- if needsTable "post_meta" then
- """CREATE TABLE post_meta (
- post_id TEXT NOT NULL REFERENCES post (id),
- name TEXT NOT NULL,
- value TEXT NOT NULL,
- PRIMARY KEY (post_id, name, value))"""
- if needsTable "post_permalink" then
- """CREATE TABLE post_permalink (
- post_id TEXT NOT NULL REFERENCES post (id),
- permalink TEXT NOT NULL,
- PRIMARY KEY (post_id, permalink))"""
if needsTable "post_revision" then
"""CREATE TABLE post_revision (
post_id TEXT NOT NULL REFERENCES post (id),
diff --git a/src/MyWebLog.Data/SQLite/Helpers.fs b/src/MyWebLog.Data/SQLite/Helpers.fs
index 88955d6..f35fa70 100644
--- a/src/MyWebLog.Data/SQLite/Helpers.fs
+++ b/src/MyWebLog.Data/SQLite/Helpers.fs
@@ -12,23 +12,6 @@ let count (cmd : SqliteCommand) = backgroundTask {
return int (it :?> int64)
}
-/// Get lists of items removed from and added to the given lists
-let diffLists<'T, 'U when 'U : equality> oldItems newItems (f : 'T -> 'U) =
- let diff compList = fun item -> not (compList |> List.exists (fun other -> f item = f other))
- List.filter (diff newItems) oldItems, List.filter (diff oldItems) newItems
-
-/// Find meta items added and removed
-let diffMetaItems (oldItems : MetaItem list) newItems =
- diffLists oldItems newItems (fun item -> $"{item.Name}|{item.Value}")
-
-/// Find the permalinks added and removed
-let diffPermalinks oldLinks newLinks =
- diffLists oldLinks newLinks Permalink.toString
-
-/// Find the revisions added and removed
-let diffRevisions oldRevs newRevs =
- diffLists oldRevs newRevs (fun (rev : Revision) -> $"{rev.AsOf.Ticks}|{MarkupText.toString rev.Text}")
-
/// Create a list of items from the given data reader
let toList<'T> (it : SqliteDataReader -> 'T) (rdr : SqliteDataReader) =
seq { while rdr.Read () do it rdr }
diff --git a/src/MyWebLog.Data/SQLite/SQLitePageData.fs b/src/MyWebLog.Data/SQLite/SQLitePageData.fs
index 5dbb71e..7ca61fc 100644
--- a/src/MyWebLog.Data/SQLite/SQLitePageData.fs
+++ b/src/MyWebLog.Data/SQLite/SQLitePageData.fs
@@ -54,7 +54,7 @@ type SQLitePageData (conn : SqliteConnection) =
/// Update a page's metadata items
let updatePageMeta pageId oldItems newItems = backgroundTask {
- let toDelete, toAdd = diffMetaItems oldItems newItems
+ let toDelete, toAdd = Utils.diffMetaItems oldItems newItems
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
@@ -82,7 +82,7 @@ type SQLitePageData (conn : SqliteConnection) =
/// Update a page's prior permalinks
let updatePagePermalinks pageId oldLinks newLinks = backgroundTask {
- let toDelete, toAdd = diffPermalinks oldLinks newLinks
+ let toDelete, toAdd = Utils.diffPermalinks oldLinks newLinks
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
@@ -108,7 +108,7 @@ type SQLitePageData (conn : SqliteConnection) =
/// Update a page's revisions
let updatePageRevisions pageId oldRevs newRevs = backgroundTask {
- let toDelete, toAdd = diffRevisions oldRevs newRevs
+ let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
diff --git a/src/MyWebLog.Data/SQLite/SQLitePostData.fs b/src/MyWebLog.Data/SQLite/SQLitePostData.fs
index fdfa1e9..5de370b 100644
--- a/src/MyWebLog.Data/SQLite/SQLitePostData.fs
+++ b/src/MyWebLog.Data/SQLite/SQLitePostData.fs
@@ -99,7 +99,7 @@ type SQLitePostData (conn : SqliteConnection) =
/// Update a post's assigned categories
let updatePostCategories postId oldCats newCats = backgroundTask {
- let toDelete, toAdd = diffLists oldCats newCats CategoryId.toString
+ let toDelete, toAdd = Utils.diffLists oldCats newCats CategoryId.toString
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
@@ -125,7 +125,7 @@ type SQLitePostData (conn : SqliteConnection) =
/// Update a post's assigned categories
let updatePostTags postId (oldTags : string list) newTags = backgroundTask {
- let toDelete, toAdd = diffLists oldTags newTags id
+ let toDelete, toAdd = Utils.diffLists oldTags newTags id
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
@@ -203,7 +203,7 @@ type SQLitePostData (conn : SqliteConnection) =
/// Update a post's metadata items
let updatePostMeta postId oldItems newItems = backgroundTask {
- let toDelete, toAdd = diffMetaItems oldItems newItems
+ let toDelete, toAdd = Utils.diffMetaItems oldItems newItems
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
@@ -231,7 +231,7 @@ type SQLitePostData (conn : SqliteConnection) =
/// Update a post's prior permalinks
let updatePostPermalinks postId oldLinks newLinks = backgroundTask {
- let toDelete, toAdd = diffPermalinks oldLinks newLinks
+ let toDelete, toAdd = Utils.diffPermalinks oldLinks newLinks
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
@@ -257,7 +257,7 @@ type SQLitePostData (conn : SqliteConnection) =
/// Update a post's revisions
let updatePostRevisions postId oldRevs newRevs = backgroundTask {
- let toDelete, toAdd = diffRevisions oldRevs newRevs
+ let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs
if List.isEmpty toDelete && List.isEmpty toAdd then
return ()
else
diff --git a/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs b/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs
index 53c4204..7a0182d 100644
--- a/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs
+++ b/src/MyWebLog.Data/SQLite/SQLiteThemeData.fs
@@ -92,8 +92,8 @@ type SQLiteThemeData (conn : SqliteConnection) =
do! write cmd
let toDelete, toAdd =
- diffLists (oldTheme |> Option.map (fun t -> t.Templates) |> Option.defaultValue [])
- theme.Templates (fun t -> t.Name)
+ Utils.diffLists (oldTheme |> Option.map (fun t -> t.Templates) |> Option.defaultValue [])
+ theme.Templates (fun t -> t.Name)
let toUpdate =
theme.Templates
|> List.filter (fun t ->
diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs
index 7013583..c498c13 100644
--- a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs
+++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs
@@ -107,7 +107,7 @@ type SQLiteWebLogData (conn : SqliteConnection) =
/// Update the custom feeds for a web log
let updateCustomFeeds (webLog : WebLog) = backgroundTask {
let! feeds = getCustomFeeds webLog
- let toDelete, toAdd = diffLists feeds webLog.Rss.CustomFeeds (fun it -> $"{CustomFeedId.toString it.Id}")
+ let toDelete, toAdd = Utils.diffLists feeds webLog.Rss.CustomFeeds (fun it -> $"{CustomFeedId.toString it.Id}")
let toId (feed : CustomFeed) = feed.Id
let toUpdate =
webLog.Rss.CustomFeeds
diff --git a/src/MyWebLog.Data/Utils.fs b/src/MyWebLog.Data/Utils.fs
index f225a49..cc06d9b 100644
--- a/src/MyWebLog.Data/Utils.fs
+++ b/src/MyWebLog.Data/Utils.fs
@@ -20,3 +20,20 @@ let rec orderByHierarchy (cats : Category list) parentId slugBase parentNames =
yield! orderByHierarchy cats (Some cat.Id) (Some fullSlug) ([ cat.Name ] |> List.append parentNames)
}
+/// Get lists of items removed from and added to the given lists
+let diffLists<'T, 'U when 'U : equality> oldItems newItems (f : 'T -> 'U) =
+ let diff compList = fun item -> not (compList |> List.exists (fun other -> f item = f other))
+ List.filter (diff newItems) oldItems, List.filter (diff oldItems) newItems
+
+/// Find meta items added and removed
+let diffMetaItems (oldItems : MetaItem list) newItems =
+ diffLists oldItems newItems (fun item -> $"{item.Name}|{item.Value}")
+
+/// Find the permalinks added and removed
+let diffPermalinks oldLinks newLinks =
+ diffLists oldLinks newLinks Permalink.toString
+
+/// Find the revisions added and removed
+let diffRevisions oldRevs newRevs =
+ diffLists oldRevs newRevs (fun (rev : Revision) -> $"{rev.AsOf.Ticks}|{MarkupText.toString rev.Text}")
+