From 5fe2077974a9c2a7368e074e8edcf871c5c33c8a Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Fri, 15 Dec 2023 22:46:12 -0500 Subject: [PATCH] WIP on module/member conversion --- src/MyWebLog.Data/Converters.fs | 54 +- .../Postgres/PostgresCategoryData.fs | 13 +- src/MyWebLog.Data/Postgres/PostgresHelpers.fs | 4 +- .../Postgres/PostgresPageData.fs | 38 +- .../Postgres/PostgresPostData.fs | 52 +- .../Postgres/PostgresUploadData.fs | 2 +- src/MyWebLog.Data/RethinkDbData.fs | 2 +- src/MyWebLog.Data/SQLite/Helpers.fs | 4 +- src/MyWebLog.Data/SQLite/SQLitePageData.fs | 40 +- src/MyWebLog.Data/SQLite/SQLitePostData.fs | 72 +-- src/MyWebLog.Data/SQLite/SQLiteUploadData.fs | 4 +- src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs | 2 +- src/MyWebLog.Data/SQLiteData.fs | 4 +- src/MyWebLog.Data/Utils.fs | 4 +- src/MyWebLog.Domain/DataTypes.fs | 22 +- src/MyWebLog.Domain/SupportTypes.fs | 192 ++++--- src/MyWebLog.Domain/ViewModels.fs | 494 +++++++++--------- src/MyWebLog/DotLiquidBespoke.fs | 18 +- src/MyWebLog/Handlers/Admin.fs | 12 +- src/MyWebLog/Handlers/Feed.fs | 30 +- src/MyWebLog/Handlers/Page.fs | 8 +- src/MyWebLog/Handlers/Post.fs | 18 +- src/MyWebLog/Handlers/Routes.fs | 10 +- src/MyWebLog/Maintenance.fs | 14 +- 24 files changed, 548 insertions(+), 565 deletions(-) diff --git a/src/MyWebLog.Data/Converters.fs b/src/MyWebLog.Data/Converters.fs index 0c3be03..d68025d 100644 --- a/src/MyWebLog.Data/Converters.fs +++ b/src/MyWebLog.Data/Converters.fs @@ -51,39 +51,39 @@ module Json = override _.ReadJson(reader: JsonReader, _: Type, _: ExplicitRating, _: bool, _: JsonSerializer) = (string >> ExplicitRating.Parse) reader.Value - type MarkupTextConverter () = - inherit JsonConverter () - override _.WriteJson (writer : JsonWriter, value : MarkupText, _ : JsonSerializer) = - writer.WriteValue (MarkupText.toString value) - override _.ReadJson (reader : JsonReader, _ : Type, _ : MarkupText, _ : bool, _ : JsonSerializer) = - (string >> MarkupText.parse) reader.Value + type MarkupTextConverter() = + inherit JsonConverter() + override _.WriteJson(writer: JsonWriter, value: MarkupText, _: JsonSerializer) = + writer.WriteValue value.Value + override _.ReadJson(reader: JsonReader, _: Type, _: MarkupText, _: bool, _: JsonSerializer) = + (string >> MarkupText.Parse) reader.Value - type PermalinkConverter () = - inherit JsonConverter () - override _.WriteJson (writer : JsonWriter, value : Permalink, _ : JsonSerializer) = - writer.WriteValue (Permalink.toString value) - override _.ReadJson (reader : JsonReader, _ : Type, _ : Permalink, _ : bool, _ : JsonSerializer) = + type PermalinkConverter() = + inherit JsonConverter() + override _.WriteJson(writer: JsonWriter, value: Permalink, _: JsonSerializer) = + writer.WriteValue value.Value + override _.ReadJson(reader: JsonReader, _: Type, _: Permalink, _: bool, _: JsonSerializer) = (string >> Permalink) reader.Value - type PageIdConverter () = - inherit JsonConverter () - override _.WriteJson (writer : JsonWriter, value : PageId, _ : JsonSerializer) = - writer.WriteValue (PageId.toString value) - override _.ReadJson (reader : JsonReader, _ : Type, _ : PageId, _ : bool, _ : JsonSerializer) = + type PageIdConverter() = + inherit JsonConverter() + override _.WriteJson(writer: JsonWriter, value: PageId, _: JsonSerializer) = + writer.WriteValue value.Value + override _.ReadJson(reader: JsonReader, _: Type, _: PageId, _: bool, _: JsonSerializer) = (string >> PageId) reader.Value - type PodcastMediumConverter () = - inherit JsonConverter () - override _.WriteJson (writer : JsonWriter, value : PodcastMedium, _ : JsonSerializer) = - writer.WriteValue (PodcastMedium.toString value) - override _.ReadJson (reader : JsonReader, _ : Type, _ : PodcastMedium, _ : bool, _ : JsonSerializer) = - (string >> PodcastMedium.parse) reader.Value + type PodcastMediumConverter() = + inherit JsonConverter() + override _.WriteJson(writer: JsonWriter, value: PodcastMedium, _: JsonSerializer) = + writer.WriteValue value.Value + override _.ReadJson(reader: JsonReader, _: Type, _: PodcastMedium, _: bool, _: JsonSerializer) = + (string >> PodcastMedium.Parse) reader.Value - type PostIdConverter () = - inherit JsonConverter () - override _.WriteJson (writer : JsonWriter, value : PostId, _ : JsonSerializer) = - writer.WriteValue (PostId.toString value) - override _.ReadJson (reader : JsonReader, _ : Type, _ : PostId, _ : bool, _ : JsonSerializer) = + type PostIdConverter() = + inherit JsonConverter() + override _.WriteJson(writer: JsonWriter, value: PostId, _: JsonSerializer) = + writer.WriteValue value.Value + override _.ReadJson(reader: JsonReader, _: Type, _: PostId, _: bool, _: JsonSerializer) = (string >> PostId) reader.Value type TagMapIdConverter () = diff --git a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs index 60ef682..08d041c 100644 --- a/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresCategoryData.fs @@ -7,7 +7,7 @@ open MyWebLog.Data open Npgsql.FSharp /// PostgreSQL myWebLog category data implementation -type PostgresCategoryData (log : ILogger) = +type PostgresCategoryData(log: ILogger) = /// Count all categories for the given web log let countAll webLogId = @@ -33,7 +33,7 @@ type PostgresCategoryData (log : ILogger) = let catIdSql, catIdParams = ordered |> Seq.filter (fun cat -> cat.ParentNames |> Array.contains it.Name) - |> Seq.map (fun cat -> cat.Id) + |> Seq.map _.Id |> Seq.append (Seq.singleton it.Id) |> List.ofSeq |> arrayContains (nameof Post.empty.CategoryIds) id @@ -43,10 +43,9 @@ type PostgresCategoryData (log : ILogger) = FROM {Table.Post} WHERE {Query.whereDataContains "@criteria"} AND {catIdSql}""" - [ "@criteria", - Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} - catIdParams - ] Map.toCount + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = Published.Value |} + catIdParams ] + Map.toCount |> Async.AwaitTask |> Async.RunSynchronously it.Id, postCount) @@ -107,7 +106,7 @@ type PostgresCategoryData (log : ILogger) = |> Sql.executeTransactionAsync [ Query.Update.partialById Table.Post, posts |> List.map (fun post -> [ - "@id", Sql.string (PostId.toString post.Id) + "@id", Sql.string post.Id.Value "@data", Query.jsonbDocParam {| CategoryIds = post.CategoryIds |> List.filter (fun cat -> cat <> catId) |} ]) diff --git a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs index 765e669..b51b1e6 100644 --- a/src/MyWebLog.Data/Postgres/PostgresHelpers.fs +++ b/src/MyWebLog.Data/Postgres/PostgresHelpers.fs @@ -144,7 +144,7 @@ module Map = /// Create a revision from the current row let toRevision (row : RowReader) : Revision = { AsOf = row.fieldValue "as_of" - Text = row.string "revision_text" |> MarkupText.parse + Text = row.string "revision_text" |> MarkupText.Parse } /// Create a theme asset from the current row @@ -206,7 +206,7 @@ module Revisions = let revParams<'TKey> (key : 'TKey) (keyFunc : 'TKey -> string) rev = [ typedParam "asOf" rev.AsOf "@id", Sql.string (keyFunc key) - "@text", Sql.string (MarkupText.toString rev.Text) + "@text", Sql.string rev.Text.Value ] /// The SQL statement to insert a revision diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 6feb078..8be3d1b 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -7,30 +7,30 @@ open MyWebLog.Data open Npgsql.FSharp /// PostgreSQL myWebLog page data implementation -type PostgresPageData (log : ILogger) = +type PostgresPageData (log: ILogger) = // SUPPORT FUNCTIONS /// Append revisions to a page - let appendPageRevisions (page : Page) = backgroundTask { + let appendPageRevisions (page: Page) = backgroundTask { log.LogTrace "Page.appendPageRevisions" - let! revisions = Revisions.findByEntityId Table.PageRevision Table.Page page.Id PageId.toString + let! revisions = Revisions.findByEntityId Table.PageRevision Table.Page page.Id _.Value return { page with Revisions = revisions } } /// Return a page with no text or revisions - let pageWithoutText (row : RowReader) = + let pageWithoutText (row: RowReader) = { fromData row with Text = "" } /// Update a page's revisions - let updatePageRevisions pageId oldRevs newRevs = + let updatePageRevisions (pageId: PageId) oldRevs newRevs = log.LogTrace "Page.updatePageRevisions" - Revisions.update Table.PageRevision Table.Page pageId PageId.toString oldRevs newRevs + Revisions.update Table.PageRevision Table.Page pageId (_.Value) oldRevs newRevs /// Does the given page exist? - let pageExists pageId webLogId = + let pageExists (pageId: PageId) webLogId = log.LogTrace "Page.pageExists" - Document.existsByWebLog Table.Page pageId PageId.toString webLogId + Document.existsByWebLog Table.Page pageId (_.Value) webLogId // IMPLEMENTATION FUNCTIONS @@ -51,9 +51,9 @@ type PostgresPageData (log : ILogger) = Count.byContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} /// Find a page by its ID (without revisions) - let findById pageId webLogId = + let findById (pageId: PageId) webLogId = log.LogTrace "Page.findById" - Document.findByIdAndWebLog Table.Page pageId PageId.toString webLogId + Document.findByIdAndWebLog Table.Page pageId (_.Value) webLogId /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { @@ -70,15 +70,15 @@ type PostgresPageData (log : ILogger) = log.LogTrace "Page.delete" match! pageExists pageId webLogId with | true -> - do! Delete.byId Table.Page (PageId.toString pageId) + do! Delete.byId Table.Page pageId.Value return true | false -> return false } /// Find a page by its permalink for the given web log - let findByPermalink permalink webLogId = + let findByPermalink (permalink: Permalink) webLogId = log.LogTrace "Page.findByPermalink" - Find.byContains Table.Page {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} + Find.byContains Table.Page {| webLogDoc webLogId with Permalink = permalink.Value |} |> tryHead /// Find the current permalink within a set of potential prior permalinks for the given web log @@ -87,7 +87,7 @@ type PostgresPageData (log : ILogger) = if List.isEmpty permalinks then return None else let linkSql, linkParam = - arrayContains (nameof Page.empty.PriorPermalinks) Permalink.toString permalinks + arrayContains (nameof Page.empty.PriorPermalinks) (fun (it: Permalink) -> it.Value) permalinks return! Custom.single $"""SELECT data ->> '{nameof Page.empty.Permalink}' AS permalink @@ -125,7 +125,7 @@ type PostgresPageData (log : ILogger) = fromData /// Restore pages from a backup - let restore (pages : Page list) = backgroundTask { + let restore (pages: Page list) = backgroundTask { log.LogTrace "Page.restore" let revisions = pages |> List.collect (fun p -> p.Revisions |> List.map (fun r -> p.Id, r)) let! _ = @@ -134,15 +134,15 @@ type PostgresPageData (log : ILogger) = |> Sql.executeTransactionAsync [ Query.insert Table.Page, pages - |> List.map (fun page -> Query.docParameters (PageId.toString page.Id) { page with Revisions = [] }) + |> List.map (fun page -> Query.docParameters page.Id.Value { page with Revisions = [] }) Revisions.insertSql Table.PageRevision, - revisions |> List.map (fun (pageId, rev) -> Revisions.revParams pageId PageId.toString rev) + revisions |> List.map (fun (pageId, rev) -> Revisions.revParams pageId (_.Value) rev) ] () } /// Save a page - let save (page : Page) = backgroundTask { + let save (page: Page) = backgroundTask { log.LogTrace "Page.save" let! oldPage = findFullById page.Id page.WebLogId do! save Table.Page { page with Revisions = [] } @@ -155,7 +155,7 @@ type PostgresPageData (log : ILogger) = log.LogTrace "Page.updatePriorPermalinks" match! pageExists pageId webLogId with | true -> - do! Update.partialById Table.Page (PageId.toString pageId) {| PriorPermalinks = permalinks |} + do! Update.partialById Table.Page pageId.Value {| PriorPermalinks = permalinks |} return true | false -> return false } diff --git a/src/MyWebLog.Data/Postgres/PostgresPostData.fs b/src/MyWebLog.Data/Postgres/PostgresPostData.fs index 70a6c54..c1f6248 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPostData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPostData.fs @@ -7,15 +7,15 @@ open MyWebLog.Data open NodaTime.Text open Npgsql.FSharp -/// PostgreSQL myWebLog post data implementation -type PostgresPostData (log : ILogger) = +/// PostgreSQL myWebLog post data implementation +type PostgresPostData(log: ILogger) = // SUPPORT FUNCTIONS /// Append revisions to a post - let appendPostRevisions (post : Post) = backgroundTask { + let appendPostRevisions (post: Post) = backgroundTask { log.LogTrace "Post.appendPostRevisions" - let! revisions = Revisions.findByEntityId Table.PostRevision Table.Post post.Id PostId.toString + let! revisions = Revisions.findByEntityId Table.PostRevision Table.Post post.Id _.Value return { post with Revisions = revisions } } @@ -24,34 +24,33 @@ type PostgresPostData (log : ILogger) = { fromData row with Text = "" } /// Update a post's revisions - let updatePostRevisions postId oldRevs newRevs = + let updatePostRevisions (postId: PostId) oldRevs newRevs = log.LogTrace "Post.updatePostRevisions" - Revisions.update Table.PostRevision Table.Post postId PostId.toString oldRevs newRevs + Revisions.update Table.PostRevision Table.Post postId (_.Value) oldRevs newRevs /// Does the given post exist? - let postExists postId webLogId = + let postExists (postId: PostId) webLogId = log.LogTrace "Post.postExists" - Document.existsByWebLog Table.Post postId PostId.toString webLogId + Document.existsByWebLog Table.Post postId (_.Value) webLogId // IMPLEMENTATION FUNCTIONS /// Count posts in a status for the given web log - let countByStatus status webLogId = + let countByStatus (status: PostStatus) webLogId = log.LogTrace "Post.countByStatus" - Count.byContains Table.Post {| webLogDoc webLogId with Status = PostStatus.toString status |} + Count.byContains Table.Post {| webLogDoc webLogId with Status = status.Value |} /// 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 PostId.toString webLogId + Document.findByIdAndWebLog Table.Post postId (_.Value) webLogId /// Find a post by its permalink for the given web log (excluding revisions and prior permalinks) - let findByPermalink permalink webLogId = + let findByPermalink (permalink: Permalink) webLogId = log.LogTrace "Post.findByPermalink" Custom.single (selectWithCriteria Table.Post) - [ "@criteria", - Query.jsonbDocParam {| webLogDoc webLogId with Permalink = Permalink.toString permalink |} - ] fromData + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Permalink = permalink.Value |} ] + fromData /// Find a complete post by its ID for the given web log let findFullById postId webLogId = backgroundTask { @@ -68,11 +67,10 @@ type PostgresPostData (log : ILogger) = log.LogTrace "Post.delete" match! postExists postId webLogId with | true -> - let theId = PostId.toString postId do! Custom.nonQuery $"""DELETE FROM {Table.PostComment} WHERE {Query.whereDataContains "@criteria"}; DELETE FROM {Table.Post} WHERE id = @id""" - [ "@id", Sql.string theId; "@criteria", Query.jsonbDocParam {| PostId = theId |} ] + [ "@id", Sql.string postId.Value; "@criteria", Query.jsonbDocParam {| PostId = postId.Value |} ] return true | false -> return false } @@ -83,7 +81,7 @@ type PostgresPostData (log : ILogger) = if List.isEmpty permalinks then return None else let linkSql, linkParam = - arrayContains (nameof Post.empty.PriorPermalinks) Permalink.toString permalinks + arrayContains (nameof Post.empty.PriorPermalinks) (fun (it: Permalink) -> it.Value) permalinks return! Custom.single $"""SELECT data ->> '{nameof Post.empty.Permalink}' AS permalink @@ -106,13 +104,14 @@ type PostgresPostData (log : ILogger) = /// Get a page of categorized posts for the given web log (excludes revisions) let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = log.LogTrace "Post.findPageOfCategorizedPosts" - let catSql, catParam = arrayContains (nameof Post.empty.CategoryIds) (_.Value) categoryIds + let catSql, catParam = + arrayContains (nameof Post.empty.CategoryIds) (fun (it: CategoryId) -> it.Value) categoryIds Custom.list $"{selectWithCriteria Table.Post} AND {catSql} ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = Published.Value |} catParam ] fromData @@ -133,7 +132,7 @@ type PostgresPostData (log : ILogger) = $"{selectWithCriteria Table.Post} ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} ] + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = Published.Value |} ] fromData /// Get a page of tagged posts for the given web log (excludes revisions and prior permalinks) @@ -144,7 +143,7 @@ type PostgresPostData (log : ILogger) = AND data['{nameof Post.empty.Tags}'] @> @tag ORDER BY data ->> '{nameof Post.empty.PublishedOn}' DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" - [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} + [ "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = Published.Value |} "@tag", Query.jsonbDocParam [| tag |] ] fromData @@ -152,7 +151,7 @@ type PostgresPostData (log : ILogger) = let findSurroundingPosts webLogId publishedOn = backgroundTask { log.LogTrace "Post.findSurroundingPosts" let queryParams () = [ - "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = PostStatus.toString Published |} + "@criteria", Query.jsonbDocParam {| webLogDoc webLogId with Status = Published.Value |} "@publishedOn", Sql.string ((InstantPattern.General.Format publishedOn).Substring (0, 19)) ] let pubField = nameof Post.empty.PublishedOn @@ -188,10 +187,9 @@ type PostgresPostData (log : ILogger) = |> Sql.fromDataSource |> Sql.executeTransactionAsync [ Query.insert Table.Post, - posts - |> List.map (fun post -> Query.docParameters (PostId.toString post.Id) { post with Revisions = [] }) + posts |> List.map (fun post -> Query.docParameters post.Id.Value { post with Revisions = [] }) Revisions.insertSql Table.PostRevision, - revisions |> List.map (fun (postId, rev) -> Revisions.revParams postId PostId.toString rev) + revisions |> List.map (fun (postId, rev) -> Revisions.revParams postId (_.Value) rev) ] () } @@ -201,7 +199,7 @@ type PostgresPostData (log : ILogger) = log.LogTrace "Post.updatePriorPermalinks" match! postExists postId webLogId with | true -> - do! Update.partialById Table.Post (PostId.toString postId) {| PriorPermalinks = permalinks |} + do! Update.partialById Table.Post postId.Value {| PriorPermalinks = permalinks |} return true | false -> return false } diff --git a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs index 97e36eb..136da11 100644 --- a/src/MyWebLog.Data/Postgres/PostgresUploadData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresUploadData.fs @@ -22,7 +22,7 @@ type PostgresUploadData (log : ILogger) = webLogIdParam upload.WebLogId typedParam "updatedOn" upload.UpdatedOn "@id", Sql.string (UploadId.toString upload.Id) - "@path", Sql.string (Permalink.toString upload.Path) + "@path", Sql.string upload.Path.Value "@data", Sql.bytea upload.Data ] diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index 73a625f..e653b1f 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -917,7 +917,7 @@ type RethinkDbData (conn : Net.IConnection, config : DataConfig, log : ILogger return Result.Error $"Upload ID {UploadId.toString uploadId} not found" } diff --git a/src/MyWebLog.Data/SQLite/Helpers.fs b/src/MyWebLog.Data/SQLite/Helpers.fs index 08ab5a4..cdd31a2 100644 --- a/src/MyWebLog.Data/SQLite/Helpers.fs +++ b/src/MyWebLog.Data/SQLite/Helpers.fs @@ -254,7 +254,7 @@ module Map = Id = getString "id" rdr |> PostId WebLogId = getString "web_log_id" rdr |> WebLogId AuthorId = getString "author_id" rdr |> WebLogUserId - Status = getString "status" rdr |> PostStatus.parse + Status = getString "status" rdr |> PostStatus.Parse Title = getString "title" rdr Permalink = toPermalink rdr PublishedOn = tryInstant "published_on" rdr @@ -270,7 +270,7 @@ module Map = /// Create a revision from the current row in the given data reader let toRevision rdr : Revision = { AsOf = getInstant "as_of" rdr - Text = getString "revision_text" rdr |> MarkupText.parse + Text = getString "revision_text" rdr |> MarkupText.Parse } /// Create a tag mapping from the current row in the given data reader diff --git a/src/MyWebLog.Data/SQLite/SQLitePageData.fs b/src/MyWebLog.Data/SQLite/SQLitePageData.fs index 5562bcc..c3ae850 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePageData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePageData.fs @@ -6,18 +6,18 @@ open MyWebLog open MyWebLog.Data open Newtonsoft.Json -/// SQLite myWebLog page data implementation -type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = +/// SQLite myWebLog page data implementation +type SQLitePageData(conn: SqliteConnection, ser: JsonSerializer) = // SUPPORT FUNCTIONS /// Add parameters for page INSERT or UPDATE statements - let addPageParameters (cmd : SqliteCommand) (page : Page) = - [ cmd.Parameters.AddWithValue ("@id", PageId.toString page.Id) + let addPageParameters (cmd: SqliteCommand) (page: Page) = + [ cmd.Parameters.AddWithValue ("@id", page.Id.Value) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString page.WebLogId) cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString page.AuthorId) cmd.Parameters.AddWithValue ("@title", page.Title) - cmd.Parameters.AddWithValue ("@permalink", Permalink.toString page.Permalink) + cmd.Parameters.AddWithValue ("@permalink", page.Permalink.Value) cmd.Parameters.AddWithValue ("@publishedOn", instantParam page.PublishedOn) cmd.Parameters.AddWithValue ("@updatedOn", instantParam page.UpdatedOn) cmd.Parameters.AddWithValue ("@isInPageList", page.IsInPageList) @@ -30,7 +30,7 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = /// Append revisions and permalinks to a page let appendPageRevisionsAndPermalinks (page : Page) = backgroundTask { use cmd = conn.CreateCommand () - cmd.Parameters.AddWithValue ("@pageId", PageId.toString page.Id) |> ignore + cmd.Parameters.AddWithValue ("@pageId", page.Id.Value) |> ignore cmd.CommandText <- "SELECT permalink FROM page_permalink WHERE page_id = @pageId" use! rdr = cmd.ExecuteReaderAsync () @@ -51,17 +51,17 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = { toPage rdr with Text = "" } /// Update a page's prior permalinks - let updatePagePermalinks pageId oldLinks newLinks = backgroundTask { + let updatePagePermalinks (pageId: PageId) oldLinks newLinks = backgroundTask { let toDelete, toAdd = Utils.diffPermalinks oldLinks newLinks if List.isEmpty toDelete && List.isEmpty toAdd then return () else use cmd = conn.CreateCommand () - [ cmd.Parameters.AddWithValue ("@pageId", PageId.toString pageId) + [ cmd.Parameters.AddWithValue ("@pageId", pageId.Value) cmd.Parameters.Add ("@link", SqliteType.Text) ] |> ignore - let runCmd link = backgroundTask { - cmd.Parameters["@link"].Value <- Permalink.toString link + let runCmd (link: Permalink) = backgroundTask { + cmd.Parameters["@link"].Value <- link.Value do! write cmd } cmd.CommandText <- "DELETE FROM page_permalink WHERE page_id = @pageId AND permalink = @link" @@ -77,7 +77,7 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = } /// Update a page's revisions - let updatePageRevisions pageId oldRevs newRevs = backgroundTask { + let updatePageRevisions (pageId: PageId) oldRevs newRevs = backgroundTask { let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs if List.isEmpty toDelete && List.isEmpty toAdd then return () @@ -85,10 +85,10 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = use cmd = conn.CreateCommand () let runCmd withText rev = backgroundTask { cmd.Parameters.Clear () - [ cmd.Parameters.AddWithValue ("@pageId", PageId.toString pageId) + [ cmd.Parameters.AddWithValue ("@pageId", pageId.Value) cmd.Parameters.AddWithValue ("@asOf", instantParam rev.AsOf) ] |> ignore - if withText then cmd.Parameters.AddWithValue ("@text", MarkupText.toString rev.Text) |> ignore + if withText then cmd.Parameters.AddWithValue ("@text", rev.Text.Value) |> ignore do! write cmd } cmd.CommandText <- "DELETE FROM page_revision WHERE page_id = @pageId AND as_of = @asOf" @@ -154,12 +154,12 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = } /// Find a page by its ID (without revisions and prior permalinks) - let findById pageId webLogId = backgroundTask { + let findById (pageId: PageId) webLogId = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- "SELECT * FROM page WHERE id = @id" - cmd.Parameters.AddWithValue ("@id", PageId.toString pageId) |> ignore + cmd.Parameters.AddWithValue ("@id", pageId.Value) |> ignore use! rdr = cmd.ExecuteReaderAsync () - return Helpers.verifyWebLog webLogId (fun it -> it.WebLogId) (Map.toPage ser) rdr + return verifyWebLog webLogId (_.WebLogId) (Map.toPage ser) rdr } /// Find a complete page by its ID @@ -175,7 +175,7 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = match! findById pageId webLogId with | Some _ -> use cmd = conn.CreateCommand () - cmd.Parameters.AddWithValue ("@id", PageId.toString pageId) |> ignore + cmd.Parameters.AddWithValue ("@id", pageId.Value) |> ignore cmd.CommandText <- "DELETE FROM page_revision WHERE page_id = @id; DELETE FROM page_permalink WHERE page_id = @id; @@ -186,11 +186,11 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = } /// Find a page by its permalink for the given web log - let findByPermalink permalink webLogId = backgroundTask { + let findByPermalink (permalink: Permalink) webLogId = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- "SELECT * FROM page WHERE web_log_id = @webLogId AND permalink = @link" addWebLogId cmd webLogId - cmd.Parameters.AddWithValue ("@link", Permalink.toString permalink) |> ignore + cmd.Parameters.AddWithValue ("@link", permalink.Value) |> ignore use! rdr = cmd.ExecuteReaderAsync () return if rdr.Read () then Some (toPage rdr) else None } @@ -198,7 +198,7 @@ type SQLitePageData (conn : SqliteConnection, ser : JsonSerializer) = /// Find the current permalink within a set of potential prior permalinks for the given web log let findCurrentPermalink permalinks webLogId = backgroundTask { use cmd = conn.CreateCommand () - let linkSql, linkParams = inClause "AND pp.permalink" "link" Permalink.toString permalinks + let linkSql, linkParams = inClause "AND pp.permalink" "link" (fun (it: Permalink) -> it.Value) permalinks cmd.CommandText <- $" SELECT p.permalink FROM page p diff --git a/src/MyWebLog.Data/SQLite/SQLitePostData.fs b/src/MyWebLog.Data/SQLite/SQLitePostData.fs index d73cf86..c12ecab 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePostData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePostData.fs @@ -7,19 +7,19 @@ open MyWebLog.Data open Newtonsoft.Json open NodaTime -/// SQLite myWebLog post data implementation -type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = +/// SQLite myWebLog post data implementation +type SQLitePostData(conn: SqliteConnection, ser: JsonSerializer) = // SUPPORT FUNCTIONS /// Add parameters for post INSERT or UPDATE statements - let addPostParameters (cmd : SqliteCommand) (post : Post) = - [ cmd.Parameters.AddWithValue ("@id", PostId.toString post.Id) + let addPostParameters (cmd: SqliteCommand) (post: Post) = + [ cmd.Parameters.AddWithValue ("@id", post.Id.Value) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString post.WebLogId) cmd.Parameters.AddWithValue ("@authorId", WebLogUserId.toString post.AuthorId) - cmd.Parameters.AddWithValue ("@status", PostStatus.toString post.Status) + cmd.Parameters.AddWithValue ("@status", post.Status.Value) cmd.Parameters.AddWithValue ("@title", post.Title) - cmd.Parameters.AddWithValue ("@permalink", Permalink.toString post.Permalink) + cmd.Parameters.AddWithValue ("@permalink", post.Permalink.Value) cmd.Parameters.AddWithValue ("@publishedOn", maybeInstant post.PublishedOn) cmd.Parameters.AddWithValue ("@updatedOn", instantParam post.UpdatedOn) cmd.Parameters.AddWithValue ("@template", maybe post.Template) @@ -32,9 +32,9 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = ] |> ignore /// Append category IDs and tags to a post - let appendPostCategoryAndTag (post : Post) = backgroundTask { + let appendPostCategoryAndTag (post: Post) = backgroundTask { use cmd = conn.CreateCommand () - cmd.Parameters.AddWithValue ("@id", PostId.toString post.Id) |> ignore + cmd.Parameters.AddWithValue ("@id", post.Id.Value) |> ignore cmd.CommandText <- "SELECT category_id AS id FROM post_category WHERE post_id = @id" use! rdr = cmd.ExecuteReaderAsync () @@ -47,9 +47,9 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = } /// Append revisions and permalinks to a post - let appendPostRevisionsAndPermalinks (post : Post) = backgroundTask { + let appendPostRevisionsAndPermalinks (post: Post) = backgroundTask { use cmd = conn.CreateCommand () - cmd.Parameters.AddWithValue ("@postId", PostId.toString post.Id) |> ignore + cmd.Parameters.AddWithValue ("@postId", post.Id.Value) |> ignore cmd.CommandText <- "SELECT permalink FROM post_permalink WHERE post_id = @postId" use! rdr = cmd.ExecuteReaderAsync () @@ -69,12 +69,12 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = Map.toPost ser /// Find just-the-post by its ID for the given web log (excludes category, tag, meta, revisions, and permalinks) - let findPostById postId webLogId = backgroundTask { + let findPostById (postId: PostId) webLogId = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- $"{selectPost} WHERE p.id = @id" - cmd.Parameters.AddWithValue ("@id", PostId.toString postId) |> ignore + cmd.Parameters.AddWithValue ("@id", postId.Value) |> ignore use! rdr = cmd.ExecuteReaderAsync () - return Helpers.verifyWebLog webLogId (fun p -> p.WebLogId) toPost rdr + return verifyWebLog webLogId (_.WebLogId) toPost rdr } /// Return a post with no revisions, prior permalinks, or text @@ -82,13 +82,13 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = { toPost rdr with Text = "" } /// Update a post's assigned categories - let updatePostCategories postId oldCats newCats = backgroundTask { + let updatePostCategories (postId: PostId) oldCats newCats = backgroundTask { let toDelete, toAdd = Utils.diffLists oldCats newCats _.Value if List.isEmpty toDelete && List.isEmpty toAdd then return () else use cmd = conn.CreateCommand () - [ cmd.Parameters.AddWithValue ("@postId", PostId.toString postId) + [ cmd.Parameters.AddWithValue ("@postId", postId.Value) cmd.Parameters.Add ("@categoryId", SqliteType.Text) ] |> ignore let runCmd (catId: CategoryId) = backgroundTask { @@ -108,16 +108,16 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = } /// Update a post's assigned categories - let updatePostTags postId (oldTags : string list) newTags = backgroundTask { + let updatePostTags (postId: PostId) (oldTags: string list) newTags = backgroundTask { let toDelete, toAdd = Utils.diffLists oldTags newTags id if List.isEmpty toDelete && List.isEmpty toAdd then return () else use cmd = conn.CreateCommand () - [ cmd.Parameters.AddWithValue ("@postId", PostId.toString postId) + [ cmd.Parameters.AddWithValue ("@postId", postId.Value) cmd.Parameters.Add ("@tag", SqliteType.Text) ] |> ignore - let runCmd (tag : string) = backgroundTask { + let runCmd (tag: string) = backgroundTask { cmd.Parameters["@tag"].Value <- tag do! write cmd } @@ -134,17 +134,17 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = } /// Update a post's prior permalinks - let updatePostPermalinks postId oldLinks newLinks = backgroundTask { + let updatePostPermalinks (postId: PostId) oldLinks newLinks = backgroundTask { let toDelete, toAdd = Utils.diffPermalinks oldLinks newLinks if List.isEmpty toDelete && List.isEmpty toAdd then return () else use cmd = conn.CreateCommand () - [ cmd.Parameters.AddWithValue ("@postId", PostId.toString postId) + [ cmd.Parameters.AddWithValue ("@postId", postId.Value) cmd.Parameters.Add ("@link", SqliteType.Text) ] |> ignore - let runCmd link = backgroundTask { - cmd.Parameters["@link"].Value <- Permalink.toString link + let runCmd (link: Permalink) = backgroundTask { + cmd.Parameters["@link"].Value <- link.Value do! write cmd } cmd.CommandText <- "DELETE FROM post_permalink WHERE post_id = @postId AND permalink = @link" @@ -160,7 +160,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = } /// Update a post's revisions - let updatePostRevisions postId oldRevs newRevs = backgroundTask { + let updatePostRevisions (postId: PostId) oldRevs newRevs = backgroundTask { let toDelete, toAdd = Utils.diffRevisions oldRevs newRevs if List.isEmpty toDelete && List.isEmpty toAdd then return () @@ -168,10 +168,10 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = use cmd = conn.CreateCommand () let runCmd withText rev = backgroundTask { cmd.Parameters.Clear () - [ cmd.Parameters.AddWithValue ("@postId", PostId.toString postId) + [ cmd.Parameters.AddWithValue ("@postId", postId.Value) cmd.Parameters.AddWithValue ("@asOf", instantParam rev.AsOf) ] |> ignore - if withText then cmd.Parameters.AddWithValue ("@text", MarkupText.toString rev.Text) |> ignore + if withText then cmd.Parameters.AddWithValue ("@text", rev.Text.Value) |> ignore do! write cmd } cmd.CommandText <- "DELETE FROM post_revision WHERE post_id = @postId AND as_of = @asOf" @@ -208,11 +208,11 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = } /// Count posts in a status for the given web log - let countByStatus status webLogId = backgroundTask { + let countByStatus (status: PostStatus) webLogId = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- "SELECT COUNT(id) FROM post WHERE web_log_id = @webLogId AND status = @status" addWebLogId cmd webLogId - cmd.Parameters.AddWithValue ("@status", PostStatus.toString status) |> ignore + cmd.Parameters.AddWithValue ("@status", status.Value) |> ignore return! count cmd } @@ -226,11 +226,11 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = } /// Find a post by its permalink for the given web log (excluding revisions and prior permalinks) - let findByPermalink permalink webLogId = backgroundTask { + let findByPermalink (permalink: Permalink) webLogId = backgroundTask { use cmd = conn.CreateCommand () cmd.CommandText <- $"{selectPost} WHERE p.web_log_id = @webLogId AND p.permalink = @link" addWebLogId cmd webLogId - cmd.Parameters.AddWithValue ("@link", Permalink.toString permalink) |> ignore + cmd.Parameters.AddWithValue ("@link", permalink.Value) |> ignore use! rdr = cmd.ExecuteReaderAsync () if rdr.Read () then let! post = appendPostCategoryAndTag (toPost rdr) @@ -253,7 +253,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = match! findFullById postId webLogId with | Some _ -> use cmd = conn.CreateCommand () - cmd.Parameters.AddWithValue ("@id", PostId.toString postId) |> ignore + cmd.Parameters.AddWithValue ("@id", postId.Value) |> ignore cmd.CommandText <- "DELETE FROM post_revision WHERE post_id = @id; DELETE FROM post_permalink WHERE post_id = @id; @@ -269,7 +269,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = /// Find the current permalink from a list of potential prior permalinks for the given web log let findCurrentPermalink permalinks webLogId = backgroundTask { use cmd = conn.CreateCommand () - let linkSql, linkParams = inClause "AND pp.permalink" "link" Permalink.toString permalinks + let linkSql, linkParams = inClause "AND pp.permalink" "link" (fun (it: Permalink) -> it.Value) permalinks cmd.CommandText <- $" SELECT p.permalink FROM post p @@ -301,7 +301,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = /// Get a page of categorized posts for the given web log (excludes revisions and prior permalinks) let findPageOfCategorizedPosts webLogId categoryIds pageNbr postsPerPage = backgroundTask { use cmd = conn.CreateCommand () - let catSql, catParams = inClause "AND pc.category_id" "catId" (_.Value) categoryIds + let catSql, catParams = inClause "AND pc.category_id" "catId" (fun (it: CategoryId) -> it.Value) categoryIds cmd.CommandText <- $" {selectPost} INNER JOIN post_category pc ON pc.post_id = p.id @@ -311,7 +311,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = ORDER BY published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" addWebLogId cmd webLogId - cmd.Parameters.AddWithValue ("@status", PostStatus.toString Published) |> ignore + cmd.Parameters.AddWithValue ("@status", Published.Value) |> ignore cmd.Parameters.AddRange catParams use! rdr = cmd.ExecuteReaderAsync () let! posts = @@ -348,7 +348,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = ORDER BY p.published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" addWebLogId cmd webLogId - cmd.Parameters.AddWithValue ("@status", PostStatus.toString Published) |> ignore + cmd.Parameters.AddWithValue ("@status", Published.Value) |> ignore use! rdr = cmd.ExecuteReaderAsync () let! posts = toList toPost rdr @@ -369,7 +369,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = ORDER BY p.published_on DESC LIMIT {postsPerPage + 1} OFFSET {(pageNbr - 1) * postsPerPage}" addWebLogId cmd webLogId - [ cmd.Parameters.AddWithValue ("@status", PostStatus.toString Published) + [ cmd.Parameters.AddWithValue ("@status", Published.Value) cmd.Parameters.AddWithValue ("@tag", tag) ] |> ignore use! rdr = cmd.ExecuteReaderAsync () @@ -391,7 +391,7 @@ type SQLitePostData (conn : SqliteConnection, ser : JsonSerializer) = ORDER BY p.published_on DESC LIMIT 1" addWebLogId cmd webLogId - [ cmd.Parameters.AddWithValue ("@status", PostStatus.toString Published) + [ cmd.Parameters.AddWithValue ("@status", Published.Value) cmd.Parameters.AddWithValue ("@publishedOn", instantParam publishedOn) ] |> ignore use! rdr = cmd.ExecuteReaderAsync () diff --git a/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs b/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs index 886e113..3614b79 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteUploadData.fs @@ -12,7 +12,7 @@ type SQLiteUploadData (conn : SqliteConnection) = let addUploadParameters (cmd : SqliteCommand) (upload : Upload) = [ cmd.Parameters.AddWithValue ("@id", UploadId.toString upload.Id) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString upload.WebLogId) - cmd.Parameters.AddWithValue ("@path", Permalink.toString upload.Path) + cmd.Parameters.AddWithValue ("@path", upload.Path.Value) cmd.Parameters.AddWithValue ("@updatedOn", instantParam upload.UpdatedOn) cmd.Parameters.AddWithValue ("@dataLength", upload.Data.Length) ] |> ignore @@ -53,7 +53,7 @@ type SQLiteUploadData (conn : SqliteConnection) = do! rdr.CloseAsync () cmd.CommandText <- "DELETE FROM upload WHERE id = @id AND web_log_id = @webLogId" do! write cmd - return Ok (Permalink.toString upload.Path) + return Ok upload.Path.Value else return Error $"""Upload ID {cmd.Parameters["@id"]} not found""" } diff --git a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs index f4b64e7..11a347c 100644 --- a/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs +++ b/src/MyWebLog.Data/SQLite/SQLiteWebLogData.fs @@ -46,7 +46,7 @@ type SQLiteWebLogData (conn : SqliteConnection, ser : JsonSerializer) = [ cmd.Parameters.AddWithValue ("@id", CustomFeedId.toString feed.Id) cmd.Parameters.AddWithValue ("@webLogId", WebLogId.toString webLogId) cmd.Parameters.AddWithValue ("@source", CustomFeedSource.toString feed.Source) - cmd.Parameters.AddWithValue ("@path", Permalink.toString feed.Path) + cmd.Parameters.AddWithValue ("@path", feed.Path.Value) cmd.Parameters.AddWithValue ("@podcast", maybe (if Option.isSome feed.Podcast then Some (Utils.serialize ser feed.Podcast) else None)) diff --git a/src/MyWebLog.Data/SQLiteData.fs b/src/MyWebLog.Data/SQLiteData.fs index 61d5f48..c25d9c2 100644 --- a/src/MyWebLog.Data/SQLiteData.fs +++ b/src/MyWebLog.Data/SQLiteData.fs @@ -195,7 +195,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger, ser : JsonS FundingUrl = Map.tryString "funding_url" podcastRdr FundingText = Map.tryString "funding_text" podcastRdr Medium = Map.tryString "medium" podcastRdr - |> Option.map PodcastMedium.parse + |> Option.map PodcastMedium.Parse } } |> List.ofSeq podcastRdr.Close () @@ -241,7 +241,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger, ser : JsonS |> List.iter (fun (postId, episode) -> cmd.CommandText <- "UPDATE post SET episode = @episode WHERE id = @id" [ cmd.Parameters.AddWithValue ("@episode", Utils.serialize ser episode) - cmd.Parameters.AddWithValue ("@id", PostId.toString postId) ] |> ignore + cmd.Parameters.AddWithValue ("@id", postId.Value) ] |> ignore let _ = cmd.ExecuteNonQuery () cmd.Parameters.Clear ()) diff --git a/src/MyWebLog.Data/Utils.fs b/src/MyWebLog.Data/Utils.fs index 14f94aa..285a5f2 100644 --- a/src/MyWebLog.Data/Utils.fs +++ b/src/MyWebLog.Data/Utils.fs @@ -34,11 +34,11 @@ let diffMetaItems (oldItems : MetaItem list) newItems = /// Find the permalinks added and removed let diffPermalinks oldLinks newLinks = - diffLists oldLinks newLinks Permalink.toString + diffLists oldLinks newLinks (fun (it: Permalink) -> it.Value) /// Find the revisions added and removed let diffRevisions oldRevs newRevs = - diffLists oldRevs newRevs (fun (rev: Revision) -> $"{rev.AsOf.ToUnixTimeTicks()}|{MarkupText.toString rev.Text}") + diffLists oldRevs newRevs (fun (rev: Revision) -> $"{rev.AsOf.ToUnixTimeTicks()}|{rev.Text.Value}") open MyWebLog.Converters open Newtonsoft.Json diff --git a/src/MyWebLog.Domain/DataTypes.fs b/src/MyWebLog.Domain/DataTypes.fs index baf4742..ad0c0d1 100644 --- a/src/MyWebLog.Domain/DataTypes.fs +++ b/src/MyWebLog.Domain/DataTypes.fs @@ -77,7 +77,7 @@ module Comment = /// An empty comment let empty = { Id = CommentId.Empty - PostId = PostId.empty + PostId = PostId.Empty InReplyToId = None Name = "" Email = "" @@ -136,11 +136,11 @@ module Page = /// An empty page let empty = { - Id = PageId.empty + Id = PageId.Empty WebLogId = WebLogId.empty AuthorId = WebLogUserId.empty Title = "" - Permalink = Permalink.empty + Permalink = Permalink.Empty PublishedOn = Noda.epoch UpdatedOn = Noda.epoch IsInPageList = false @@ -209,12 +209,12 @@ module Post = /// An empty post let empty = { - Id = PostId.empty + Id = PostId.Empty WebLogId = WebLogId.empty AuthorId = WebLogUserId.empty Status = Draft Title = "" - Permalink = Permalink.empty + Permalink = Permalink.Empty PublishedOn = None UpdatedOn = Noda.epoch Text = "" @@ -330,7 +330,7 @@ module Upload = let empty = { Id = UploadId.empty WebLogId = WebLogId.empty - Path = Permalink.empty + Path = Permalink.Empty UpdatedOn = Noda.epoch Data = [||] } @@ -406,16 +406,16 @@ module WebLog = $"{scheme[0]}://{host[0]}", if host.Length > 1 then $"""/{String.Join("/", host |> Array.skip 1)}""" else "" /// Generate an absolute URL for the given link - let absoluteUrl webLog permalink = - $"{webLog.UrlBase}/{Permalink.toString permalink}" + let absoluteUrl webLog (permalink: Permalink) = + $"{webLog.UrlBase}/{permalink.Value}" /// Generate a relative URL for the given link - let relativeUrl webLog permalink = + let relativeUrl webLog (permalink: Permalink) = let _, leadPath = hostAndPath webLog - $"{leadPath}/{Permalink.toString permalink}" + $"{leadPath}/{permalink.Value}" /// Convert an Instant (UTC reference) to the web log's local date/time - let localTime webLog (date : Instant) = + let localTime webLog (date: Instant) = match DateTimeZoneProviders.Tzdb[webLog.TimeZone] with | null -> date.ToDateTimeUtc() | tz -> date.InZone(tz).ToDateTimeUnspecified() diff --git a/src/MyWebLog.Domain/SupportTypes.fs b/src/MyWebLog.Domain/SupportTypes.fs index e6ac59c..ed25389 100644 --- a/src/MyWebLog.Domain/SupportTypes.fs +++ b/src/MyWebLog.Domain/SupportTypes.fs @@ -1,16 +1,22 @@ namespace MyWebLog open System +open Markdig open NodaTime /// Support functions for domain definition [] module private Helpers = + open Markdown.ColorCode + /// Create a new ID (short GUID) // https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID let newId () = Convert.ToBase64String(Guid.NewGuid().ToByteArray()).Replace('/', '_').Replace('+', '-')[..22] + + /// Pipeline with most extensions enabled + let markdownPipeline = MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().UseColorCode().Build() /// Functions to support NodaTime manipulation @@ -275,9 +281,6 @@ type Episode = { this.Duration |> Option.map (DurationPattern.CreateWithInvariantCulture("H:mm:ss").Format) -open Markdig -open Markdown.ColorCode - /// Types of markup text type MarkupText = /// Markdown text @@ -285,30 +288,27 @@ type MarkupText = /// HTML text | Html of string -/// Functions to support markup text -module MarkupText = - - /// Pipeline with most extensions enabled - let private _pipeline = MarkdownPipelineBuilder().UseSmartyPants().UseAdvancedExtensions().UseColorCode().Build() - - /// Get the source type for the markup text - let sourceType = function Markdown _ -> "Markdown" | Html _ -> "HTML" - - /// Get the raw text, regardless of type - let text = function Markdown text -> text | Html text -> text - - /// Get the string representation of the markup text - let toString it = $"{sourceType it}: {text it}" - - /// Get the HTML representation of the markup text - let toHtml = function Markdown text -> Markdown.ToHtml(text, _pipeline) | Html text -> text - /// Parse a string into a MarkupText instance - let parse (it : string) = + static member Parse(it: string) = match it with | text when text.StartsWith "Markdown: " -> Markdown text[10..] | text when text.StartsWith "HTML: " -> Html text[6..] | text -> invalidOp $"Cannot derive type of text ({text})" + + /// The source type for the markup text + member this.SourceType = + match this with Markdown _ -> "Markdown" | Html _ -> "HTML" + + /// The raw text, regardless of type + member this.Text = + match this with Markdown text -> text | Html text -> text + + /// The string representation of the markup text + member this.Value = $"{this.SourceType}: {this.Text}" + + /// The HTML representation of the markup text + member this.AsHtml() = + match this with Markdown text -> Markdown.ToHtml(text, markdownPipeline) | Html text -> text /// An item of metadata @@ -319,15 +319,13 @@ type MetaItem = { /// The metadata value Value : string -} - -/// Functions to support metadata items -module MetaItem = - +} with + /// An empty metadata item - let empty = + static member Empty = { Name = ""; Value = "" } + /// A revision of a page or post [] type Revision = { @@ -336,46 +334,45 @@ type Revision = { /// The text of the revision Text : MarkupText -} - -/// Functions to support revisions -module Revision = +} with /// An empty revision - let empty = + static member Empty = { AsOf = Noda.epoch; Text = Html "" } /// A permanent link -type Permalink = Permalink of string +[] +type Permalink = + | Permalink of string -/// Functions to support permalinks -module Permalink = - /// An empty permalink - let empty = Permalink "" - - /// Convert a permalink to a string - let toString = function Permalink p -> p + static member Empty = Permalink "" + + /// The string value of this permalink + member this.Value = + match this with Permalink it -> it /// An identifier for a page -type PageId = PageId of string +[] +type PageId = + | PageId of string -/// Functions to support page IDs -module PageId = - /// An empty page ID - let empty = PageId "" - - /// Convert a page ID to a string - let toString = function PageId pi -> pi + static member Empty = PageId "" /// Create a new page ID - let create = newId >> PageId + static member Create = + newId >> PageId + + /// The string value of this page ID + member this.Value = + match this with PageId it -> it /// PodcastIndex.org podcast:medium allowed values +[] type PodcastMedium = | Podcast | Music @@ -385,87 +382,82 @@ type PodcastMedium = | Newsletter | Blog -/// Functions to support podcast medium -module PodcastMedium = - - /// Convert a podcast medium to a string - let toString = - function - | Podcast -> "podcast" - | Music -> "music" - | Video -> "video" - | Film -> "film" - | Audiobook -> "audiobook" - | Newsletter -> "newsletter" - | Blog -> "blog" - /// Parse a string into a podcast medium - let parse value = - match value with - | "podcast" -> Podcast - | "music" -> Music - | "video" -> Video - | "film" -> Film - | "audiobook" -> Audiobook + static member Parse = + function + | "podcast" -> Podcast + | "music" -> Music + | "video" -> Video + | "film" -> Film + | "audiobook" -> Audiobook | "newsletter" -> Newsletter - | "blog" -> Blog - | it -> invalidArg "medium" $"{it} is not a valid podcast medium" + | "blog" -> Blog + | it -> invalidArg "medium" $"{it} is not a valid podcast medium" + + /// The string value of this podcast medium + member this.Value = + match this with + | Podcast -> "podcast" + | Music -> "music" + | Video -> "video" + | Film -> "film" + | Audiobook -> "audiobook" + | Newsletter -> "newsletter" + | Blog -> "blog" /// Statuses for posts +[] type PostStatus = /// The post should not be publicly available | Draft /// The post is publicly viewable | Published -/// Functions to support post statuses -module PostStatus = - - /// Convert a post status to a string - let toString = function Draft -> "Draft" | Published -> "Published" - /// Parse a string into a post status - let parse value = - match value with + static member Parse = + function | "Draft" -> Draft | "Published" -> Published - | it -> invalidArg "status" $"{it} is not a valid post status" + | it -> invalidArg "status" $"{it} is not a valid post status" + + /// The string representation of this post status + member this.Value = + match this with Draft -> "Draft" | Published -> "Published" /// An identifier for a post -type PostId = PostId of string +[] +type PostId = + | PostId of string -/// Functions to support post IDs -module PostId = - /// An empty post ID - let empty = PostId "" - - /// Convert a post ID to a string - let toString = function PostId pi -> pi + static member Empty = PostId "" /// Create a new post ID - let create = newId >> PostId + static member Create = + newId >> PostId + + /// Convert a post ID to a string + member this.Value = + match this with PostId it -> it /// A redirection for a previously valid URL +[] type RedirectRule = { /// The From string or pattern - From : string + From: string /// The To string or pattern - To : string + To: string /// Whether to use regular expressions on this rule - IsRegex : bool -} - -/// Functions to support redirect rules -module RedirectRule = - + IsRegex: bool +} with + /// An empty redirect rule - let empty = { + static member Empty = { From = "" To = "" IsRegex = false diff --git a/src/MyWebLog.Domain/ViewModels.fs b/src/MyWebLog.Domain/ViewModels.fs index 28c85e5..2f9a176 100644 --- a/src/MyWebLog.Domain/ViewModels.fs +++ b/src/MyWebLog.Domain/ViewModels.fs @@ -89,14 +89,14 @@ type DisplayCustomFeed = { module DisplayCustomFeed = /// Create a display version from a custom feed - let fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed = + let fromFeed (cats: DisplayCategory[]) (feed: CustomFeed) : DisplayCustomFeed = let source = match feed.Source with | Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}" | Tag tag -> $"Tag: {tag}" { Id = CustomFeedId.toString feed.Id Source = source - Path = Permalink.toString feed.Path + Path = feed.Path.Value IsPodcast = Option.isSome feed.Podcast } @@ -136,32 +136,30 @@ type DisplayPage = } /// Create a minimal display page (no text or metadata) from a database page - static member FromPageMinimal webLog (page : Page) = - let pageId = PageId.toString page.Id - { Id = pageId - AuthorId = WebLogUserId.toString page.AuthorId - Title = page.Title - Permalink = Permalink.toString page.Permalink - PublishedOn = WebLog.localTime webLog page.PublishedOn - UpdatedOn = WebLog.localTime webLog page.UpdatedOn - IsInPageList = page.IsInPageList - IsDefault = pageId = webLog.DefaultPage - Text = "" - Metadata = [] - } + static member FromPageMinimal webLog (page: Page) = { + Id = page.Id.Value + AuthorId = WebLogUserId.toString page.AuthorId + Title = page.Title + Permalink = page.Permalink.Value + PublishedOn = WebLog.localTime webLog page.PublishedOn + UpdatedOn = WebLog.localTime webLog page.UpdatedOn + IsInPageList = page.IsInPageList + IsDefault = page.Id.Value = webLog.DefaultPage + Text = "" + Metadata = [] + } /// Create a display page from a database page static member FromPage webLog (page : Page) = let _, extra = WebLog.hostAndPath webLog - let pageId = PageId.toString page.Id - { Id = pageId + { Id = page.Id.Value AuthorId = WebLogUserId.toString page.AuthorId Title = page.Title - Permalink = Permalink.toString page.Permalink + Permalink = page.Permalink.Value PublishedOn = WebLog.localTime webLog page.PublishedOn UpdatedOn = WebLog.localTime webLog page.UpdatedOn IsInPageList = page.IsInPageList - IsDefault = pageId = webLog.DefaultPage + IsDefault = page.Id.Value = webLog.DefaultPage Text = addBaseToRelativeUrls extra page.Text Metadata = page.Metadata } @@ -187,7 +185,7 @@ module DisplayRevision = let fromRevision webLog (rev : Revision) = { AsOf = rev.AsOf.ToDateTimeUtc () AsOfLocal = WebLog.localTime webLog rev.AsOf - Format = MarkupText.sourceType rev.Text + Format = rev.Text.SourceType } @@ -253,7 +251,7 @@ module DisplayUpload = /// Create a display uploaded file let fromUpload webLog source (upload : Upload) = - let path = Permalink.toString upload.Path + let path = upload.Path.Value let name = Path.GetFileName path { Id = UploadId.toString upload.Id Name = name @@ -436,13 +434,13 @@ type EditCustomFeedModel = } /// Create a model from a custom feed - static member fromFeed (feed : CustomFeed) = + static member fromFeed (feed: CustomFeed) = let rss = { EditCustomFeedModel.empty with Id = CustomFeedId.toString feed.Id SourceType = match feed.Source with Category _ -> "category" | Tag _ -> "tag" SourceValue = match feed.Source with Category (CategoryId catId) -> catId | Tag tag -> tag - Path = Permalink.toString feed.Path + Path = feed.Path.Value } match feed.Podcast with | Some p -> @@ -454,7 +452,7 @@ type EditCustomFeedModel = Summary = p.Summary DisplayedAuthor = p.DisplayedAuthor Email = p.Email - ImageUrl = Permalink.toString p.ImageUrl + ImageUrl = p.ImageUrl.Value AppleCategory = p.AppleCategory AppleSubcategory = defaultArg p.AppleSubcategory "" Explicit = p.Explicit.Value @@ -462,10 +460,8 @@ type EditCustomFeedModel = MediaBaseUrl = defaultArg p.MediaBaseUrl "" FundingUrl = defaultArg p.FundingUrl "" FundingText = defaultArg p.FundingText "" - PodcastGuid = p.PodcastGuid - |> Option.map (fun it -> it.ToString().ToLowerInvariant ()) - |> Option.defaultValue "" - Medium = p.Medium |> Option.map PodcastMedium.toString |> Option.defaultValue "" + PodcastGuid = p.PodcastGuid |> Option.map _.ToString().ToLowerInvariant() |> Option.defaultValue "" + Medium = p.Medium |> Option.map _.Value |> Option.defaultValue "" } | None -> rss @@ -492,7 +488,7 @@ type EditCustomFeedModel = PodcastGuid = noneIfBlank this.PodcastGuid |> Option.map Guid.Parse FundingUrl = noneIfBlank this.FundingUrl FundingText = noneIfBlank this.FundingText - Medium = noneIfBlank this.Medium |> Option.map PodcastMedium.parse + Medium = noneIfBlank this.Medium |> Option.map PodcastMedium.Parse } else None @@ -530,61 +526,61 @@ type EditMyInfoModel = /// View model to edit a page [] -type EditPageModel = - { /// The ID of the page being edited - PageId : string +type EditPageModel = { + /// The ID of the page being edited + PageId: string - /// The title of the page - Title : string + /// The title of the page + Title: string - /// The permalink for the page - Permalink : string + /// The permalink for the page + Permalink: string - /// The template to use to display the page - Template : string - - /// Whether this page is shown in the page list - IsShownInPageList : bool + /// The template to use to display the page + Template: string + + /// Whether this page is shown in the page list + IsShownInPageList: bool - /// The source format for the text - Source : string + /// The source format for the text + Source: string - /// The text of the page - Text : string - - /// Names of metadata items - MetaNames : string[] - - /// Values of metadata items - MetaValues : string[] - } + /// The text of the page + Text: string + + /// Names of metadata items + MetaNames: string array + + /// Values of metadata items + MetaValues: string array +} with /// Create an edit model from an existing page - static member fromPage (page : Page) = + static member fromPage (page: Page) = let latest = - match page.Revisions |> List.sortByDescending (fun r -> r.AsOf) |> List.tryHead with + match page.Revisions |> List.sortByDescending _.AsOf |> List.tryHead with | Some rev -> rev - | None -> Revision.empty - let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.empty ] } else page - { PageId = PageId.toString page.Id + | None -> Revision.Empty + let page = if page.Metadata |> List.isEmpty then { page with Metadata = [ MetaItem.Empty ] } else page + { PageId = page.Id.Value Title = page.Title - Permalink = Permalink.toString page.Permalink + Permalink = page.Permalink.Value Template = defaultArg page.Template "" IsShownInPageList = page.IsInPageList - Source = MarkupText.sourceType latest.Text - Text = MarkupText.text latest.Text - MetaNames = page.Metadata |> List.map (fun m -> m.Name) |> Array.ofList - MetaValues = page.Metadata |> List.map (fun m -> m.Value) |> Array.ofList + Source = latest.Text.SourceType + Text = latest.Text.Text + MetaNames = page.Metadata |> List.map _.Name |> Array.ofList + MetaValues = page.Metadata |> List.map _.Value |> Array.ofList } /// Whether this is a new page member this.IsNew = this.PageId = "new" /// Update a page with values from this model - member this.UpdatePage (page : Page) now = - let revision = { AsOf = now; Text = MarkupText.parse $"{this.Source}: {this.Text}" } + member this.UpdatePage (page: Page) now = + let revision = { AsOf = now; Text = MarkupText.Parse $"{this.Source}: {this.Text}" } // Detect a permalink change, and add the prior one to the prior list - match Permalink.toString page.Permalink with + match page.Permalink.Value with | "" -> page | link when link = this.Permalink -> page | _ -> { page with PriorPermalinks = page.Permalink :: page.PriorPermalinks } @@ -596,7 +592,7 @@ type EditPageModel = UpdatedOn = now IsInPageList = this.IsShownInPageList Template = match this.Template with "" -> None | tmpl -> Some tmpl - Text = MarkupText.toHtml revision.Text + Text = revision.Text.AsHtml() Metadata = Seq.zip this.MetaNames this.MetaValues |> Seq.filter (fun it -> fst it > "") |> Seq.map (fun it -> { Name = fst it; Value = snd it }) @@ -610,127 +606,127 @@ type EditPageModel = /// View model to edit a post [] -type EditPostModel = - { /// The ID of the post being edited - PostId : string +type EditPostModel = { + /// The ID of the post being edited + PostId: string - /// The title of the post - Title : string + /// The title of the post + Title: string - /// The permalink for the post - Permalink : string + /// The permalink for the post + Permalink: string - /// The source format for the text - Source : string + /// The source format for the text + Source: string - /// The text of the post - Text : string - - /// The tags for the post - Tags : string - - /// The template used to display the post - Template : string - - /// The category IDs for the post - CategoryIds : string[] - - /// The post status - Status : string - - /// Whether this post should be published - DoPublish : bool - - /// Names of metadata items - MetaNames : string[] - - /// Values of metadata items - MetaValues : string[] - - /// Whether to override the published date/time - SetPublished : bool - - /// The published date/time to override - PubOverride : Nullable - - /// Whether all revisions should be purged and the override date set as the updated date as well - SetUpdated : bool - - /// Whether this post has a podcast episode - IsEpisode : bool - - /// The URL for the media for this episode (may be permalink) - Media : string - - /// The size (in bytes) of the media for this episode - Length : int64 - - /// The duration of the media for this episode - Duration : string - - /// The media type (optional, defaults to podcast-defined media type) - MediaType : string - - /// The URL for the image for this episode (may be permalink; optional, defaults to podcast image) - ImageUrl : string - - /// A subtitle for the episode (optional) - Subtitle : string - - /// The explicit rating for this episode (optional, defaults to podcast setting) - Explicit : string - - /// The URL for the chapter file for the episode (may be permalink; optional) - ChapterFile : string - - /// The type of the chapter file (optional; defaults to application/json+chapters if chapterFile is provided) - ChapterType : string - - /// The URL for the transcript (may be permalink; optional) - TranscriptUrl : string - - /// The MIME type for the transcript (optional, recommended if transcriptUrl is provided) - TranscriptType : string - - /// The language of the transcript (optional) - TranscriptLang : string - - /// Whether the provided transcript should be presented as captions - TranscriptCaptions : bool - - /// The season number (optional) - SeasonNumber : int - - /// A description of this season (optional, ignored if season number is not provided) - SeasonDescription : string - - /// The episode number (decimal; optional) - EpisodeNumber : string - - /// A description of this episode (optional, ignored if episode number is not provided) - EpisodeDescription : string - } + /// The text of the post + Text: string + + /// The tags for the post + Tags: string + + /// The template used to display the post + Template: string + + /// The category IDs for the post + CategoryIds: string array + + /// The post status + Status: string + + /// Whether this post should be published + DoPublish: bool + + /// Names of metadata items + MetaNames: string array + + /// Values of metadata items + MetaValues: string array + + /// Whether to override the published date/time + SetPublished: bool + + /// The published date/time to override + PubOverride: Nullable + + /// Whether all revisions should be purged and the override date set as the updated date as well + SetUpdated: bool + + /// Whether this post has a podcast episode + IsEpisode: bool + + /// The URL for the media for this episode (may be permalink) + Media: string + + /// The size (in bytes) of the media for this episode + Length: int64 + + /// The duration of the media for this episode + Duration: string + + /// The media type (optional, defaults to podcast-defined media type) + MediaType: string + + /// The URL for the image for this episode (may be permalink; optional, defaults to podcast image) + ImageUrl: string + + /// A subtitle for the episode (optional) + Subtitle: string + + /// The explicit rating for this episode (optional, defaults to podcast setting) + Explicit: string + + /// The URL for the chapter file for the episode (may be permalink; optional) + ChapterFile: string + + /// The type of the chapter file (optional; defaults to application/json+chapters if chapterFile is provided) + ChapterType: string + + /// The URL for the transcript (may be permalink; optional) + TranscriptUrl: string + + /// The MIME type for the transcript (optional, recommended if transcriptUrl is provided) + TranscriptType: string + + /// The language of the transcript (optional) + TranscriptLang: string + + /// Whether the provided transcript should be presented as captions + TranscriptCaptions: bool + + /// The season number (optional) + SeasonNumber: int + + /// A description of this season (optional, ignored if season number is not provided) + SeasonDescription: string + + /// The episode number (decimal; optional) + EpisodeNumber: string + + /// A description of this episode (optional, ignored if episode number is not provided) + EpisodeDescription: string +} with /// Create an edit model from an existing past - static member fromPost webLog (post : Post) = + static member fromPost webLog (post: Post) = let latest = - match post.Revisions |> List.sortByDescending (_.AsOf) |> List.tryHead with + match post.Revisions |> List.sortByDescending _.AsOf |> List.tryHead with | Some rev -> rev - | None -> Revision.empty - let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.empty ] } else post + | None -> Revision.Empty + let post = if post.Metadata |> List.isEmpty then { post with Metadata = [ MetaItem.Empty ] } else post let episode = defaultArg post.Episode Episode.Empty - { PostId = PostId.toString post.Id + { PostId = post.Id.Value Title = post.Title - Permalink = Permalink.toString post.Permalink - Source = MarkupText.sourceType latest.Text - Text = MarkupText.text latest.Text + Permalink = post.Permalink.Value + Source = latest.Text.SourceType + Text = latest.Text.Text Tags = String.Join (", ", post.Tags) Template = defaultArg post.Template "" - CategoryIds = post.CategoryIds |> List.map (_.Value) |> Array.ofList - Status = PostStatus.toString post.Status + CategoryIds = post.CategoryIds |> List.map _.Value |> Array.ofList + Status = post.Status.Value DoPublish = false - MetaNames = post.Metadata |> List.map (_.Name) |> Array.ofList - MetaValues = post.Metadata |> List.map (_.Value) |> Array.ofList + MetaNames = post.Metadata |> List.map _.Name |> Array.ofList + MetaValues = post.Metadata |> List.map _.Value |> Array.ofList SetPublished = false PubOverride = post.PublishedOn |> Option.map (WebLog.localTime webLog) |> Option.toNullable SetUpdated = false @@ -741,7 +737,7 @@ type EditPostModel = MediaType = defaultArg episode.MediaType "" ImageUrl = defaultArg episode.ImageUrl "" Subtitle = defaultArg episode.Subtitle "" - Explicit = defaultArg (episode.Explicit |> Option.map (_.Value)) "" + Explicit = defaultArg (episode.Explicit |> Option.map _.Value) "" ChapterFile = defaultArg episode.ChapterFile "" ChapterType = defaultArg episode.ChapterType "" TranscriptUrl = defaultArg episode.TranscriptUrl "" @@ -758,10 +754,10 @@ type EditPostModel = member this.IsNew = this.PostId = "new" /// Update a post with values from the submitted form - member this.UpdatePost (post : Post) now = - let revision = { AsOf = now; Text = MarkupText.parse $"{this.Source}: {this.Text}" } + member this.UpdatePost (post: Post) now = + let revision = { AsOf = now; Text = MarkupText.Parse $"{this.Source}: {this.Text}" } // Detect a permalink change, and add the prior one to the prior list - match Permalink.toString post.Permalink with + match post.Permalink.Value with | "" -> post | link when link = this.Permalink -> post | _ -> { post with PriorPermalinks = post.Permalink :: post.PriorPermalinks } @@ -772,7 +768,7 @@ type EditPostModel = Permalink = Permalink this.Permalink PublishedOn = if this.DoPublish then Some now else post.PublishedOn UpdatedOn = now - Text = MarkupText.toHtml revision.Text + Text = revision.Text.AsHtml() Tags = this.Tags.Split "," |> Seq.ofArray |> Seq.map (fun it -> it.Trim().ToLower ()) @@ -1005,39 +1001,39 @@ type LogOnModel = /// View model to manage permalinks [] -type ManagePermalinksModel = - { /// The ID for the entity being edited - Id : string - - /// The type of entity being edited ("page" or "post") - Entity : string - - /// The current title of the page or post - CurrentTitle : string - - /// The current permalink of the page or post - CurrentPermalink : string - - /// The prior permalinks for the page or post - Prior : string[] - } +type ManagePermalinksModel = { + /// The ID for the entity being edited + Id: string + + /// The type of entity being edited ("page" or "post") + Entity: string + + /// The current title of the page or post + CurrentTitle: string + + /// The current permalink of the page or post + CurrentPermalink: string + + /// The prior permalinks for the page or post + Prior: string array +} with /// Create a permalink model from a page - static member fromPage (pg : Page) = - { Id = PageId.toString pg.Id + static member fromPage (pg: Page) = + { Id = pg.Id.Value Entity = "page" CurrentTitle = pg.Title - CurrentPermalink = Permalink.toString pg.Permalink - Prior = pg.PriorPermalinks |> List.map Permalink.toString |> Array.ofList + CurrentPermalink = pg.Permalink.Value + Prior = pg.PriorPermalinks |> List.map _.Value |> Array.ofList } /// Create a permalink model from a post - static member fromPost (post : Post) = - { Id = PostId.toString post.Id + static member fromPost (post: Post) = + { Id = post.Id.Value Entity = "post" CurrentTitle = post.Title - CurrentPermalink = Permalink.toString post.Permalink - Prior = post.PriorPermalinks |> List.map Permalink.toString |> Array.ofList + CurrentPermalink = post.Permalink.Value + Prior = post.PriorPermalinks |> List.map _.Value |> Array.ofList } @@ -1054,20 +1050,20 @@ type ManageRevisionsModel = CurrentTitle : string /// The revisions for the page or post - Revisions : DisplayRevision[] + Revisions : DisplayRevision array } /// Create a revision model from a page - static member fromPage webLog (pg : Page) = - { Id = PageId.toString pg.Id + static member fromPage webLog (pg: Page) = + { Id = pg.Id.Value Entity = "page" CurrentTitle = pg.Title Revisions = pg.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList } /// Create a revision model from a post - static member fromPost webLog (post : Post) = - { Id = PostId.toString post.Id + static member fromPost webLog (post: Post) = + { Id = post.Id.Value Entity = "post" CurrentTitle = post.Title Revisions = post.Revisions |> List.map (DisplayRevision.fromRevision webLog) |> Array.ofList @@ -1076,53 +1072,53 @@ type ManageRevisionsModel = /// View model for posts in a list [] -type PostListItem = - { /// The ID of the post - Id : string - - /// The ID of the user who authored the post - AuthorId : string - - /// The status of the post - Status : string - - /// The title of the post - Title : string - - /// The permalink for the post - Permalink : string - - /// When this post was published - PublishedOn : Nullable - - /// When this post was last updated - UpdatedOn : DateTime - - /// The text of the post - Text : string - - /// The IDs of the categories for this post - CategoryIds : string list - - /// Tags for the post - Tags : string list - - /// The podcast episode information for this post - Episode : Episode option - - /// Metadata for the post - Metadata : MetaItem list - } +type PostListItem = { + /// The ID of the post + Id: string + + /// The ID of the user who authored the post + AuthorId: string + + /// The status of the post + Status: string + + /// The title of the post + Title: string + + /// The permalink for the post + Permalink: string + + /// When this post was published + PublishedOn: Nullable + + /// When this post was last updated + UpdatedOn: DateTime + + /// The text of the post + Text: string + + /// The IDs of the categories for this post + CategoryIds: string list + + /// Tags for the post + Tags: string list + + /// The podcast episode information for this post + Episode: Episode option + + /// Metadata for the post + Metadata: MetaItem list +} with /// Create a post list item from a post - static member fromPost (webLog : WebLog) (post : Post) = + static member fromPost (webLog: WebLog) (post: Post) = let _, extra = WebLog.hostAndPath webLog let inTZ = WebLog.localTime webLog - { Id = PostId.toString post.Id + { Id = post.Id.Value AuthorId = WebLogUserId.toString post.AuthorId - Status = PostStatus.toString post.Status + Status = post.Status.Value Title = post.Title - Permalink = Permalink.toString post.Permalink + Permalink = post.Permalink.Value PublishedOn = post.PublishedOn |> Option.map inTZ |> Option.toNullable UpdatedOn = inTZ post.UpdatedOn Text = addBaseToRelativeUrls extra post.Text diff --git a/src/MyWebLog/DotLiquidBespoke.fs b/src/MyWebLog/DotLiquidBespoke.fs index 83e9a3f..45006b7 100644 --- a/src/MyWebLog/DotLiquidBespoke.fs +++ b/src/MyWebLog/DotLiquidBespoke.fs @@ -101,10 +101,10 @@ type ThemeAssetFilter () = /// Create various items in the page header based on the state of the page being generated -type PageHeadTag () = - inherit Tag () +type PageHeadTag() = + inherit Tag() - override this.Render (context : Context, result : TextWriter) = + override this.Render(context: Context, result: TextWriter) = let webLog = context.WebLog // spacer let s = " " @@ -115,9 +115,9 @@ type PageHeadTag () = // Theme assets if assetExists "style.css" webLog then - result.WriteLine $"""{s}""" + result.WriteLine $"""{s}""" if assetExists "favicon.ico" webLog then - result.WriteLine $"""{s}""" + result.WriteLine $"""{s}""" // RSS feeds and canonical URLs let feedLink title url = @@ -126,16 +126,16 @@ type PageHeadTag () = $"""{s}""" if webLog.Rss.IsFeedEnabled && getBool "is_home" then - result.WriteLine (feedLink webLog.Name webLog.Rss.FeedName) - result.WriteLine $"""{s}""" + result.WriteLine(feedLink webLog.Name webLog.Rss.FeedName) + result.WriteLine $"""{s}""" if webLog.Rss.IsCategoryEnabled && getBool "is_category_home" then let slug = context.Environments[0].["slug"] :?> string - result.WriteLine (feedLink webLog.Name $"category/{slug}/{webLog.Rss.FeedName}") + result.WriteLine(feedLink webLog.Name $"category/{slug}/{webLog.Rss.FeedName}") if webLog.Rss.IsTagEnabled && getBool "is_tag_home" then let slug = context.Environments[0].["slug"] :?> string - result.WriteLine (feedLink webLog.Name $"tag/{slug}/{webLog.Rss.FeedName}") + result.WriteLine(feedLink webLog.Name $"tag/{slug}/{webLog.Rss.FeedName}") if getBool "is_post" then let post = context.Environments[0].["model"] :?> PostDisplay diff --git a/src/MyWebLog/Handlers/Admin.fs b/src/MyWebLog/Handlers/Admin.fs index de59270..0e9af33 100644 --- a/src/MyWebLog/Handlers/Admin.fs +++ b/src/MyWebLog/Handlers/Admin.fs @@ -233,7 +233,7 @@ module RedirectRules = if idx = -1 then return! hashForPage "Add Redirect Rule" - |> addToHash "model" (EditRedirectRuleModel.fromRule -1 RedirectRule.empty) + |> addToHash "model" (EditRedirectRuleModel.fromRule -1 RedirectRule.Empty) |> withAntiCsrf ctx |> adminBareView "redirect-edit" next ctx else @@ -260,7 +260,7 @@ module RedirectRules = let! model = ctx.BindFormAsync () let isNew = idx = -1 let rules = ctx.WebLog.RedirectRules - let rule = model.UpdateRule (if isNew then RedirectRule.empty else List.item idx rules) + let rule = model.UpdateRule (if isNew then RedirectRule.Empty else List.item idx rules) let newRules = match isNew with | true when model.InsertAtTop -> List.insertAt 0 rule rules @@ -545,7 +545,7 @@ module WebLog = match! TemplateCache.get adminTheme "tag-mapping-list-body" ctx.Data with | Ok tagMapTemplate -> let! allPages = data.Page.All ctx.WebLog.Id - let! themes = data.Theme.All () + let! themes = data.Theme.All() let! users = data.WebLogUser.FindByWebLog ctx.WebLog.Id let! hash = hashForPage "Web Log Settings" @@ -553,10 +553,10 @@ module WebLog = |> addToHash ViewContext.Model (SettingsModel.fromWebLog ctx.WebLog) |> addToHash "pages" ( seq { - KeyValuePair.Create ("posts", "- First Page of Posts -") + KeyValuePair.Create("posts", "- First Page of Posts -") yield! allPages - |> List.sortBy (fun p -> p.Title.ToLower ()) - |> List.map (fun p -> KeyValuePair.Create (PageId.toString p.Id, p.Title)) + |> List.sortBy _.Title.ToLower() + |> List.map (fun p -> KeyValuePair.Create(p.Id.Value, p.Title)) } |> Array.ofSeq) |> addToHash "themes" ( diff --git a/src/MyWebLog/Handlers/Feed.fs b/src/MyWebLog/Handlers/Feed.fs index 2db2de4..1d8dcda 100644 --- a/src/MyWebLog/Handlers/Feed.fs +++ b/src/MyWebLog/Handlers/Feed.fs @@ -37,13 +37,12 @@ let deriveFeedType (ctx : HttpContext) feedPath : (FeedType * int) option = | false -> // Category and tag feeds are handled by defined routes; check for custom feed match webLog.Rss.CustomFeeds - |> List.tryFind (fun it -> feedPath.EndsWith (Permalink.toString it.Path)) with + |> List.tryFind (fun it -> feedPath.EndsWith it.Path.Value) with | Some feed -> debug (fun () -> "Found custom feed") - Some (Custom (feed, feedPath), - feed.Podcast |> Option.map (fun p -> p.ItemsInFeed) |> Option.defaultValue postCount) + Some (Custom (feed, feedPath), feed.Podcast |> Option.map _.ItemsInFeed |> Option.defaultValue postCount) | None -> - debug (fun () -> $"No matching feed found") + debug (fun () -> "No matching feed found") None /// Determine the function to retrieve posts for the given feed @@ -142,7 +141,7 @@ let private addEpisode webLog (podcast : PodcastOptions) (episode : Episode) (po | link when Option.isSome podcast.MediaBaseUrl -> $"{podcast.MediaBaseUrl.Value}{link}" | link -> WebLog.absoluteUrl webLog (Permalink link) let epMediaType = [ episode.MediaType; podcast.DefaultMediaType ] |> List.tryFind Option.isSome |> Option.flatten - let epImageUrl = defaultArg episode.ImageUrl (Permalink.toString podcast.ImageUrl) |> toAbsolute webLog + let epImageUrl = defaultArg episode.ImageUrl podcast.ImageUrl.Value |> toAbsolute webLog let epExplicit = (defaultArg episode.Explicit podcast.Explicit).Value let xmlDoc = XmlDocument() @@ -310,8 +309,7 @@ let private addPodcast webLog (rssFeed : SyndicationFeed) (feed : CustomFeed) = podcast.PodcastGuid |> Option.iter (fun guid -> rssFeed.ElementExtensions.Add("guid", Namespace.podcast, guid.ToString().ToLowerInvariant())) - podcast.Medium - |> Option.iter (fun med -> rssFeed.ElementExtensions.Add("medium", Namespace.podcast, PodcastMedium.toString med)) + podcast.Medium |> Option.iter (fun med -> rssFeed.ElementExtensions.Add("medium", Namespace.podcast, med.Value)) /// Get the feed's self reference and non-feed link let private selfAndLink webLog feedType ctx = @@ -370,7 +368,7 @@ let createFeed (feedType : FeedType) posts : HttpHandler = fun next ctx -> backg match podcast, post.Episode with | Some feed, Some episode -> addEpisode webLog (Option.get feed.Podcast) episode post item | Some _, _ -> - warn "Feed" ctx $"[{webLog.Name} {Permalink.toString self}] \"{stripHtml post.Title}\" has no media" + warn "Feed" ctx $"[{webLog.Name} {self.Value}] \"{stripHtml post.Title}\" has no media" item | _ -> item @@ -437,14 +435,14 @@ let editCustomFeed feedId : HttpHandler = requireAccess WebLogAdmin >=> fun next |> withAntiCsrf ctx |> addToHash ViewContext.Model (EditCustomFeedModel.fromFeed f) |> addToHash "medium_values" [| - KeyValuePair.Create ("", "– Unspecified –") - KeyValuePair.Create (PodcastMedium.toString Podcast, "Podcast") - KeyValuePair.Create (PodcastMedium.toString Music, "Music") - KeyValuePair.Create (PodcastMedium.toString Video, "Video") - KeyValuePair.Create (PodcastMedium.toString Film, "Film") - KeyValuePair.Create (PodcastMedium.toString Audiobook, "Audiobook") - KeyValuePair.Create (PodcastMedium.toString Newsletter, "Newsletter") - KeyValuePair.Create (PodcastMedium.toString Blog, "Blog") + KeyValuePair.Create("", "– Unspecified –") + KeyValuePair.Create(Podcast.Value, "Podcast") + KeyValuePair.Create(Music.Value, "Music") + KeyValuePair.Create(Video.Value, "Video") + KeyValuePair.Create(Film.Value, "Film") + KeyValuePair.Create(Audiobook.Value, "Audiobook") + KeyValuePair.Create(Newsletter.Value, "Newsletter") + KeyValuePair.Create(Blog.Value, "Blog") |] |> adminView "custom-feed-edit" next ctx | None -> Error.notFound next ctx diff --git a/src/MyWebLog/Handlers/Page.fs b/src/MyWebLog/Handlers/Page.fs index cfdebbd..1eece85 100644 --- a/src/MyWebLog/Handlers/Page.fs +++ b/src/MyWebLog/Handlers/Page.fs @@ -133,7 +133,7 @@ let previewRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun return! {| content = [ """
""" - (MarkupText.toHtml >> addBaseToRelativeUrls extra) rev.Text + rev.Text.AsHtml() |> addBaseToRelativeUrls extra "
" ] |> String.concat "" @@ -174,13 +174,13 @@ let deleteRevision (pgId, revDate) : HttpHandler = requireAccess Author >=> fun // POST /admin/page/save let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { - let! model = ctx.BindFormAsync () + let! model = ctx.BindFormAsync() let data = ctx.Data let now = Noda.now () let tryPage = if model.IsNew then { Page.empty with - Id = PageId.create () + Id = PageId.Create() WebLogId = ctx.WebLog.Id AuthorId = ctx.UserId PublishedOn = now @@ -193,7 +193,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { do! (if model.IsNew then data.Page.Add else data.Page.Update) updatedPage if updateList then do! PageListCache.update ctx do! addMessage ctx { UserMessage.success with Message = "Page saved successfully" } - return! redirectToGet $"admin/page/{PageId.toString page.Id}/edit" next ctx + return! redirectToGet $"admin/page/{page.Id.Value}/edit" next ctx | Some _ -> return! Error.notAuthorized next ctx | None -> return! Error.notFound next ctx } diff --git a/src/MyWebLog/Handlers/Post.fs b/src/MyWebLog/Handlers/Post.fs index 0f5ea97..d9e87cd 100644 --- a/src/MyWebLog/Handlers/Post.fs +++ b/src/MyWebLog/Handlers/Post.fs @@ -39,7 +39,7 @@ open MyWebLog.Data open MyWebLog.ViewModels /// Convert a list of posts into items ready to be displayed -let preparePostList webLog posts listType (url : string) pageNbr perPage (data : IData) = task { +let preparePostList webLog posts listType (url: string) pageNbr perPage (data: IData) = task { let! authors = getAuthors webLog posts data let! tagMappings = getTagMappings webLog posts data let relUrl it = Some <| WebLog.relativeUrl webLog (Permalink it) @@ -58,7 +58,7 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage (data : | _ -> Task.FromResult (None, None) let newerLink = match listType, pageNbr with - | SinglePost, _ -> newerPost |> Option.map (fun p -> Permalink.toString p.Permalink) + | SinglePost, _ -> newerPost |> Option.map _.Permalink.Value | _, 1 -> None | PostList, 2 when webLog.DefaultPage = "posts" -> Some "" | PostList, _ -> relUrl $"page/{pageNbr - 1}" @@ -70,7 +70,7 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage (data : | AdminList, _ -> relUrl $"admin/posts/page/{pageNbr - 1}" let olderLink = match listType, List.length posts > perPage with - | SinglePost, _ -> olderPost |> Option.map (fun p -> Permalink.toString p.Permalink) + | SinglePost, _ -> olderPost |> Option.map _.Permalink.Value | _, false -> None | PostList, true -> relUrl $"page/{pageNbr + 1}" | CategoryList, true -> relUrl $"category/{url}/page/{pageNbr + 1}" @@ -81,9 +81,9 @@ let preparePostList webLog posts listType (url : string) pageNbr perPage (data : Authors = authors Subtitle = None NewerLink = newerLink - NewerName = newerPost |> Option.map (fun p -> p.Title) + NewerName = newerPost |> Option.map _.Title OlderLink = olderLink - OlderName = olderPost |> Option.map (fun p -> p.Title) + OlderName = olderPost |> Option.map _.Title } return makeHash {||} @@ -333,7 +333,7 @@ let previewRevision (postId, revDate) : HttpHandler = requireAccess Author >=> f return! {| content = [ """
""" - (MarkupText.toHtml >> addBaseToRelativeUrls extra) rev.Text + rev.Text.AsHtml() |> addBaseToRelativeUrls extra "
" ] |> String.concat "" @@ -374,12 +374,12 @@ let deleteRevision (postId, revDate) : HttpHandler = requireAccess Author >=> fu // POST /admin/post/save let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { - let! model = ctx.BindFormAsync () + let! model = ctx.BindFormAsync() let data = ctx.Data let tryPost = if model.IsNew then { Post.empty with - Id = PostId.create () + Id = PostId.Create() WebLogId = ctx.WebLog.Id AuthorId = ctx.UserId } |> someTask @@ -410,7 +410,7 @@ let save : HttpHandler = requireAccess Author >=> fun next ctx -> task { |> List.length = List.length priorCats) then do! CategoryCache.update ctx do! addMessage ctx { UserMessage.success with Message = "Post saved successfully" } - return! redirectToGet $"admin/post/{PostId.toString post.Id}/edit" next ctx + return! redirectToGet $"admin/post/{post.Id.Value}/edit" next ctx | Some _ -> return! Error.notAuthorized next ctx | None -> return! Error.notFound next ctx } diff --git a/src/MyWebLog/Handlers/Routes.fs b/src/MyWebLog/Handlers/Routes.fs index 5c6d371..c128864 100644 --- a/src/MyWebLog/Handlers/Routes.fs +++ b/src/MyWebLog/Handlers/Routes.fs @@ -11,20 +11,20 @@ module CatchAll = open MyWebLog.ViewModels /// Sequence where the first returned value is the proper handler for the link - let private deriveAction (ctx : HttpContext) : HttpHandler seq = + let private deriveAction (ctx: HttpContext): HttpHandler seq = let webLog = ctx.WebLog let data = ctx.Data let debug = debug "Routes.CatchAll" ctx let textLink = let _, extra = WebLog.hostAndPath webLog let url = string ctx.Request.Path - (if extra = "" then url else url.Substring extra.Length).ToLowerInvariant () + (if extra = "" then url else url.Substring extra.Length).ToLowerInvariant() let await it = (Async.AwaitTask >> Async.RunSynchronously) it seq { debug (fun () -> $"Considering URL {textLink}") // Home page directory without the directory slash - if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.empty) - let permalink = Permalink (textLink.Substring 1) + if textLink = "" then yield redirectTo true (WebLog.relativeUrl webLog Permalink.Empty) + let permalink = Permalink textLink[1..] // Current post match data.Post.FindByPermalink permalink webLog.Id |> await with | Some post -> @@ -80,7 +80,7 @@ module CatchAll = } // GET {all-of-the-above} - let route : HttpHandler = fun next ctx -> + let route: HttpHandler = fun next ctx -> match deriveAction ctx |> Seq.tryHead with Some handler -> handler next ctx | None -> Error.notFound next ctx diff --git a/src/MyWebLog/Maintenance.fs b/src/MyWebLog/Maintenance.fs index ee7d934..8d0f68f 100644 --- a/src/MyWebLog/Maintenance.fs +++ b/src/MyWebLog/Maintenance.fs @@ -23,12 +23,12 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task { // Create the web log let webLogId = WebLogId.create () let userId = WebLogUserId.create () - let homePageId = PageId.create () + let homePageId = PageId.Create() let slug = Handlers.Upload.makeSlug args[2] // If this is the first web log being created, the user will be an installation admin; otherwise, they will be an // admin just over their web log - let! webLogs = data.WebLog.All () + let! webLogs = data.WebLog.All() let accessLevel = if List.isEmpty webLogs then Administrator else WebLogAdmin do! data.WebLog.Add @@ -37,7 +37,7 @@ let private doCreateWebLog (args : string[]) (sp : IServiceProvider) = task { Name = args[2] Slug = slug UrlBase = args[1] - DefaultPage = PageId.toString homePageId + DefaultPage = homePageId.Value TimeZone = timeZone } @@ -110,8 +110,8 @@ let private importPriorPermalinks urlBase file (sp : IServiceProvider) = task { let! withLinks = data.Post.FindFullById post.Id post.WebLogId let! _ = data.Post.UpdatePriorPermalinks post.Id post.WebLogId (old :: withLinks.Value.PriorPermalinks) - printfn $"{Permalink.toString old} -> {Permalink.toString current}" - | None -> eprintfn $"Cannot find current post for {Permalink.toString current}" + printfn $"{old.Value} -> {current.Value}" + | None -> eprintfn $"Cannot find current post for {current.Value}" printfn "Done!" | None -> eprintfn $"No web log found at {urlBase}" } @@ -336,8 +336,8 @@ module Backup = let newWebLogId = WebLogId.create () let newCatIds = archive.Categories |> List.map (fun cat -> cat.Id, CategoryId.Create ()) |> dict let newMapIds = archive.TagMappings |> List.map (fun tm -> tm.Id, TagMapId.create ()) |> dict - let newPageIds = archive.Pages |> List.map (fun page -> page.Id, PageId.create ()) |> dict - let newPostIds = archive.Posts |> List.map (fun post -> post.Id, PostId.create ()) |> dict + let newPageIds = archive.Pages |> List.map (fun page -> page.Id, PageId.Create ()) |> dict + let newPostIds = archive.Posts |> List.map (fun post -> post.Id, PostId.Create ()) |> dict let newUserIds = archive.Users |> List.map (fun user -> user.Id, WebLogUserId.create ()) |> dict let newUpIds = archive.Uploads |> List.map (fun up -> up.Id, UploadId.create ()) |> dict return