diff --git a/src/MyWebLog.Data/Postgres/PostgresPageData.fs b/src/MyWebLog.Data/Postgres/PostgresPageData.fs index 465172b..d8350bd 100644 --- a/src/MyWebLog.Data/Postgres/PostgresPageData.fs +++ b/src/MyWebLog.Data/Postgres/PostgresPageData.fs @@ -53,15 +53,18 @@ type PostgresPageData(log: ILogger) = log.LogTrace "Page.countListed" Count.byContains Table.Page {| webLogDoc webLogId with IsInPageList = true |} - /// Find a page by its ID (without revisions) - let findById pageId webLogId = + /// Find a page by its ID (without revisions or prior permalinks) + let findById pageId webLogId = backgroundTask { log.LogTrace "Page.findById" - Document.findByIdAndWebLog Table.Page pageId webLogId + match! Document.findByIdAndWebLog Table.Page pageId webLogId with + | Some page -> return Some { page with PriorPermalinks = [] } + | None -> return None + } /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { log.LogTrace "Page.findFullById" - match! findById pageId webLogId with + match! Document.findByIdAndWebLog Table.Page pageId webLogId with | Some page -> let! withMore = appendPageRevisions page return Some withMore diff --git a/src/MyWebLog.Data/RethinkDbData.fs b/src/MyWebLog.Data/RethinkDbData.fs index ce65ef3..70fbb6f 100644 --- a/src/MyWebLog.Data/RethinkDbData.fs +++ b/src/MyWebLog.Data/RethinkDbData.fs @@ -444,20 +444,27 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger 0UL } - member _.FindById pageId webLogId = - rethink { - withTable Table.Page - get pageId - without [ nameof Page.Empty.PriorPermalinks; nameof Page.Empty.Revisions ] - resultOption; withRetryOptionDefault - } - |> verifyWebLog webLogId (fun it -> it.WebLogId) <| conn + member _.FindById pageId webLogId = backgroundTask { + let! page = + rethink { + withTable Table.Page + getAll [ pageId ] + without [ nameof Page.Empty.PriorPermalinks; nameof Page.Empty.Revisions ] + result; withRetryDefault + } + |> tryFirst <| conn + return + page + |> Option.filter (fun pg -> pg.WebLogId = webLogId) + |> Option.map (fun pg -> { pg with Revisions = []; PriorPermalinks = [] }) + } member _.FindByPermalink permalink webLogId = rethink { withTable Table.Page getAll [ [| webLogId :> obj; permalink |] ] (nameof Page.Empty.Permalink) - without [ nameof Page.Empty.PriorPermalinks; nameof Page.Empty.Revisions ] + merge (r.HashMap(nameof Page.Empty.PriorPermalinks, [||]) + .With(nameof Page.Empty.Revisions, [||])) limit 1 result; withRetryDefault } @@ -474,7 +481,7 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger tryFirst) conn - return result |> Option.map (fun pg -> pg.Permalink) + return result |> Option.map _.Permalink } member _.FindFullById pageId webLogId = @@ -483,7 +490,7 @@ type RethinkDbData(conn: Net.IConnection, config: DataConfig, log: ILogger verifyWebLog webLogId (fun it -> it.WebLogId) <| conn + |> verifyWebLog webLogId _.WebLogId <| conn member _.FindFullByWebLog webLogId = rethink { withTable Table.Page diff --git a/src/MyWebLog.Data/SQLite/SQLitePageData.fs b/src/MyWebLog.Data/SQLite/SQLitePageData.fs index 738b5f4..1d11140 100644 --- a/src/MyWebLog.Data/SQLite/SQLitePageData.fs +++ b/src/MyWebLog.Data/SQLite/SQLitePageData.fs @@ -57,15 +57,18 @@ type SQLitePageData(conn: SqliteConnection, log: ILogger) = [ webLogParam webLogId ] (toCount >> int) - /// Find a page by its ID (without revisions) - let findById pageId webLogId = + /// Find a page by its ID (without revisions and prior permalinks) + let findById pageId webLogId = backgroundTask { log.LogTrace "Page.findById" - Document.findByIdAndWebLog Table.Page pageId webLogId conn + match! Document.findByIdAndWebLog Table.Page pageId webLogId conn with + | Some page -> return Some { page with PriorPermalinks = [] } + | None -> return None + } /// Find a complete page by its ID let findFullById pageId webLogId = backgroundTask { log.LogTrace "Page.findFullById" - match! findById pageId webLogId with + match! Document.findByIdAndWebLog Table.Page pageId webLogId conn with | Some page -> let! page = appendPageRevisions page return Some page diff --git a/src/MyWebLog.Tests/Data/PageDataTests.fs b/src/MyWebLog.Tests/Data/PageDataTests.fs index 1bece19..d8513ee 100644 --- a/src/MyWebLog.Tests/Data/PageDataTests.fs +++ b/src/MyWebLog.Tests/Data/PageDataTests.fs @@ -1,5 +1,6 @@ module PageDataTests +open System open Expecto open MyWebLog open MyWebLog.Data @@ -8,6 +9,12 @@ open NodaTime /// The ID of the root web log let rootId = WebLogId "uSitJEuD3UyzWC9jgOHc8g" +/// The ID of the "A cool page" page +let coolPageId = PageId "hgc_BLEZ50SoAWLuPNISvA" + +/// The published and updated time of the "A cool page" page +let coolPagePublished = Instant.FromDateTimeOffset(DateTimeOffset.Parse "2024-01-20T22:14:28Z") + let ``Add succeeds`` (data: IData) = task { let page = { Id = PageId "added-page" @@ -55,6 +62,8 @@ let ``All succeeds`` (data: IData) = task { Expect.isEmpty pg.Metadata $"Page {idx} should have had no metadata" Expect.isEmpty pg.Revisions $"Page {idx} should have had no revisions" Expect.isEmpty pg.PriorPermalinks $"Page {idx} should have had no prior permalinks") + let! others = data.Page.All (WebLogId "not-there") + Expect.isEmpty others "There should not be pages retrieved" } let ``CountAll succeeds`` (data: IData) = task { @@ -66,3 +75,54 @@ let ``CountListed succeeds`` (data: IData) = task { let! pages = data.Page.CountListed rootId Expect.equal pages 1 "There should have been 1 page in the page list" } + +let ``FindById succeeds when a page is found`` (data: IData) = task { + let! page = data.Page.FindById coolPageId rootId + Expect.isSome page "A page should have been returned" + let pg = page.Value + Expect.equal pg.Id coolPageId "The wrong page was retrieved" + Expect.equal pg.WebLogId rootId "The page's web log did not match the called parameter" + Expect.equal pg.AuthorId (WebLogUserId "5EM2rimH9kONpmd2zQkiVA") "Author ID is incorrect" + Expect.equal pg.Title "Page Title" "Title is incorrect" + Expect.equal pg.Permalink (Permalink "a-cool-page.html") "Permalink is incorrect" + Expect.equal + pg.PublishedOn (Instant.FromDateTimeOffset(DateTimeOffset.Parse "2024-01-20T22:14:28Z")) "Published On is incorrect" + Expect.equal pg.UpdatedOn (Instant.FromDateTimeOffset(DateTimeOffset.Parse "2024-01-20T22:14:28Z")) "Updated On is incorrect" + Expect.isFalse pg.IsInPageList "Is in page list flag should not have been set" + Expect.equal pg.Text "

A Cool Page

\n

It really is cool!

\n" "Text is incorrect" + Expect.hasLength pg.Metadata 2 "There should be 2 metadata items on this page" + Expect.equal pg.Metadata[0].Name "Cool" "Meta item 0 name is incorrect" + Expect.equal pg.Metadata[0].Value "true" "Meta item 0 value is incorrect" + Expect.equal pg.Metadata[1].Name "Warm" "Meta item 1 name is incorrect" + Expect.equal pg.Metadata[1].Value "false" "Meta item 1 value is incorrect" + Expect.isEmpty pg.Revisions "Revisions should not have been retrieved" + Expect.isEmpty pg.PriorPermalinks "Prior permalinks should not have been retrieved" +} + +let ``FindById succeeds when a page is not found (incorrect weblog)`` (data: IData) = task { + let! page = data.Page.FindById coolPageId (WebLogId "wrong") + Expect.isNone page "The page should not have been retrieved" +} + +let ``FindById succeeds when a page is not found (bad page ID)`` (data: IData) = task { + let! page = data.Page.FindById (PageId "missing") rootId + Expect.isNone page "The page should not have been retrieved" +} + +let ``FindFullById succeeds when a page is found`` (data: IData) = task { + let! page = data.Page.FindFullById coolPageId rootId + Expect.isSome page "A page should have been returned" + let pg = page.Value + Expect.equal pg.Id coolPageId "The wrong page was retrieved" + Expect.equal pg.WebLogId rootId "The page's web log did not match the called parameter" + Expect.hasLength pg.Revisions 1 "There should be 1 revision" + Expect.equal pg.Revisions[0].AsOf coolPagePublished "Revision 0 as-of is incorrect" + Expect.equal pg.Revisions[0].Text (Markdown "# A Cool Page\n\nIt really is cool!") "Revision 0 text is incorrect" + Expect.hasLength pg.PriorPermalinks 1 "There should be 1 prior permalink" + Expect.equal pg.PriorPermalinks[0] (Permalink "a-cool-pg.html") "Prior permalink 0 is incorrect" +} + +let ``FindFullById succeeds when a page is not found`` (data: IData) = task { + let! page = data.Page.FindFullById (PageId "not-there") rootId + Expect.isNone page "A page should not have been retrieved" +} diff --git a/src/MyWebLog.Tests/Data/PostgresDataTests.fs b/src/MyWebLog.Tests/Data/PostgresDataTests.fs index 6266bc6..8a87552 100644 --- a/src/MyWebLog.Tests/Data/PostgresDataTests.fs +++ b/src/MyWebLog.Tests/Data/PostgresDataTests.fs @@ -131,6 +131,25 @@ let pageTests = testList "Page" [ testTask "CountListed succeeds" { do! PageDataTests.``CountListed succeeds`` (mkData ()) } + testList "FindById" [ + testTask "succeeds when a page is found" { + do! PageDataTests.``FindById succeeds when a page is found`` (mkData ()) + } + testTask "succeeds when a page is not found (incorrect weblog)" { + do! PageDataTests.``FindById succeeds when a page is not found (incorrect weblog)`` (mkData ()) + } + testTask "succeeds when a page is not found (bad page ID)" { + do! PageDataTests.``FindById succeeds when a page is not found (bad page ID)`` (mkData ()) + } + ] + testList "FindFullById" [ + testTask "succeeds when a page is found" { + do! PageDataTests.``FindFullById succeeds when a page is found`` (mkData ()) + } + testTask "succeeds when a page is not found" { + do! PageDataTests.``FindFullById succeeds when a page is not found`` (mkData ()) + } + ] ] /// Drop the throwaway PostgreSQL database diff --git a/src/MyWebLog.Tests/Data/RethinkDbTests.fs b/src/MyWebLog.Tests/Data/RethinkDbTests.fs index d5abf93..f6d3863 100644 --- a/src/MyWebLog.Tests/Data/RethinkDbTests.fs +++ b/src/MyWebLog.Tests/Data/RethinkDbTests.fs @@ -130,6 +130,25 @@ let pageTests = testList "Page" [ testTask "CountListed succeeds" { do! PageDataTests.``CountListed succeeds`` data.Value } + testList "FindById" [ + testTask "succeeds when a page is found" { + do! PageDataTests.``FindById succeeds when a page is found`` data.Value + } + testTask "succeeds when a page is not found (incorrect weblog)" { + do! PageDataTests.``FindById succeeds when a page is not found (incorrect weblog)`` data.Value + } + testTask "succeeds when a page is not found (bad page ID)" { + do! PageDataTests.``FindById succeeds when a page is not found (bad page ID)`` data.Value + } + ] + testList "FindFullById" [ + testTask "succeeds when a page is found" { + do! PageDataTests.``FindFullById succeeds when a page is found`` data.Value + } + testTask "succeeds when a page is not found" { + do! PageDataTests.``FindFullById succeeds when a page is not found`` data.Value + } + ] ] /// Drop the throwaway RethinkDB database diff --git a/src/MyWebLog.Tests/Data/SQLiteDataTests.fs b/src/MyWebLog.Tests/Data/SQLiteDataTests.fs index a6bcde2..b2c724c 100644 --- a/src/MyWebLog.Tests/Data/SQLiteDataTests.fs +++ b/src/MyWebLog.Tests/Data/SQLiteDataTests.fs @@ -164,6 +164,35 @@ let pageTests = testList "Page" [ try do! PageDataTests.``CountListed succeeds`` data finally dispose data } + testList "FindById" [ + testTask "succeeds when a page is found" { + let data = mkData () + try do! PageDataTests.``FindById succeeds when a page is found`` data + finally dispose data + } + testTask "succeeds when a page is not found (incorrect weblog)" { + let data = mkData () + try do! PageDataTests.``FindById succeeds when a page is not found (incorrect weblog)`` data + finally dispose data + } + testTask "succeeds when a page is not found (bad page ID)" { + let data = mkData () + try do! PageDataTests.``FindById succeeds when a page is not found (bad page ID)`` data + finally dispose data + } + ] + testList "FindFullById" [ + testTask "succeeds when a page is found" { + let data = mkData () + try do! PageDataTests.``FindFullById succeeds when a page is found`` data + finally dispose data + } + testTask "succeeds when a page is not found" { + let data = mkData () + try do! PageDataTests.``FindFullById succeeds when a page is not found`` data + finally dispose data + } + ] ] /// Delete the SQLite database diff --git a/src/MyWebLog.Tests/root-weblog.json b/src/MyWebLog.Tests/root-weblog.json index 7577c81..39babe3 100644 --- a/src/MyWebLog.Tests/root-weblog.json +++ b/src/MyWebLog.Tests/root-weblog.json @@ -1 +1,344 @@ -{"WebLog":{"Id":"uSitJEuD3UyzWC9jgOHc8g","Name":"Root WebLog","Slug":"root-weblog","Subtitle":"This is the main one","DefaultPage":"posts","PostsPerPage":9,"ThemeId":"default","UrlBase":"http://localhost:8081","TimeZone":"America/Denver","Rss":{"IsFeedEnabled":true,"FeedName":"feed","ItemsInFeed":7,"IsCategoryEnabled":true,"IsTagEnabled":true,"Copyright":"CC40-NC-BY","CustomFeeds":[{"Id":"isPQ6drbDEydxohQzaiYtQ","Source":"tag:podcast","Path":"podcast-feed","Podcast":{"Title":"Root Podcast","ItemsInFeed":23,"Summary":"All things that happen in the domain root","DisplayedAuthor":"Podcaster Extraordinaire","Email":"podcaster@example.com","ImageUrl":"images/cover-art.png","AppleCategory":"Fiction","AppleSubcategory":"Drama","Explicit":"no","DefaultMediaType":"audio/mpeg","MediaBaseUrl":"https://media.example.com/root/","PodcastGuid":"10fd7f79-c719-4e1d-9da7-10405dd4fd96","FundingUrl":"https://example.com/support-us","FundingText":"Support Our Work","Medium":"newsletter"}}]},"AutoHtmx":true,"Uploads":"Database","RedirectRules":[]},"Users":[{"Id":"5EM2rimH9kONpmd2zQkiVA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Email":"root@example.com","FirstName":"Root","LastName":"Owner","PreferredName":"Admin","PasswordHash":"AQAAAAIAAYagAAAAEEnq9J9lKZoMQZaTOJHKIQo44skDdzDigzqS+o6myMop38YuHfm/vNs9b/WpYjsOxg==","AccessLevel":"Administrator","CreatedOn":"2024-01-20T21:49:03Z","LastSeenOn":"2024-01-20T22:25:03Z"},{"Id":"GPbJaSOwTkKt14ZKYyveKA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Email":"editor@example.com","FirstName":"Ed","LastName":"It-Or","PreferredName":"Edits","PasswordHash":"AQAAAAIAAYagAAAAEA8E3NwJkZO+q35FTmUT0wMNB8IpBOSVACKQcccXpaWfZJMRmZzjPEzd4j/f9h+rEA==","AccessLevel":"Editor","CreatedOn":"2024-01-20T21:58:42Z"},{"Id":"iIRNLSeY0EanxRPyqGuwVg","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Email":"author@example.com","FirstName":"Author","LastName":"Dude","PreferredName":"Mister","PasswordHash":"AQAAAAIAAYagAAAAEBYNf1sR/pjaX2dZgqlvpH/Tqpz2h/CG3rsk/wH2ReTysjpK/gxSqht7IULWXM7KHQ==","Url":"https://example.com/author","AccessLevel":"Author","CreatedOn":"2024-01-20T21:53:27Z"}],"Theme":{"Id":"default","Name":"myWebLog Default Theme","Version":"2.1.0","Templates":[{"Name":"single-post","Text":"{%- assign post = model.posts | first -%}\n

{{ post.title }}

\n

\n {% if post.published_on -%}\n Published {{ post.published_on | date: \"dddd, MMMM d, yyyy\" }}\n at {{ post.published_on | date: \"h:mm tt\" | downcase }}\n {%- else -%}\n **DRAFT**\n {% endif %}\n by {{ model.authors | value: post.author_id }}\n {%- if is_editor or is_author and user_id == post.author_id %}\n • Edit Post\n {%- endif %}\n

\n
\n
\n
{{ post.text }}
\n {%- assign cat_count = post.category_ids | size -%}\n {% if cat_count > 0 -%}\n

\n Categorized under\n {% for cat_id in post.category_ids -%}\n {% assign cat = categories | where: \"Id\", cat_id | first %}\n \n \n {{ cat.name }}\n \n \n {% unless forloop.last %} • {% endunless %}\n {%- endfor %}\n

\n {%- endif %}\n {%- assign tag_count = post.tags | size -%}\n {% if tag_count > 0 -%}\n

\n Tagged\n {% for tag in post.tags %}\n \n {{ tag }}\n \n {% unless forloop.last %} • {% endunless %}\n {%- endfor %}\n

\n {%- endif %}\n
\n
\n \n
\n
\n"},{"Name":"single-page","Text":"

{{ page.title }}

\n
{{ page.text }}
\n"},{"Name":"layout","Text":"\n\n \n \n \n {{ page_title | strip_html }}{% if page_title %} « {% endif %}{{ web_log.name | strip_html }}\n {% page_head -%}\n
\n \n
\n
\n {% if messages %}\n
\n {% for msg in messages %}\n \n {% endfor %}\n
\n {% endif %}\n {{ content }} \n
\n
\n
\n
\n myWebLog\n
\n
\n \n\n"},{"Name":"index","Text":"{%- if is_category or is_tag %}\n

{{ page_title }}

\n {%- if subtitle %}

{{ subtitle }}

{% endif -%}\n{% endif %}\n{%- assign post_count = model.posts | size -%}\n{%- if post_count > 0 %}\n
\n {%- for post in model.posts %}\n
\n

\n \n {{ post.title }}\n \n

\n

\n Published on {{ post.published_on | date: \"MMMM d, yyyy\" }}\n at {{ post.published_on | date: \"h:mmtt\" | downcase }}\n by {{ model.authors | value: post.author_id }}\n {{ post.text }}\n {%- assign category_count = post.category_ids | size -%}\n {%- assign tag_count = post.tags | size -%}\n {% if category_count > 0 or tag_count > 0 %}\n

\n

\n {%- if category_count > 0 -%}\n Categorized under:\n {% for cat in post.category_ids -%}\n {%- assign this_cat = categories | where: \"Id\", cat | first -%}\n {{ this_cat.name }}{% unless forloop.last %}, {% endunless %}\n {%- assign cat_names = this_cat.name | concat: cat_names -%}\n {%- endfor -%}\n {%- assign cat_names = \"\" -%}\n
\n {% endif -%}\n {%- if tag_count > 0 %}\n Tagged: {{ post.tags | join: \", \" }}\n {% endif -%}\n

\n {% endif %}\n
\n
\n {% endfor %}\n
\n \n{%- else %}\n

No posts found

\n{%- endif %}\n"}]},"Assets":[{"Id":"default/style.css","UpdatedOn":"2023-07-02T20:36:28Z","Data":"Lm1lc3NhZ2VzIHsKICBtYXgtd2lkdGg6IDYwcmVtOwogIG1hcmdpbjogYXV0bzsKfQpibG9ja3F1b3RlIHsKICBib3JkZXItbGVmdDogc29saWQgNHB4IGxpZ2h0Z3JheTsKICBwYWRkaW5nLWxlZnQ6IDFyZW07Cn0KLml0ZW0tbWV0YSB7CiAgZm9udC1zaXplOiAxLjFyZW07CiAgZm9udC13ZWlnaHQ6IG5vcm1hbDs7Cn0KLml0ZW0tbWV0YTo6YmVmb3JlIHsKICBjb250ZW50OiAiwrsiOwogIHZlcnRpY2FsLWFsaWduOiB0ZXh0LXRvcDsKfQphOmxpbmssIGE6dmlzaXRlZCB7CiAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwp9CmE6aG92ZXIgewogIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOwp9Cg=="}],"Categories":[{"Id":"S5JflPsJ9EG7gA2LD4m92A","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Name":"Favorites","Slug":"favorites","Description":"Favorite posts"},{"Id":"jw6N69YtTEWVHAO33jHU-w","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Name":"Spitball","Slug":"spitball","Description":"Posts that may or may not work"},{"Id":"ScVpyu1e7UiP7bDdge3ZEw","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Name":"Moonshot","Slug":"moonshot","ParentId":"jw6N69YtTEWVHAO33jHU-w"}],"TagMappings":[{"Id":"Icm027noqE-rPHKZA98vAw","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Tag":"f#","UrlValue":"f-sharp"},{"Id":"GdryXh-S0kGsNBs2RIacGA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","Tag":"ghoti","UrlValue":"fish"}],"Pages":[{"Id":"hgc_BLEZ50SoAWLuPNISvA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Title":"Page Title","Permalink":"a-cool-page.html","PublishedOn":"2024-01-20T22:14:28Z","UpdatedOn":"2024-01-20T22:14:28Z","IsInPageList":false,"Text":"

A Cool Page

\n

It really is cool!

\n","Metadata":[{"Name":"Cool","Value":"true"},{"Name":"Warm","Value":"false"}],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:14:28Z","Text":"Markdown: # A Cool Page\n\nIt really is cool!"}]},{"Id":"KouRjvSmm0Wz6TMD8xf67A","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Title":"Yet Another Page","Permalink":"this-again.html","PublishedOn":"2024-01-20T22:15:08Z","UpdatedOn":"2024-01-20T22:15:08Z","IsInPageList":true,"Text":"

Page 2

\n\n

It's a trip.","Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:15:08Z","Text":"HTML:

Page 2

\n\n

It's a trip."}]}],"Posts":[{"Id":"RCsCU2puYEmkpzotoi8p4g","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Test Post 1","Permalink":"2024/test-post-1.html","PublishedOn":"2024-01-20T22:17:29Z","UpdatedOn":"2024-01-20T22:17:29Z","Text":"

Introduction

\n

Visit my web site or my local page for more information.

\n","CategoryIds":["ScVpyu1e7UiP7bDdge3ZEw"],"Tags":["f#","howdy","intro"],"Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:17:29Z","Text":"Markdown: ## Introduction\n\nVisit [my web site](https://example.com) or [my local page](/a-fine-page.html) for more information."}]},{"Id":"osxMfWGlAkyugUbJ1-xD1g","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Episode 1","Permalink":"2024/episode-1.html","PublishedOn":"2024-01-20T22:24:01Z","UpdatedOn":"2024-01-20T22:24:01Z","Text":"

It's the launch of my new podcast - y'all come listen!","CategoryIds":["S5JflPsJ9EG7gA2LD4m92A"],"Tags":["general","podcast"],"Episode":{"Media":"episode-1.mp3","Length":124302,"Duration":"0:12:22","ImageUrl":"images/ep1-cover.png","Subtitle":"An introduction to this podcast","Explicit":"clean","ChapterFile":"uploads/chapters.json","TranscriptUrl":"uploads/transcript.srt","TranscriptType":"application/srt","TranscriptLang":"en","TranscriptCaptions":true,"SeasonNumber":1,"SeasonDescription":"The First Season","EpisodeNumber":1.0,"EpisodeDescription":"The first episode ever!"},"Metadata":[{"Name":"Density","Value":"Non-existent"},{"Name":"Intensity","Value":"Low"}],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:24:01Z","Text":"HTML:

It's the launch of my new podcast - y'all come listen!"}]},{"Id":"l4_Eh4aFO06SqqJjOymNzA","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Episode 2","Permalink":"2024/episode-2.html","PublishedOn":"2024-01-20T22:31:32Z","UpdatedOn":"2024-01-20T22:31:32Z","Text":"

m i n i m a l","CategoryIds":[],"Tags":["podcast"],"Episode":{"Media":"episode-2.mp3","Length":12873952,"Duration":"1:03:24","SeasonNumber":1,"SeasonDescription":"The First Season","EpisodeNumber":2.0,"EpisodeDescription":"A long update"},"Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:31:32Z","Text":"HTML:

m i n i m a l"}]},{"Id":"QweKbWQiOkqqrjEdgP9wwg","WebLogId":"uSitJEuD3UyzWC9jgOHc8g","AuthorId":"5EM2rimH9kONpmd2zQkiVA","Status":"Published","Title":"Something May Happen","Permalink":"2024/something.html","PublishedOn":"2024-01-20T22:32:59Z","UpdatedOn":"2024-01-20T22:32:59Z","Text":"

Hmm

","CategoryIds":["jw6N69YtTEWVHAO33jHU-w"],"Tags":["f#","ghoti","speculation"],"Metadata":[],"PriorPermalinks":[],"Revisions":[{"AsOf":"2024-01-20T22:32:59Z","Text":"HTML:

Hmm

"}]}],"Uploads":[]} \ No newline at end of file +{ + "WebLog": { + "Id": "uSitJEuD3UyzWC9jgOHc8g", + "Name": "Root WebLog", + "Slug": "root-weblog", + "Subtitle": "This is the main one", + "DefaultPage": "posts", + "PostsPerPage": 9, + "ThemeId": "default", + "UrlBase": "http://localhost:8081", + "TimeZone": "America/Denver", + "Rss": { + "IsFeedEnabled": true, + "FeedName": "feed", + "ItemsInFeed": 7, + "IsCategoryEnabled": true, + "IsTagEnabled": true, + "Copyright": "CC40-NC-BY", + "CustomFeeds": [ + { + "Id": "isPQ6drbDEydxohQzaiYtQ", + "Source": "tag:podcast", + "Path": "podcast-feed", + "Podcast": { + "Title": "Root Podcast", + "ItemsInFeed": 23, + "Summary": "All things that happen in the domain root", + "DisplayedAuthor": "Podcaster Extraordinaire", + "Email": "podcaster@example.com", + "ImageUrl": "images/cover-art.png", + "AppleCategory": "Fiction", + "AppleSubcategory": "Drama", + "Explicit": "no", + "DefaultMediaType": "audio/mpeg", + "MediaBaseUrl": "https://media.example.com/root/", + "PodcastGuid": "10fd7f79-c719-4e1d-9da7-10405dd4fd96", + "FundingUrl": "https://example.com/support-us", + "FundingText": "Support Our Work", + "Medium": "newsletter" + } + } + ] + }, + "AutoHtmx": true, + "Uploads": "Database", + "RedirectRules": [] + }, + "Users": [ + { + "Id": "5EM2rimH9kONpmd2zQkiVA", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Email": "root@example.com", + "FirstName": "Root", + "LastName": "Owner", + "PreferredName": "Admin", + "PasswordHash": "AQAAAAIAAYagAAAAEEnq9J9lKZoMQZaTOJHKIQo44skDdzDigzqS+o6myMop38YuHfm/vNs9b/WpYjsOxg==", + "AccessLevel": "Administrator", + "CreatedOn": "2024-01-20T21:49:03Z", + "LastSeenOn": "2024-01-20T22:25:03Z" + }, + { + "Id": "GPbJaSOwTkKt14ZKYyveKA", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Email": "editor@example.com", + "FirstName": "Ed", + "LastName": "It-Or", + "PreferredName": "Edits", + "PasswordHash": "AQAAAAIAAYagAAAAEA8E3NwJkZO+q35FTmUT0wMNB8IpBOSVACKQcccXpaWfZJMRmZzjPEzd4j/f9h+rEA==", + "AccessLevel": "Editor", + "CreatedOn": "2024-01-20T21:58:42Z" + }, + { + "Id": "iIRNLSeY0EanxRPyqGuwVg", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Email": "author@example.com", + "FirstName": "Author", + "LastName": "Dude", + "PreferredName": "Mister", + "PasswordHash": "AQAAAAIAAYagAAAAEBYNf1sR/pjaX2dZgqlvpH/Tqpz2h/CG3rsk/wH2ReTysjpK/gxSqht7IULWXM7KHQ==", + "Url": "https://example.com/author", + "AccessLevel": "Author", + "CreatedOn": "2024-01-20T21:53:27Z" + } + ], + "Theme": { + "Id": "default", + "Name": "myWebLog Default Theme", + "Version": "2.1.0", + "Templates": [ + { + "Name": "single-post", + "Text": "{%- assign post = model.posts | first -%}\n

{{ post.title }}

\n

\n {% if post.published_on -%}\n Published {{ post.published_on | date: \"dddd, MMMM d, yyyy\" }}\n at {{ post.published_on | date: \"h:mm tt\" | downcase }}\n {%- else -%}\n **DRAFT**\n {% endif %}\n by {{ model.authors | value: post.author_id }}\n {%- if is_editor or is_author and user_id == post.author_id %}\n • Edit Post\n {%- endif %}\n

\n
\n
\n
{{ post.text }}
\n {%- assign cat_count = post.category_ids | size -%}\n {% if cat_count > 0 -%}\n

\n Categorized under\n {% for cat_id in post.category_ids -%}\n {% assign cat = categories | where: \"Id\", cat_id | first %}\n \n \n {{ cat.name }}\n \n \n {% unless forloop.last %} • {% endunless %}\n {%- endfor %}\n

\n {%- endif %}\n {%- assign tag_count = post.tags | size -%}\n {% if tag_count > 0 -%}\n

\n Tagged\n {% for tag in post.tags %}\n \n {{ tag }}\n \n {% unless forloop.last %} • {% endunless %}\n {%- endfor %}\n

\n {%- endif %}\n
\n
\n \n
\n
\n" + }, + { + "Name": "single-page", + "Text": "

{{ page.title }}

\n
{{ page.text }}
\n" + }, + { + "Name": "layout", + "Text": "\n\n \n \n \n {{ page_title | strip_html }}{% if page_title %} « {% endif %}{{ web_log.name | strip_html }}\n {% page_head -%}\n
\n \n
\n
\n {% if messages %}\n
\n {% for msg in messages %}\n \n {% endfor %}\n
\n {% endif %}\n {{ content }} \n
\n
\n
\n
\n myWebLog\n
\n
\n \n\n" + }, + { + "Name": "index", + "Text": "{%- if is_category or is_tag %}\n

{{ page_title }}

\n {%- if subtitle %}

{{ subtitle }}

{% endif -%}\n{% endif %}\n{%- assign post_count = model.posts | size -%}\n{%- if post_count > 0 %}\n
\n {%- for post in model.posts %}\n
\n

\n \n {{ post.title }}\n \n

\n

\n Published on {{ post.published_on | date: \"MMMM d, yyyy\" }}\n at {{ post.published_on | date: \"h:mmtt\" | downcase }}\n by {{ model.authors | value: post.author_id }}\n {{ post.text }}\n {%- assign category_count = post.category_ids | size -%}\n {%- assign tag_count = post.tags | size -%}\n {% if category_count > 0 or tag_count > 0 %}\n

\n

\n {%- if category_count > 0 -%}\n Categorized under:\n {% for cat in post.category_ids -%}\n {%- assign this_cat = categories | where: \"Id\", cat | first -%}\n {{ this_cat.name }}{% unless forloop.last %}, {% endunless %}\n {%- assign cat_names = this_cat.name | concat: cat_names -%}\n {%- endfor -%}\n {%- assign cat_names = \"\" -%}\n
\n {% endif -%}\n {%- if tag_count > 0 %}\n Tagged: {{ post.tags | join: \", \" }}\n {% endif -%}\n

\n {% endif %}\n
\n
\n {% endfor %}\n
\n \n{%- else %}\n

No posts found

\n{%- endif %}\n" + } + ] + }, + "Assets": [ + { + "Id": "default/style.css", + "UpdatedOn": "2023-07-02T20:36:28Z", + "Data": "Lm1lc3NhZ2VzIHsKICBtYXgtd2lkdGg6IDYwcmVtOwogIG1hcmdpbjogYXV0bzsKfQpibG9ja3F1b3RlIHsKICBib3JkZXItbGVmdDogc29saWQgNHB4IGxpZ2h0Z3JheTsKICBwYWRkaW5nLWxlZnQ6IDFyZW07Cn0KLml0ZW0tbWV0YSB7CiAgZm9udC1zaXplOiAxLjFyZW07CiAgZm9udC13ZWlnaHQ6IG5vcm1hbDs7Cn0KLml0ZW0tbWV0YTo6YmVmb3JlIHsKICBjb250ZW50OiAiwrsiOwogIHZlcnRpY2FsLWFsaWduOiB0ZXh0LXRvcDsKfQphOmxpbmssIGE6dmlzaXRlZCB7CiAgdGV4dC1kZWNvcmF0aW9uOiBub25lOwp9CmE6aG92ZXIgewogIHRleHQtZGVjb3JhdGlvbjogdW5kZXJsaW5lOwp9Cg==" + } + ], + "Categories": [ + { + "Id": "S5JflPsJ9EG7gA2LD4m92A", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Name": "Favorites", + "Slug": "favorites", + "Description": "Favorite posts" + }, + { + "Id": "jw6N69YtTEWVHAO33jHU-w", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Name": "Spitball", + "Slug": "spitball", + "Description": "Posts that may or may not work" + }, + { + "Id": "ScVpyu1e7UiP7bDdge3ZEw", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Name": "Moonshot", + "Slug": "moonshot", + "ParentId": "jw6N69YtTEWVHAO33jHU-w" + } + ], + "TagMappings": [ + { + "Id": "Icm027noqE-rPHKZA98vAw", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Tag": "f#", + "UrlValue": "f-sharp" + }, + { + "Id": "GdryXh-S0kGsNBs2RIacGA", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "Tag": "ghoti", + "UrlValue": "fish" + } + ], + "Pages": [ + { + "Id": "hgc_BLEZ50SoAWLuPNISvA", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Title": "Page Title", + "Permalink": "a-cool-page.html", + "PublishedOn": "2024-01-20T22:14:28Z", + "UpdatedOn": "2024-01-20T22:14:28Z", + "IsInPageList": false, + "Text": "

A Cool Page

\n

It really is cool!

\n", + "Metadata": [ + { + "Name": "Cool", + "Value": "true" + }, + { + "Name": "Warm", + "Value": "false" + } + ], + "PriorPermalinks": [ + "a-cool-pg.html" + ], + "Revisions": [ + { + "AsOf": "2024-01-20T22:14:28Z", + "Text": "Markdown: # A Cool Page\n\nIt really is cool!" + } + ] + }, + { + "Id": "KouRjvSmm0Wz6TMD8xf67A", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Title": "Yet Another Page", + "Permalink": "this-again.html", + "PublishedOn": "2024-01-20T22:15:08Z", + "UpdatedOn": "2024-01-20T22:15:08Z", + "IsInPageList": true, + "Text": "

Page 2

\n\n

It's a trip.", + "Metadata": [], + "PriorPermalinks": [], + "Revisions": [ + { + "AsOf": "2024-01-20T22:15:08Z", + "Text": "HTML:

Page 2

\n\n

It's a trip." + } + ] + } + ], + "Posts": [ + { + "Id": "RCsCU2puYEmkpzotoi8p4g", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Status": "Published", + "Title": "Test Post 1", + "Permalink": "2024/test-post-1.html", + "PublishedOn": "2024-01-20T22:17:29Z", + "UpdatedOn": "2024-01-20T22:17:29Z", + "Text": "

Introduction

\n

Visit my web site or my local page for more information.

\n", + "CategoryIds": [ + "ScVpyu1e7UiP7bDdge3ZEw" + ], + "Tags": [ + "f#", + "howdy", + "intro" + ], + "Metadata": [], + "PriorPermalinks": [], + "Revisions": [ + { + "AsOf": "2024-01-20T22:17:29Z", + "Text": "Markdown: ## Introduction\n\nVisit [my web site](https://example.com) or [my local page](/a-fine-page.html) for more information." + } + ] + }, + { + "Id": "osxMfWGlAkyugUbJ1-xD1g", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Status": "Published", + "Title": "Episode 1", + "Permalink": "2024/episode-1.html", + "PublishedOn": "2024-01-20T22:24:01Z", + "UpdatedOn": "2024-01-20T22:24:01Z", + "Text": "

It's the launch of my new podcast - y'all come listen!", + "CategoryIds": [ + "S5JflPsJ9EG7gA2LD4m92A" + ], + "Tags": [ + "general", + "podcast" + ], + "Episode": { + "Media": "episode-1.mp3", + "Length": 124302, + "Duration": "0:12:22", + "ImageUrl": "images/ep1-cover.png", + "Subtitle": "An introduction to this podcast", + "Explicit": "clean", + "ChapterFile": "uploads/chapters.json", + "TranscriptUrl": "uploads/transcript.srt", + "TranscriptType": "application/srt", + "TranscriptLang": "en", + "TranscriptCaptions": true, + "SeasonNumber": 1, + "SeasonDescription": "The First Season", + "EpisodeNumber": 1.0, + "EpisodeDescription": "The first episode ever!" + }, + "Metadata": [ + { + "Name": "Density", + "Value": "Non-existent" + }, + { + "Name": "Intensity", + "Value": "Low" + } + ], + "PriorPermalinks": [], + "Revisions": [ + { + "AsOf": "2024-01-20T22:24:01Z", + "Text": "HTML:

It's the launch of my new podcast - y'all come listen!" + } + ] + }, + { + "Id": "l4_Eh4aFO06SqqJjOymNzA", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Status": "Published", + "Title": "Episode 2", + "Permalink": "2024/episode-2.html", + "PublishedOn": "2024-01-20T22:31:32Z", + "UpdatedOn": "2024-01-20T22:31:32Z", + "Text": "

m i n i m a l", + "CategoryIds": [], + "Tags": [ + "podcast" + ], + "Episode": { + "Media": "episode-2.mp3", + "Length": 12873952, + "Duration": "1:03:24", + "SeasonNumber": 1, + "SeasonDescription": "The First Season", + "EpisodeNumber": 2.0, + "EpisodeDescription": "A long update" + }, + "Metadata": [], + "PriorPermalinks": [], + "Revisions": [ + { + "AsOf": "2024-01-20T22:31:32Z", + "Text": "HTML:

m i n i m a l" + } + ] + }, + { + "Id": "QweKbWQiOkqqrjEdgP9wwg", + "WebLogId": "uSitJEuD3UyzWC9jgOHc8g", + "AuthorId": "5EM2rimH9kONpmd2zQkiVA", + "Status": "Published", + "Title": "Something May Happen", + "Permalink": "2024/something.html", + "PublishedOn": "2024-01-20T22:32:59Z", + "UpdatedOn": "2024-01-20T22:32:59Z", + "Text": "

Hmm

", + "CategoryIds": [ + "jw6N69YtTEWVHAO33jHU-w" + ], + "Tags": [ + "f#", + "ghoti", + "speculation" + ], + "Metadata": [], + "PriorPermalinks": [], + "Revisions": [ + { + "AsOf": "2024-01-20T22:32:59Z", + "Text": "HTML:

Hmm

" + } + ] + } + ], + "Uploads": [] +} \ No newline at end of file