WIP on SQLite/JSON data
This commit is contained in:
		
							parent
							
								
									715e545ed5
								
							
						
					
					
						commit
						ec2d43acde
					
				| @ -2,6 +2,63 @@ | |||||||
| [<AutoOpen>] | [<AutoOpen>] | ||||||
| module MyWebLog.Data.SQLite.Helpers | module MyWebLog.Data.SQLite.Helpers | ||||||
| 
 | 
 | ||||||
|  | /// The table names used in the SQLite implementation | ||||||
|  | [<RequireQualifiedAccess>] | ||||||
|  | module Table = | ||||||
|  |      | ||||||
|  |     /// Categories | ||||||
|  |     [<Literal>] | ||||||
|  |     let Category = "category" | ||||||
|  |      | ||||||
|  |     /// Database Version | ||||||
|  |     [<Literal>] | ||||||
|  |     let DbVersion = "db_version" | ||||||
|  |      | ||||||
|  |     /// Pages | ||||||
|  |     [<Literal>] | ||||||
|  |     let Page = "page" | ||||||
|  |      | ||||||
|  |     /// Page Revisions | ||||||
|  |     [<Literal>] | ||||||
|  |     let PageRevision = "page_revision" | ||||||
|  |      | ||||||
|  |     /// Posts | ||||||
|  |     [<Literal>] | ||||||
|  |     let Post = "post" | ||||||
|  |      | ||||||
|  |     /// Post Comments | ||||||
|  |     [<Literal>] | ||||||
|  |     let PostComment = "post_comment" | ||||||
|  |      | ||||||
|  |     /// Post Revisions | ||||||
|  |     [<Literal>] | ||||||
|  |     let PostRevision = "post_revision" | ||||||
|  |      | ||||||
|  |     /// Tag/URL Mappings | ||||||
|  |     [<Literal>] | ||||||
|  |     let TagMap = "tag_map" | ||||||
|  |      | ||||||
|  |     /// Themes | ||||||
|  |     [<Literal>] | ||||||
|  |     let Theme = "theme" | ||||||
|  |      | ||||||
|  |     /// Theme Assets | ||||||
|  |     [<Literal>] | ||||||
|  |     let ThemeAsset = "theme_asset" | ||||||
|  |      | ||||||
|  |     /// Uploads | ||||||
|  |     [<Literal>] | ||||||
|  |     let Upload = "upload" | ||||||
|  |      | ||||||
|  |     /// Web Logs | ||||||
|  |     [<Literal>] | ||||||
|  |     let WebLog = "web_log" | ||||||
|  |      | ||||||
|  |     /// Users | ||||||
|  |     [<Literal>] | ||||||
|  |     let WebLogUser = "web_log_user" | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| open System | open System | ||||||
| open Microsoft.Data.Sqlite | open Microsoft.Data.Sqlite | ||||||
| open MyWebLog | open MyWebLog | ||||||
|  | |||||||
| @ -27,17 +27,9 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS | |||||||
|             not (List.contains table tables) |             not (List.contains table tables) | ||||||
|         seq { |         seq { | ||||||
|             // Theme tables |             // Theme tables | ||||||
|             if needsTable "theme" then |             if needsTable Table.Theme then | ||||||
|                 "CREATE TABLE theme ( |                 $"CREATE TABLE {Table.Theme} (data TEXT NOT NULL); | ||||||
|                     id       TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.Theme}_key ON {Table.Theme} (data ->> 'Id')"; | ||||||
|                     name     TEXT NOT NULL, |  | ||||||
|                     version  TEXT NOT NULL)" |  | ||||||
|             if needsTable "theme_template" then |  | ||||||
|                 "CREATE TABLE theme_template ( |  | ||||||
|                     theme_id  TEXT NOT NULL REFERENCES theme (id), |  | ||||||
|                     name      TEXT NOT NULL, |  | ||||||
|                     template  TEXT NOT NULL, |  | ||||||
|                     PRIMARY KEY (theme_id, name))" |  | ||||||
|             if needsTable "theme_asset" then |             if needsTable "theme_asset" then | ||||||
|                 "CREATE TABLE theme_asset ( |                 "CREATE TABLE theme_asset ( | ||||||
|                     theme_id    TEXT NOT NULL REFERENCES theme (id), |                     theme_id    TEXT NOT NULL REFERENCES theme (id), | ||||||
| @ -46,139 +38,54 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS | |||||||
|                     data        BLOB NOT NULL, |                     data        BLOB NOT NULL, | ||||||
|                     PRIMARY KEY (theme_id, path))" |                     PRIMARY KEY (theme_id, path))" | ||||||
|              |              | ||||||
|             // Web log tables |             // Web log table | ||||||
|             if needsTable "web_log" then |             if needsTable Table.WebLog then | ||||||
|                 "CREATE TABLE web_log ( |                 $"CREATE TABLE {Table.WebLog} (data TEXT NOT NULL); | ||||||
|                     id                   TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.WebLog}_key ON {Table.WebLog} (data ->> 'Id')" | ||||||
|                     name                 TEXT NOT NULL, |  | ||||||
|                     slug                 TEXT NOT NULL, |  | ||||||
|                     subtitle             TEXT, |  | ||||||
|                     default_page         TEXT NOT NULL, |  | ||||||
|                     posts_per_page       INTEGER NOT NULL, |  | ||||||
|                     theme_id             TEXT NOT NULL REFERENCES theme (id), |  | ||||||
|                     url_base             TEXT NOT NULL, |  | ||||||
|                     time_zone            TEXT NOT NULL, |  | ||||||
|                     auto_htmx            INTEGER NOT NULL DEFAULT 0, |  | ||||||
|                     uploads              TEXT NOT NULL, |  | ||||||
|                     is_feed_enabled      INTEGER NOT NULL DEFAULT 0, |  | ||||||
|                     feed_name            TEXT NOT NULL, |  | ||||||
|                     items_in_feed        INTEGER, |  | ||||||
|                     is_category_enabled  INTEGER NOT NULL DEFAULT 0, |  | ||||||
|                     is_tag_enabled       INTEGER NOT NULL DEFAULT 0, |  | ||||||
|                     copyright            TEXT, |  | ||||||
|                     redirect_rules       TEXT NOT NULL DEFAULT '[]'); |  | ||||||
|                 CREATE INDEX web_log_theme_idx ON web_log (theme_id)" |  | ||||||
|             if needsTable "web_log_feed" then |  | ||||||
|                 "CREATE TABLE web_log_feed ( |  | ||||||
|                     id          TEXT PRIMARY KEY, |  | ||||||
|                     web_log_id  TEXT NOT NULL REFERENCES web_log (id), |  | ||||||
|                     source      TEXT NOT NULL, |  | ||||||
|                     path        TEXT NOT NULL, |  | ||||||
|                     podcast     TEXT); |  | ||||||
|                 CREATE INDEX web_log_feed_web_log_idx ON web_log_feed (web_log_id)" |  | ||||||
|              |              | ||||||
|             // Category table |             // Category table | ||||||
|             if needsTable "category" then |             if needsTable Table.Category then | ||||||
|                 "CREATE TABLE category ( |                 $"CREATE TABLE {Table.Category} (data TEXT NOT NULL); | ||||||
|                     id           TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.Category}_key ON {Table.Category} (data -> 'Id'); | ||||||
|                     web_log_id   TEXT NOT NULL REFERENCES web_log (id), |                   CREATE INDEX idx_{Table.Category}_web_log ON {Table.Category} (data ->> 'WebLogId')" | ||||||
|                     name         TEXT NOT NULL, |  | ||||||
|                     slug         TEXT NOT NULL, |  | ||||||
|                     description  TEXT, |  | ||||||
|                     parent_id    TEXT); |  | ||||||
|                 CREATE INDEX category_web_log_idx ON category (web_log_id)" |  | ||||||
|              |              | ||||||
|             // Web log user table |             // Web log user table | ||||||
|             if needsTable "web_log_user" then |             if needsTable Table.WebLogUser then | ||||||
|                 "CREATE TABLE web_log_user ( |                 $"CREATE TABLE web_log_user (data TEXT NOT NULL); | ||||||
|                     id              TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.WebLogUser}_key ON {Table.WebLogUser} (data ->> 'Id'); | ||||||
|                     web_log_id      TEXT NOT NULL REFERENCES web_log (id), |                   CREATE INDEX idx_{Table.WebLogUser}_email ON {Table.WebLogUser} (data ->> 'WebLogId', data ->> 'Email')" | ||||||
|                     email           TEXT NOT NULL, |  | ||||||
|                     first_name      TEXT NOT NULL, |  | ||||||
|                     last_name       TEXT NOT NULL, |  | ||||||
|                     preferred_name  TEXT NOT NULL, |  | ||||||
|                     password_hash   TEXT NOT NULL, |  | ||||||
|                     url             TEXT, |  | ||||||
|                     access_level    TEXT NOT NULL, |  | ||||||
|                     created_on      TEXT NOT NULL, |  | ||||||
|                     last_seen_on    TEXT); |  | ||||||
|                 CREATE INDEX web_log_user_web_log_idx ON web_log_user (web_log_id); |  | ||||||
|                 CREATE INDEX web_log_user_email_idx   ON web_log_user (web_log_id, email)" |  | ||||||
|              |              | ||||||
|             // Page tables |             // Page tables | ||||||
|             if needsTable "page" then |             if needsTable Table.Page then | ||||||
|                 "CREATE TABLE page ( |                 $"CREATE TABLE {Table.Page} (data TEXT NOT NULL); | ||||||
|                     id               TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.Page}_key ON {Table.Page} (data ->> 'Id'); | ||||||
|                     web_log_id       TEXT NOT NULL REFERENCES web_log (id), |                   CREATE INDEX idx_{Table.Page}_author ON {Table.Page} (data ->> 'AuthorId'); | ||||||
|                     author_id        TEXT NOT NULL REFERENCES web_log_user (id), |                   CREATE INDEX idx_{Table.Page}_permalink ON {Table.Page} (data ->> 'WebLogId', data ->> 'Permalink')" | ||||||
|                     title            TEXT NOT NULL, |             if needsTable Table.PageRevision then | ||||||
|                     permalink        TEXT NOT NULL, |  | ||||||
|                     published_on     TEXT NOT NULL, |  | ||||||
|                     updated_on       TEXT NOT NULL, |  | ||||||
|                     is_in_page_list  INTEGER NOT NULL DEFAULT 0, |  | ||||||
|                     template         TEXT, |  | ||||||
|                     page_text        TEXT NOT NULL, |  | ||||||
|                     meta_items       TEXT); |  | ||||||
|                 CREATE INDEX page_web_log_idx   ON page (web_log_id); |  | ||||||
|                 CREATE INDEX page_author_idx    ON page (author_id); |  | ||||||
|                 CREATE INDEX page_permalink_idx ON page (web_log_id, permalink)" |  | ||||||
|             if needsTable "page_permalink" then |  | ||||||
|                 "CREATE TABLE page_permalink ( |  | ||||||
|                     page_id    TEXT NOT NULL REFERENCES page (id), |  | ||||||
|                     permalink  TEXT NOT NULL, |  | ||||||
|                     PRIMARY KEY (page_id, permalink))" |  | ||||||
|             if needsTable "page_revision" then |  | ||||||
|                 "CREATE TABLE page_revision ( |                 "CREATE TABLE page_revision ( | ||||||
|                     page_id        TEXT NOT NULL REFERENCES page (id), |                     page_id        TEXT NOT NULL, | ||||||
|                     as_of          TEXT NOT NULL, |                     as_of          TEXT NOT NULL, | ||||||
|                     revision_text  TEXT NOT NULL, |                     revision_text  TEXT NOT NULL, | ||||||
|                     PRIMARY KEY (page_id, as_of))" |                     PRIMARY KEY (page_id, as_of))" | ||||||
|              |              | ||||||
|             // Post tables |             // Post tables | ||||||
|             if needsTable "post" then |             if needsTable Table.Post then | ||||||
|                 "CREATE TABLE post ( |                 $"CREATE TABLE {Table.Post} (data TEXT NOT NULL); | ||||||
|                     id            TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.Post}_key ON {Table.Post} (data ->> 'Id'); | ||||||
|                     web_log_id    TEXT NOT NULL REFERENCES web_log (id), |                   CREATE INDEX idx_{Table.Post}_author ON {Table.Post} (data ->> 'AuthorId'); | ||||||
|                     author_id     TEXT NOT NULL REFERENCES web_log_user (id), |                   CREATE INDEX idx_{Table.Post}_status ON {Table.Post} (data ->> 'WebLogId', data ->> 'Status', data ->> 'UpdatedOn'); | ||||||
|                     status        TEXT NOT NULL, |                   CREATE INDEX idx_{Table.Post}_permalink ON {Table.Post} (data ->> 'WebLogId', data ->> 'Permalink')" | ||||||
|                     title         TEXT NOT NULL, |                   // TODO: index categories by post? | ||||||
|                     permalink     TEXT NOT NULL, |             if needsTable Table.PostRevision then | ||||||
|                     published_on  TEXT, |                 $"CREATE TABLE {Table.PostRevision} ( | ||||||
|                     updated_on    TEXT NOT NULL, |                     post_id        TEXT NOT NULL, | ||||||
|                     template      TEXT, |  | ||||||
|                     post_text     TEXT NOT NULL, |  | ||||||
|                     meta_items    TEXT, |  | ||||||
|                     episode       TEXT); |  | ||||||
|                 CREATE INDEX post_web_log_idx   ON post (web_log_id); |  | ||||||
|                 CREATE INDEX post_author_idx    ON post (author_id); |  | ||||||
|                 CREATE INDEX post_status_idx    ON post (web_log_id, status, updated_on); |  | ||||||
|                 CREATE INDEX post_permalink_idx ON post (web_log_id, permalink)" |  | ||||||
|             if needsTable "post_category" then |  | ||||||
|                 "CREATE TABLE post_category ( |  | ||||||
|                     post_id      TEXT NOT NULL REFERENCES post (id), |  | ||||||
|                     category_id  TEXT NOT NULL REFERENCES category (id), |  | ||||||
|                     PRIMARY KEY (post_id, category_id)); |  | ||||||
|                 CREATE INDEX post_category_category_idx ON post_category (category_id)" |  | ||||||
|             if needsTable "post_tag" then |  | ||||||
|                 "CREATE TABLE post_tag ( |  | ||||||
|                     post_id  TEXT NOT NULL REFERENCES post (id), |  | ||||||
|                     tag      TEXT NOT NULL, |  | ||||||
|                     PRIMARY KEY (post_id, tag))" |  | ||||||
|             if needsTable "post_permalink" then |  | ||||||
|                 "CREATE TABLE post_permalink ( |  | ||||||
|                     post_id    TEXT NOT NULL REFERENCES post (id), |  | ||||||
|                     permalink  TEXT NOT NULL, |  | ||||||
|                     PRIMARY KEY (post_id, permalink))" |  | ||||||
|             if needsTable "post_revision" then |  | ||||||
|                 "CREATE TABLE post_revision ( |  | ||||||
|                     post_id        TEXT NOT NULL REFERENCES post (id), |  | ||||||
|                     as_of          TEXT NOT NULL, |                     as_of          TEXT NOT NULL, | ||||||
|                     revision_text  TEXT NOT NULL, |                     revision_text  TEXT NOT NULL, | ||||||
|                     PRIMARY KEY (post_id, as_of))" |                     PRIMARY KEY (post_id, as_of))" | ||||||
|             if needsTable "post_comment" then |             if needsTable Table.PostComment then | ||||||
|                 "CREATE TABLE post_comment ( |                 $"CREATE TABLE {Table.PostComment} ( | ||||||
|                     id              TEXT PRIMARY KEY, |                     id              TEXT PRIMARY KEY, | ||||||
|                     post_id         TEXT NOT NULL REFERENCES post(id), |                     post_id         TEXT NOT NULL, | ||||||
|                     in_reply_to_id  TEXT, |                     in_reply_to_id  TEXT, | ||||||
|                     name            TEXT NOT NULL, |                     name            TEXT NOT NULL, | ||||||
|                     email           TEXT NOT NULL, |                     email           TEXT NOT NULL, | ||||||
| @ -186,32 +93,28 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS | |||||||
|                     status          TEXT NOT NULL, |                     status          TEXT NOT NULL, | ||||||
|                     posted_on       TEXT NOT NULL, |                     posted_on       TEXT NOT NULL, | ||||||
|                     comment_text    TEXT NOT NULL); |                     comment_text    TEXT NOT NULL); | ||||||
|                 CREATE INDEX post_comment_post_idx ON post_comment (post_id)" |                   CREATE INDEX idx_{Table.PostComment}_post ON {Table.PostComment} (post_id)" | ||||||
|              |              | ||||||
|             // Tag map table |             // Tag map table | ||||||
|             if needsTable "tag_map" then |             if needsTable Table.TagMap then | ||||||
|                 "CREATE TABLE tag_map ( |                 $"CREATE TABLE {Table.TagMap} (data TEXT NOT NULL); | ||||||
|                     id          TEXT PRIMARY KEY, |                   CREATE UNIQUE INDEX idx_{Table.TagMap}_key ON {Table.TagMap} (data ->> 'Id'); | ||||||
|                     web_log_id  TEXT NOT NULL REFERENCES web_log (id), |                   CREATE INDEX idx_{Table.TagMap}_tag ON {Table.TagMap} (data ->> 'WebLogId', data ->> 'UrlValue')"; | ||||||
|                     tag         TEXT NOT NULL, |  | ||||||
|                     url_value   TEXT NOT NULL); |  | ||||||
|                 CREATE INDEX tag_map_web_log_idx ON tag_map (web_log_id)" |  | ||||||
|              |              | ||||||
|             // Uploaded file table |             // Uploaded file table | ||||||
|             if needsTable "upload" then |             if needsTable Table.Upload then | ||||||
|                 "CREATE TABLE upload ( |                 $"CREATE TABLE {Table.Upload} ( | ||||||
|                     id          TEXT PRIMARY KEY, |                     id          TEXT PRIMARY KEY, | ||||||
|                     web_log_id  TEXT NOT NULL REFERENCES web_log (id), |                     web_log_id  TEXT NOT NULL, | ||||||
|                     path        TEXT NOT NULL, |                     path        TEXT NOT NULL, | ||||||
|                     updated_on  TEXT NOT NULL, |                     updated_on  TEXT NOT NULL, | ||||||
|                     data        BLOB NOT NULL); |                     data        BLOB NOT NULL); | ||||||
|                 CREATE INDEX upload_web_log_idx ON upload (web_log_id); |                   CREATE INDEX idx_{Table.Upload}_path ON {Table.Upload} (web_log_id, path)" | ||||||
|                 CREATE INDEX upload_path_idx    ON upload (web_log_id, path)" |  | ||||||
|              |              | ||||||
|             // Database version table |             // Database version table | ||||||
|             if needsTable "db_version" then |             if needsTable Table.DbVersion then | ||||||
|                 "CREATE TABLE db_version (id TEXT PRIMARY KEY); |                 $"CREATE TABLE {Table.DbVersion} (id TEXT PRIMARY KEY); | ||||||
|                  INSERT INTO db_version VALUES ('v2')" |                   INSERT INTO {Table.DbVersion} VALUES ('v2.1')" | ||||||
|         } |         } | ||||||
|         |> Seq.map (fun sql -> |         |> Seq.map (fun sql -> | ||||||
|             log.LogInformation $"Creating {(sql.Split ' ')[2]} table..." |             log.LogInformation $"Creating {(sql.Split ' ')[2]} table..." | ||||||
| @ -224,7 +127,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS | |||||||
|     /// Set the database version to the specified version |     /// Set the database version to the specified version | ||||||
|     let setDbVersion version = backgroundTask { |     let setDbVersion version = backgroundTask { | ||||||
|         use cmd = conn.CreateCommand () |         use cmd = conn.CreateCommand () | ||||||
|         cmd.CommandText <- $"DELETE FROM db_version; INSERT INTO db_version VALUES ('%s{version}')" |         cmd.CommandText <- $"DELETE FROM {Table.DbVersion}; INSERT INTO {Table.DbVersion} VALUES ('%s{version}')" | ||||||
|         do! write cmd |         do! write cmd | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @ -600,7 +503,7 @@ type SQLiteData (conn : SqliteConnection, log : ILogger<SQLiteData>, ser : JsonS | |||||||
|             do! ensureTables () |             do! ensureTables () | ||||||
|              |              | ||||||
|             use cmd = conn.CreateCommand () |             use cmd = conn.CreateCommand () | ||||||
|             cmd.CommandText <- "SELECT id FROM db_version" |             cmd.CommandText <- $"SELECT id FROM {Table.DbVersion}" | ||||||
|             use! rdr = cmd.ExecuteReaderAsync () |             use! rdr = cmd.ExecuteReaderAsync () | ||||||
|             do! migrate (if rdr.Read () then Some (Map.getString "id" rdr) else None) |             do! migrate (if rdr.Read () then Some (Map.getString "id" rdr) else None) | ||||||
|         } |         } | ||||||
|  | |||||||
| @ -6,8 +6,8 @@ open NodaTime | |||||||
| 
 | 
 | ||||||
| /// A category under which a post may be identified | /// A category under which a post may be identified | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Category = | type Category = { | ||||||
|     {   /// The ID of the category |     /// The ID of the category | ||||||
|     Id : CategoryId |     Id : CategoryId | ||||||
| 
 | 
 | ||||||
|     /// The ID of the web log to which the category belongs |     /// The ID of the web log to which the category belongs | ||||||
| @ -30,8 +30,8 @@ type Category = | |||||||
| module Category = | module Category = | ||||||
|      |      | ||||||
|     /// An empty category |     /// An empty category | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id          = CategoryId.empty |         Id          = CategoryId.empty | ||||||
|         WebLogId    = WebLogId.empty |         WebLogId    = WebLogId.empty | ||||||
|         Name        = "" |         Name        = "" | ||||||
|         Slug        = "" |         Slug        = "" | ||||||
| @ -42,8 +42,8 @@ module Category = | |||||||
| 
 | 
 | ||||||
| /// A comment on a post | /// A comment on a post | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Comment = | type Comment = { | ||||||
|     {   /// The ID of the comment |     /// The ID of the comment | ||||||
|     Id : CommentId |     Id : CommentId | ||||||
| 
 | 
 | ||||||
|     /// The ID of the post to which this comment applies |     /// The ID of the post to which this comment applies | ||||||
| @ -75,8 +75,8 @@ type Comment = | |||||||
| module Comment = | module Comment = | ||||||
|      |      | ||||||
|     /// An empty comment |     /// An empty comment | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id          = CommentId.empty |         Id          = CommentId.empty | ||||||
|         PostId      = PostId.empty |         PostId      = PostId.empty | ||||||
|         InReplyToId = None |         InReplyToId = None | ||||||
|         Name        = "" |         Name        = "" | ||||||
| @ -90,8 +90,8 @@ module Comment = | |||||||
| 
 | 
 | ||||||
| /// A page (text not associated with a date/time) | /// A page (text not associated with a date/time) | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Page = | type Page = { | ||||||
|     {   /// The ID of this page |     /// The ID of this page | ||||||
|     Id : PageId |     Id : PageId | ||||||
| 
 | 
 | ||||||
|     /// The ID of the web log to which this page belongs |     /// The ID of the web log to which this page belongs | ||||||
| @ -135,8 +135,8 @@ type Page = | |||||||
| module Page = | module Page = | ||||||
|      |      | ||||||
|     /// An empty page |     /// An empty page | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id              = PageId.empty |         Id              = PageId.empty | ||||||
|         WebLogId        = WebLogId.empty |         WebLogId        = WebLogId.empty | ||||||
|         AuthorId        = WebLogUserId.empty |         AuthorId        = WebLogUserId.empty | ||||||
|         Title           = "" |         Title           = "" | ||||||
| @ -154,8 +154,8 @@ module Page = | |||||||
| 
 | 
 | ||||||
| /// A web log post | /// A web log post | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Post = | type Post = { | ||||||
|     {   /// The ID of this post |     /// The ID of this post | ||||||
|     Id : PostId |     Id : PostId | ||||||
| 
 | 
 | ||||||
|     /// The ID of the web log to which this post belongs |     /// The ID of the web log to which this post belongs | ||||||
| @ -208,8 +208,8 @@ type Post = | |||||||
| module Post = | module Post = | ||||||
|      |      | ||||||
|     /// An empty post |     /// An empty post | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id              = PostId.empty |         Id              = PostId.empty | ||||||
|         WebLogId        = WebLogId.empty |         WebLogId        = WebLogId.empty | ||||||
|         AuthorId        = WebLogUserId.empty |         AuthorId        = WebLogUserId.empty | ||||||
|         Status          = Draft |         Status          = Draft | ||||||
| @ -229,8 +229,8 @@ module Post = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1") | /// A mapping between a tag and its URL value, used to translate restricted characters (ex. "#1" -> "number-1") | ||||||
| type TagMap = | type TagMap = { | ||||||
|     {   /// The ID of this tag mapping |     /// The ID of this tag mapping | ||||||
|     Id : TagMapId |     Id : TagMapId | ||||||
|      |      | ||||||
|     /// The ID of the web log to which this tag mapping belongs |     /// The ID of the web log to which this tag mapping belongs | ||||||
| @ -247,8 +247,8 @@ type TagMap = | |||||||
| module TagMap = | module TagMap = | ||||||
|      |      | ||||||
|     /// An empty tag mapping |     /// An empty tag mapping | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id       = TagMapId.empty |         Id       = TagMapId.empty | ||||||
|         WebLogId = WebLogId.empty |         WebLogId = WebLogId.empty | ||||||
|         Tag      = "" |         Tag      = "" | ||||||
|         UrlValue = "" |         UrlValue = "" | ||||||
| @ -256,8 +256,8 @@ module TagMap = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A theme | /// A theme | ||||||
| type Theme = | type Theme = { | ||||||
|     {   /// The ID / path of the theme |     /// The ID / path of the theme | ||||||
|     Id : ThemeId |     Id : ThemeId | ||||||
|      |      | ||||||
|     /// A long name of the theme |     /// A long name of the theme | ||||||
| @ -274,8 +274,8 @@ type Theme = | |||||||
| module Theme = | module Theme = | ||||||
|      |      | ||||||
|     /// An empty theme |     /// An empty theme | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id        = ThemeId "" |         Id        = ThemeId "" | ||||||
|         Name      = "" |         Name      = "" | ||||||
|         Version   = "" |         Version   = "" | ||||||
|         Templates = [] |         Templates = [] | ||||||
| @ -283,8 +283,7 @@ module Theme = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path]) | /// A theme asset (a file served as part of a theme, at /themes/[theme]/[asset-path]) | ||||||
| type ThemeAsset = | type ThemeAsset = { | ||||||
|     { |  | ||||||
|     /// The ID of the asset (consists of theme and path) |     /// The ID of the asset (consists of theme and path) | ||||||
|     Id : ThemeAssetId |     Id : ThemeAssetId | ||||||
|      |      | ||||||
| @ -299,16 +298,16 @@ type ThemeAsset = | |||||||
| module ThemeAsset = | module ThemeAsset = | ||||||
|      |      | ||||||
|     /// An empty theme asset |     /// An empty theme asset | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id        = ThemeAssetId (ThemeId "", "") |         Id        = ThemeAssetId (ThemeId "", "") | ||||||
|         UpdatedOn = Noda.epoch |         UpdatedOn = Noda.epoch | ||||||
|         Data      = [||] |         Data      = [||] | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An uploaded file | /// An uploaded file | ||||||
| type Upload = | type Upload = { | ||||||
|     {   /// The ID of the upload |     /// The ID of the upload | ||||||
|     Id : UploadId |     Id : UploadId | ||||||
|      |      | ||||||
|     /// The ID of the web log to which this upload belongs |     /// The ID of the web log to which this upload belongs | ||||||
| @ -328,8 +327,8 @@ type Upload = | |||||||
| module Upload = | module Upload = | ||||||
|      |      | ||||||
|     /// An empty upload |     /// An empty upload | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id        = UploadId.empty |         Id        = UploadId.empty | ||||||
|         WebLogId  = WebLogId.empty |         WebLogId  = WebLogId.empty | ||||||
|         Path      = Permalink.empty |         Path      = Permalink.empty | ||||||
|         UpdatedOn = Noda.epoch |         UpdatedOn = Noda.epoch | ||||||
| @ -339,8 +338,8 @@ module Upload = | |||||||
| 
 | 
 | ||||||
| /// A web log | /// A web log | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type WebLog = | type WebLog = { | ||||||
|     {   /// The ID of the web log |     /// The ID of the web log | ||||||
|     Id : WebLogId |     Id : WebLogId | ||||||
| 
 | 
 | ||||||
|     /// The name of the web log |     /// The name of the web log | ||||||
| @ -384,8 +383,8 @@ type WebLog = | |||||||
| module WebLog = | module WebLog = | ||||||
|      |      | ||||||
|     /// An empty web log |     /// An empty web log | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id            = WebLogId.empty |         Id            = WebLogId.empty | ||||||
|         Name          = "" |         Name          = "" | ||||||
|         Slug          = "" |         Slug          = "" | ||||||
|         Subtitle      = None |         Subtitle      = None | ||||||
| @ -424,8 +423,8 @@ module WebLog = | |||||||
| 
 | 
 | ||||||
| /// A user of the web log | /// A user of the web log | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type WebLogUser = | type WebLogUser = { | ||||||
|     {   /// The ID of the user |     /// The ID of the user | ||||||
|     Id : WebLogUserId |     Id : WebLogUserId | ||||||
| 
 | 
 | ||||||
|     /// The ID of the web log to which this user belongs |     /// The ID of the web log to which this user belongs | ||||||
| @ -463,8 +462,8 @@ type WebLogUser = | |||||||
| module WebLogUser = | module WebLogUser = | ||||||
|      |      | ||||||
|     /// An empty web log user |     /// An empty web log user | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id            = WebLogUserId.empty |         Id            = WebLogUserId.empty | ||||||
|         WebLogId      = WebLogId.empty |         WebLogId      = WebLogId.empty | ||||||
|         Email         = "" |         Email         = "" | ||||||
|         FirstName     = "" |         FirstName     = "" | ||||||
|  | |||||||
| @ -10,7 +10,7 @@ module private Helpers = | |||||||
|     /// Create a new ID (short GUID) |     /// Create a new ID (short GUID) | ||||||
|     // https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID |     // https://www.madskristensen.net/blog/A-shorter-and-URL-friendly-GUID | ||||||
|     let newId () = |     let newId () = | ||||||
|         Convert.ToBase64String(Guid.NewGuid().ToByteArray ()).Replace('/', '_').Replace('+', '-').Substring (0, 22) |         Convert.ToBase64String(Guid.NewGuid().ToByteArray ()).Replace('/', '_').Replace('+', '-')[..22] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Functions to support NodaTime manipulation | /// Functions to support NodaTime manipulation | ||||||
| @ -22,18 +22,17 @@ module Noda = | |||||||
|     /// The Unix epoch |     /// The Unix epoch | ||||||
|     let epoch = Instant.FromUnixTimeSeconds 0L |     let epoch = Instant.FromUnixTimeSeconds 0L | ||||||
|          |          | ||||||
|          |  | ||||||
|     /// Truncate an instant to remove fractional seconds |     /// Truncate an instant to remove fractional seconds | ||||||
|     let toSecondsPrecision (value : Instant) = |     let toSecondsPrecision (value : Instant) = | ||||||
|         Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds()) |         Instant.FromUnixTimeSeconds(value.ToUnixTimeSeconds()) | ||||||
|      |      | ||||||
|     /// The current Instant, with fractional seconds truncated |     /// The current Instant, with fractional seconds truncated | ||||||
|     let now () = |     let now = | ||||||
|         toSecondsPrecision (clock.GetCurrentInstant ()) |         clock.GetCurrentInstant >> toSecondsPrecision | ||||||
|      |      | ||||||
|     /// Convert a date/time to an Instant with whole seconds |     /// Convert a date/time to an Instant with whole seconds | ||||||
|     let fromDateTime (dt : DateTime) = |     let fromDateTime (dt : DateTime) = | ||||||
|         toSecondsPrecision (Instant.FromDateTimeUtc (DateTime (dt.Ticks, DateTimeKind.Utc))) |         Instant.FromDateTimeUtc(DateTime(dt.Ticks, DateTimeKind.Utc)) |> toSecondsPrecision | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A user's access level | /// A user's access level | ||||||
| @ -94,7 +93,7 @@ module CategoryId = | |||||||
|     let toString = function CategoryId ci -> ci |     let toString = function CategoryId ci -> ci | ||||||
|      |      | ||||||
|     /// Create a new category ID |     /// Create a new category ID | ||||||
|     let create () = CategoryId (newId ()) |     let create = newId >> CategoryId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An identifier for a comment | /// An identifier for a comment | ||||||
| @ -110,7 +109,7 @@ module CommentId = | |||||||
|     let toString = function CommentId ci -> ci |     let toString = function CommentId ci -> ci | ||||||
|      |      | ||||||
|     /// Create a new comment ID |     /// Create a new comment ID | ||||||
|     let create () = CommentId (newId ()) |     let create = newId >> CommentId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Statuses for post comments | /// Statuses for post comments | ||||||
| @ -134,7 +133,7 @@ module CommentStatus = | |||||||
|         | "Approved" -> Approved |         | "Approved" -> Approved | ||||||
|         | "Pending"  -> Pending |         | "Pending"  -> Pending | ||||||
|         | "Spam"     -> Spam |         | "Spam"     -> Spam | ||||||
|         | it         -> invalidOp $"{it} is not a valid post status" |         | it         -> invalidArg "status" $"{it} is not a valid comment status" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Valid values for the iTunes explicit rating | /// Valid values for the iTunes explicit rating | ||||||
| @ -158,12 +157,12 @@ module ExplicitRating = | |||||||
|         | "yes"   -> Yes |         | "yes"   -> Yes | ||||||
|         | "no"    -> No |         | "no"    -> No | ||||||
|         | "clean" -> Clean |         | "clean" -> Clean | ||||||
|         | x       -> raise (invalidArg "rating" $"{x} is not a valid explicit rating") |         | x       -> invalidArg "rating" $"{x} is not a valid explicit rating" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A location (specified by Podcast Index) | /// A location (specified by Podcast Index) | ||||||
| type Location = | type Location = { | ||||||
|     {   /// The name of the location (free-form text) |     /// The name of the location (free-form text) | ||||||
|     Name : string |     Name : string | ||||||
| 
 | 
 | ||||||
|     /// A geographic coordinate string (RFC 5870) |     /// A geographic coordinate string (RFC 5870) | ||||||
| @ -175,8 +174,8 @@ type Location = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A chapter in a podcast episode | /// A chapter in a podcast episode | ||||||
| type Chapter = | type Chapter = { | ||||||
|     {   /// The start time for the chapter |     /// The start time for the chapter | ||||||
|     StartTime : Duration |     StartTime : Duration | ||||||
| 
 | 
 | ||||||
|     /// The title for this chapter |     /// The title for this chapter | ||||||
| @ -199,8 +198,8 @@ type Chapter = | |||||||
| open NodaTime.Text | open NodaTime.Text | ||||||
| 
 | 
 | ||||||
| /// A podcast episode | /// A podcast episode | ||||||
| type Episode = | type Episode = { | ||||||
|     {   /// The URL to the media file for the episode (may be permalink) |     /// The URL to the media file for the episode (may be permalink) | ||||||
|     Media : string |     Media : string | ||||||
|      |      | ||||||
|     /// The length of the media file, in bytes |     /// The length of the media file, in bytes | ||||||
| @ -259,8 +258,8 @@ type Episode = | |||||||
| module Episode = | module Episode = | ||||||
|      |      | ||||||
|     /// An empty episode |     /// An empty episode | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Media              = "" |         Media              = "" | ||||||
|         Length             = 0L |         Length             = 0L | ||||||
|         Duration           = None |         Duration           = None | ||||||
|         MediaType          = None |         MediaType          = None | ||||||
| @ -316,15 +315,15 @@ module MarkupText = | |||||||
|     /// Parse a string into a MarkupText instance |     /// Parse a string into a MarkupText instance | ||||||
|     let parse (it : string) = |     let parse (it : string) = | ||||||
|         match it with |         match it with | ||||||
|         | text when text.StartsWith "Markdown: " -> Markdown (text.Substring 10) |         | text when text.StartsWith "Markdown: " -> Markdown text[10..] | ||||||
|         | text when text.StartsWith "HTML: " -> Html (text.Substring 6) |         | text when text.StartsWith "HTML: " -> Html text[6..] | ||||||
|         | text -> invalidOp $"Cannot derive type of text ({text})" |         | text -> invalidOp $"Cannot derive type of text ({text})" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An item of metadata | /// An item of metadata | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type MetaItem = | type MetaItem = { | ||||||
|     {   /// The name of the metadata value |     /// The name of the metadata value | ||||||
|     Name : string |     Name : string | ||||||
|      |      | ||||||
|     /// The metadata value |     /// The metadata value | ||||||
| @ -340,8 +339,8 @@ module MetaItem = | |||||||
| 
 | 
 | ||||||
| /// A revision of a page or post | /// A revision of a page or post | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type Revision = | type Revision = { | ||||||
|     {   /// When this revision was saved |     /// When this revision was saved | ||||||
|     AsOf : Instant |     AsOf : Instant | ||||||
| 
 | 
 | ||||||
|     /// The text of the revision |     /// The text of the revision | ||||||
| @ -353,9 +352,7 @@ module Revision = | |||||||
|      |      | ||||||
|     /// An empty revision |     /// An empty revision | ||||||
|     let empty = |     let empty = | ||||||
|         {   AsOf = Noda.epoch |         { AsOf = Noda.epoch; Text = Html "" } | ||||||
|             Text = Html "" |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A permanent link | /// A permanent link | ||||||
| @ -384,7 +381,7 @@ module PageId = | |||||||
|     let toString = function PageId pi -> pi |     let toString = function PageId pi -> pi | ||||||
|      |      | ||||||
|     /// Create a new page ID |     /// Create a new page ID | ||||||
|     let create () = PageId (newId ()) |     let create = newId >> PageId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// PodcastIndex.org podcast:medium allowed values | /// PodcastIndex.org podcast:medium allowed values | ||||||
| @ -421,7 +418,7 @@ module PodcastMedium = | |||||||
|         | "audiobook"  -> Audiobook |         | "audiobook"  -> Audiobook | ||||||
|         | "newsletter" -> Newsletter |         | "newsletter" -> Newsletter | ||||||
|         | "blog"       -> Blog |         | "blog"       -> Blog | ||||||
|         | it           -> invalidOp $"{it} is not a valid podcast medium" |         | it           -> invalidArg "medium" $"{it} is not a valid podcast medium" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Statuses for posts | /// Statuses for posts | ||||||
| @ -442,7 +439,7 @@ module PostStatus = | |||||||
|         match value with |         match value with | ||||||
|         | "Draft" -> Draft |         | "Draft" -> Draft | ||||||
|         | "Published" -> Published |         | "Published" -> Published | ||||||
|         | it          -> invalidOp $"{it} is not a valid post status" |         | it          -> invalidArg "status" $"{it} is not a valid post status" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An identifier for a post | /// An identifier for a post | ||||||
| @ -458,12 +455,12 @@ module PostId = | |||||||
|     let toString = function PostId pi -> pi |     let toString = function PostId pi -> pi | ||||||
|      |      | ||||||
|     /// Create a new post ID |     /// Create a new post ID | ||||||
|     let create () = PostId (newId ()) |     let create = newId >> PostId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A redirection for a previously valid URL | /// A redirection for a previously valid URL | ||||||
| type RedirectRule = | type RedirectRule = { | ||||||
|     {   /// The From string or pattern |     /// The From string or pattern | ||||||
|     From : string |     From : string | ||||||
|      |      | ||||||
|     /// The To string or pattern |     /// The To string or pattern | ||||||
| @ -477,8 +474,8 @@ type RedirectRule = | |||||||
| module RedirectRule = | module RedirectRule = | ||||||
| 
 | 
 | ||||||
|     /// An empty redirect rule |     /// An empty redirect rule | ||||||
|     let empty = |     let empty = { | ||||||
|         {   From    = "" |         From    = "" | ||||||
|         To      = "" |         To      = "" | ||||||
|         IsRegex = false |         IsRegex = false | ||||||
|     } |     } | ||||||
| @ -497,7 +494,7 @@ module CustomFeedId = | |||||||
|     let toString = function CustomFeedId pi -> pi |     let toString = function CustomFeedId pi -> pi | ||||||
|      |      | ||||||
|     /// Create a new custom feed ID |     /// Create a new custom feed ID | ||||||
|     let create () = CustomFeedId (newId ()) |     let create = newId >> CustomFeedId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// The source for a custom feed | /// The source for a custom feed | ||||||
| @ -525,8 +522,8 @@ module CustomFeedSource = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Options for a feed that describes a podcast | /// Options for a feed that describes a podcast | ||||||
| type PodcastOptions = | type PodcastOptions = { | ||||||
|     {   /// The title of the podcast |     /// The title of the podcast | ||||||
|     Title : string |     Title : string | ||||||
|      |      | ||||||
|     /// A subtitle for the podcast |     /// A subtitle for the podcast | ||||||
| @ -577,8 +574,8 @@ type PodcastOptions = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A custom feed | /// A custom feed | ||||||
| type CustomFeed = | type CustomFeed = { | ||||||
|     {   /// The ID of the custom feed |     /// The ID of the custom feed | ||||||
|     Id : CustomFeedId |     Id : CustomFeedId | ||||||
|      |      | ||||||
|     /// The source for the custom feed |     /// The source for the custom feed | ||||||
| @ -595,8 +592,8 @@ type CustomFeed = | |||||||
| module CustomFeed = | module CustomFeed = | ||||||
|      |      | ||||||
|     /// An empty custom feed |     /// An empty custom feed | ||||||
|     let empty = |     let empty = { | ||||||
|         {   Id      = CustomFeedId "" |         Id      = CustomFeedId "" | ||||||
|         Source  = Category (CategoryId "") |         Source  = Category (CategoryId "") | ||||||
|         Path    = Permalink "" |         Path    = Permalink "" | ||||||
|         Podcast = None |         Podcast = None | ||||||
| @ -605,8 +602,8 @@ module CustomFeed = | |||||||
| 
 | 
 | ||||||
| /// Really Simple Syndication (RSS) options for this web log | /// Really Simple Syndication (RSS) options for this web log | ||||||
| [<CLIMutable; NoComparison; NoEquality>] | [<CLIMutable; NoComparison; NoEquality>] | ||||||
| type RssOptions = | type RssOptions = { | ||||||
|     {   /// Whether the site feed of posts is enabled |     /// Whether the site feed of posts is enabled | ||||||
|     IsFeedEnabled : bool |     IsFeedEnabled : bool | ||||||
|      |      | ||||||
|     /// The name of the file generated for the site feed |     /// The name of the file generated for the site feed | ||||||
| @ -632,8 +629,8 @@ type RssOptions = | |||||||
| module RssOptions = | module RssOptions = | ||||||
|      |      | ||||||
|     /// An empty set of RSS options |     /// An empty set of RSS options | ||||||
|     let empty = |     let empty = { | ||||||
|         {   IsFeedEnabled     = true |         IsFeedEnabled     = true | ||||||
|         FeedName          = "feed.xml" |         FeedName          = "feed.xml" | ||||||
|         ItemsInFeed       = None |         ItemsInFeed       = None | ||||||
|         IsCategoryEnabled = true |         IsCategoryEnabled = true | ||||||
| @ -656,7 +653,7 @@ module TagMapId = | |||||||
|     let toString = function TagMapId tmi -> tmi |     let toString = function TagMapId tmi -> tmi | ||||||
|      |      | ||||||
|     /// Create a new tag mapping ID |     /// Create a new tag mapping ID | ||||||
|     let create () = TagMapId (newId ()) |     let create = newId >> TagMapId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An identifier for a theme (represents its path) | /// An identifier for a theme (represents its path) | ||||||
| @ -683,8 +680,8 @@ module ThemeAssetId = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A template for a theme | /// A template for a theme | ||||||
| type ThemeTemplate = | type ThemeTemplate = { | ||||||
|     {   /// The name of the template |     /// The name of the template | ||||||
|     Name : string |     Name : string | ||||||
|      |      | ||||||
|     /// The text of the template |     /// The text of the template | ||||||
| @ -696,9 +693,7 @@ module ThemeTemplate = | |||||||
|      |      | ||||||
|     /// An empty theme template |     /// An empty theme template | ||||||
|     let empty = |     let empty = | ||||||
|         {   Name = "" |         { Name = ""; Text = "" } | ||||||
|             Text = "" |  | ||||||
|         } |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// Where uploads should be placed | /// Where uploads should be placed | ||||||
| @ -717,7 +712,7 @@ module UploadDestination = | |||||||
|         match value with |         match value with | ||||||
|         | "Database" -> Database |         | "Database" -> Database | ||||||
|         | "Disk"     -> Disk |         | "Disk"     -> Disk | ||||||
|         | it         -> invalidOp $"{it} is not a valid upload destination" |         | it         -> invalidArg "destination" $"{it} is not a valid upload destination" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An identifier for an upload | /// An identifier for an upload | ||||||
| @ -733,7 +728,7 @@ module UploadId = | |||||||
|     let toString = function UploadId ui -> ui |     let toString = function UploadId ui -> ui | ||||||
|      |      | ||||||
|     /// Create a new upload ID |     /// Create a new upload ID | ||||||
|     let create () = UploadId (newId ()) |     let create = newId >> UploadId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// An identifier for a web log | /// An identifier for a web log | ||||||
| @ -749,7 +744,7 @@ module WebLogId = | |||||||
|     let toString = function WebLogId wli -> wli |     let toString = function WebLogId wli -> wli | ||||||
|      |      | ||||||
|     /// Create a new web log ID |     /// Create a new web log ID | ||||||
|     let create () = WebLogId (newId ()) |     let create = newId >> WebLogId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @ -766,6 +761,6 @@ module WebLogUserId = | |||||||
|     let toString = function WebLogUserId wli -> wli |     let toString = function WebLogUserId wli -> wli | ||||||
|      |      | ||||||
|     /// Create a new web log user ID |     /// Create a new web log user ID | ||||||
|     let create () = WebLogUserId (newId ()) |     let create = newId >> WebLogUserId | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -26,8 +26,8 @@ module PublicHelpers = | |||||||
| 
 | 
 | ||||||
| /// The model used to display the admin dashboard | /// The model used to display the admin dashboard | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type DashboardModel = | type DashboardModel = { | ||||||
|     {   /// The number of published posts |     /// The number of published posts | ||||||
|     Posts : int |     Posts : int | ||||||
| 
 | 
 | ||||||
|     /// The number of post drafts |     /// The number of post drafts | ||||||
| @ -49,8 +49,8 @@ type DashboardModel = | |||||||
| 
 | 
 | ||||||
| /// Details about a category, used to display category lists | /// Details about a category, used to display category lists | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type DisplayCategory = | type DisplayCategory = { | ||||||
|     {   /// The ID of the category |     /// The ID of the category | ||||||
|     Id : string |     Id : string | ||||||
|      |      | ||||||
|     /// The slug for the category |     /// The slug for the category | ||||||
| @ -71,8 +71,8 @@ type DisplayCategory = | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| /// A display version of a custom feed definition | /// A display version of a custom feed definition | ||||||
| type DisplayCustomFeed = | type DisplayCustomFeed = { | ||||||
|     {   /// The ID of the custom feed |     /// The ID of the custom feed | ||||||
|     Id : string |     Id : string | ||||||
|      |      | ||||||
|     /// The source of the custom feed |     /// The source of the custom feed | ||||||
| @ -85,8 +85,11 @@ type DisplayCustomFeed = | |||||||
|     IsPodcast : bool |     IsPodcast : bool | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Support functions for custom feed displays | ||||||
|  | module DisplayCustomFeed = | ||||||
|  |      | ||||||
|     /// Create a display version from a custom feed |     /// Create a display version from a custom feed | ||||||
|     static member fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed = |     let fromFeed (cats : DisplayCategory[]) (feed : CustomFeed) : DisplayCustomFeed = | ||||||
|         let source = |         let source = | ||||||
|             match feed.Source with |             match feed.Source with | ||||||
|             | Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}" |             | Category (CategoryId catId) -> $"Category: {(cats |> Array.find (fun cat -> cat.Id = catId)).Name}" | ||||||
| @ -133,7 +136,7 @@ type DisplayPage = | |||||||
|     } |     } | ||||||
|      |      | ||||||
|     /// Create a minimal display page (no text or metadata) from a database page |     /// Create a minimal display page (no text or metadata) from a database page | ||||||
|     static member fromPageMinimal webLog (page : Page) = |     static member FromPageMinimal webLog (page : Page) = | ||||||
|         let pageId = PageId.toString page.Id |         let pageId = PageId.toString page.Id | ||||||
|         {   Id           = pageId |         {   Id           = pageId | ||||||
|             AuthorId     = WebLogUserId.toString page.AuthorId |             AuthorId     = WebLogUserId.toString page.AuthorId | ||||||
| @ -148,7 +151,7 @@ type DisplayPage = | |||||||
|         } |         } | ||||||
|      |      | ||||||
|     /// Create a display page from a database page |     /// Create a display page from a database page | ||||||
|     static member fromPage webLog (page : Page) = |     static member FromPage webLog (page : Page) = | ||||||
|         let _, extra = WebLog.hostAndPath webLog |         let _, extra = WebLog.hostAndPath webLog | ||||||
|         let pageId = PageId.toString page.Id |         let pageId = PageId.toString page.Id | ||||||
|         {   Id           = pageId |         {   Id           = pageId | ||||||
| @ -166,8 +169,8 @@ type DisplayPage = | |||||||
| 
 | 
 | ||||||
| /// Information about a revision used for display | /// Information about a revision used for display | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type DisplayRevision = | type DisplayRevision = { | ||||||
|     {   /// The as-of date/time for the revision |     /// The as-of date/time for the revision | ||||||
|     AsOf : DateTime |     AsOf : DateTime | ||||||
|      |      | ||||||
|     /// The as-of date/time for the revision in the web log's local time zone |     /// The as-of date/time for the revision in the web log's local time zone | ||||||
| @ -176,10 +179,12 @@ type DisplayRevision = | |||||||
|     /// The format of the text of the revision |     /// The format of the text of the revision | ||||||
|     Format : string |     Format : string | ||||||
| } | } | ||||||
| with | 
 | ||||||
|  | /// Functions to support displaying revisions | ||||||
|  | module DisplayRevision = | ||||||
|      |      | ||||||
|     /// Create a display revision from an actual revision |     /// Create a display revision from an actual revision | ||||||
|     static member fromRevision webLog (rev : Revision) = |     let fromRevision webLog (rev : Revision) = | ||||||
|         {   AsOf      = rev.AsOf.ToDateTimeUtc () |         {   AsOf      = rev.AsOf.ToDateTimeUtc () | ||||||
|             AsOfLocal = WebLog.localTime webLog rev.AsOf |             AsOfLocal = WebLog.localTime webLog rev.AsOf | ||||||
|             Format    = MarkupText.sourceType rev.Text |             Format    = MarkupText.sourceType rev.Text | ||||||
| @ -190,8 +195,8 @@ open System.IO | |||||||
| 
 | 
 | ||||||
| /// Information about a theme used for display | /// Information about a theme used for display | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type DisplayTheme = | type DisplayTheme = { | ||||||
|     {   /// The ID / path slug of the theme |     /// The ID / path slug of the theme | ||||||
|     Id : string |     Id : string | ||||||
|      |      | ||||||
|     /// The name of the theme |     /// The name of the theme | ||||||
| @ -209,10 +214,12 @@ type DisplayTheme = | |||||||
|     /// Whether the theme .zip file exists on the filesystem |     /// Whether the theme .zip file exists on the filesystem | ||||||
|     IsOnDisk : bool |     IsOnDisk : bool | ||||||
| } | } | ||||||
| with | 
 | ||||||
|  | /// Functions to support displaying themes | ||||||
|  | module DisplayTheme = | ||||||
|      |      | ||||||
|     /// Create a display theme from a theme |     /// Create a display theme from a theme | ||||||
|     static member fromTheme inUseFunc (theme : Theme) = |     let fromTheme inUseFunc (theme : Theme) = | ||||||
|         {   Id            = ThemeId.toString theme.Id |         {   Id            = ThemeId.toString theme.Id | ||||||
|             Name          = theme.Name |             Name          = theme.Name | ||||||
|             Version       = theme.Version |             Version       = theme.Version | ||||||
| @ -224,8 +231,8 @@ with | |||||||
| 
 | 
 | ||||||
| /// Information about an uploaded file used for display | /// Information about an uploaded file used for display | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type DisplayUpload = | type DisplayUpload = { | ||||||
|     {   /// The ID of the uploaded file |     /// The ID of the uploaded file | ||||||
|     Id : string |     Id : string | ||||||
|      |      | ||||||
|     /// The name of the uploaded file |     /// The name of the uploaded file | ||||||
| @ -241,8 +248,11 @@ type DisplayUpload = | |||||||
|     Source : string |     Source : string | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Functions to support displaying uploads | ||||||
|  | module DisplayUpload = | ||||||
|  |      | ||||||
|     /// Create a display uploaded file |     /// Create a display uploaded file | ||||||
|     static member fromUpload webLog source (upload : Upload) = |     let fromUpload webLog source (upload : Upload) = | ||||||
|         let path = Permalink.toString upload.Path |         let path = Permalink.toString upload.Path | ||||||
|         let name = Path.GetFileName path |         let name = Path.GetFileName path | ||||||
|         {   Id        = UploadId.toString upload.Id |         {   Id        = UploadId.toString upload.Id | ||||||
| @ -255,8 +265,8 @@ type DisplayUpload = | |||||||
| 
 | 
 | ||||||
| /// View model to display a user's information | /// View model to display a user's information | ||||||
| [<NoComparison; NoEquality>] | [<NoComparison; NoEquality>] | ||||||
| type DisplayUser = | type DisplayUser = { | ||||||
|     {   /// The ID of the user |     /// The ID of the user | ||||||
|     Id : string |     Id : string | ||||||
| 
 | 
 | ||||||
|     /// The user name (e-mail address) |     /// The user name (e-mail address) | ||||||
| @ -284,8 +294,11 @@ type DisplayUser = | |||||||
|     LastSeenOn : Nullable<DateTime> |     LastSeenOn : Nullable<DateTime> | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | /// Functions to support displaying a user's information | ||||||
|  | module DisplayUser = | ||||||
|  |      | ||||||
|     /// Construct a displayed user from a web log user |     /// Construct a displayed user from a web log user | ||||||
|     static member fromUser webLog (user : WebLogUser) = |     let fromUser webLog (user : WebLogUser) = | ||||||
|         {   Id            = WebLogUserId.toString user.Id |         {   Id            = WebLogUserId.toString user.Id | ||||||
|             Email         = user.Email |             Email         = user.Email | ||||||
|             FirstName     = user.FirstName |             FirstName     = user.FirstName | ||||||
|  | |||||||
| @ -131,7 +131,7 @@ module PageListCache = | |||||||
|     let private fillPages (webLog : WebLog) pages = |     let private fillPages (webLog : WebLog) pages = | ||||||
|         _cache[webLog.Id] <- |         _cache[webLog.Id] <- | ||||||
|             pages |             pages | ||||||
|             |> List.map (fun pg -> DisplayPage.fromPage webLog { pg with Text = "" }) |             |> List.map (fun pg -> DisplayPage.FromPage webLog { pg with Text = "" }) | ||||||
|             |> Array.ofList |             |> Array.ofList | ||||||
|      |      | ||||||
|     /// Are there pages cached for this web log? |     /// Are there pages cached for this web log? | ||||||
|  | |||||||
| @ -15,7 +15,7 @@ let all pageNbr : HttpHandler = requireAccess Author >=> fun next ctx -> task { | |||||||
|         |> addToHash "pages"     (pages |         |> addToHash "pages"     (pages | ||||||
|                                   |> Seq.ofList |                                   |> Seq.ofList | ||||||
|                                   |> Seq.truncate 25 |                                   |> Seq.truncate 25 | ||||||
|                                   |> Seq.map (DisplayPage.fromPageMinimal ctx.WebLog) |                                   |> Seq.map (DisplayPage.FromPageMinimal ctx.WebLog) | ||||||
|                                   |> List.ofSeq) |                                   |> List.ofSeq) | ||||||
|         |> addToHash "page_nbr"  pageNbr |         |> addToHash "page_nbr"  pageNbr | ||||||
|         |> addToHash "prev_page" (if pageNbr = 2 then "" else $"/page/{pageNbr - 1}") |         |> addToHash "prev_page" (if pageNbr = 2 then "" else $"/page/{pageNbr - 1}") | ||||||
|  | |||||||
| @ -200,7 +200,7 @@ let home : HttpHandler = fun next ctx -> task { | |||||||
|         | Some page -> |         | Some page -> | ||||||
|             return! |             return! | ||||||
|                 hashForPage page.Title |                 hashForPage page.Title | ||||||
|                 |> addToHash "page" (DisplayPage.fromPage webLog page) |                 |> addToHash "page" (DisplayPage.FromPage webLog page) | ||||||
|                 |> addToHash ViewContext.IsHome true |                 |> addToHash ViewContext.IsHome true | ||||||
|                 |> themedView (defaultArg page.Template "single-page") next ctx |                 |> themedView (defaultArg page.Template "single-page") next ctx | ||||||
|         | None -> return! Error.notFound next ctx |         | None -> return! Error.notFound next ctx | ||||||
|  | |||||||
| @ -40,7 +40,7 @@ module CatchAll = | |||||||
|                 debug (fun () -> "Found page by permalink") |                 debug (fun () -> "Found page by permalink") | ||||||
|                 yield fun next ctx -> |                 yield fun next ctx -> | ||||||
|                     hashForPage page.Title |                     hashForPage page.Title | ||||||
|                     |> addToHash "page"             (DisplayPage.fromPage webLog page) |                     |> addToHash "page"             (DisplayPage.FromPage webLog page) | ||||||
|                     |> addToHash ViewContext.IsPage true |                     |> addToHash ViewContext.IsPage true | ||||||
|                     |> themedView (defaultArg page.Template "single-page") next ctx |                     |> themedView (defaultArg page.Template "single-page") next ctx | ||||||
|             | None -> () |             | None -> () | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user