Version 2.1 #41
| @ -4,6 +4,10 @@ | ||||
| 		<ProjectReference Include="..\MyWebLog.Domain\MyWebLog.Domain.fsproj" /> | ||||
| 	</ItemGroup> | ||||
| 
 | ||||
| 	<ItemGroup> | ||||
| 		<ProjectReference Include="..\..\..\BitBadger.Sqlite.Documents\src\BitBadger.Sqlite.FSharp.Documents\BitBadger.Sqlite.FSharp.Documents.fsproj" /> | ||||
| 	</ItemGroup> | ||||
| 	 | ||||
| 	<ItemGroup> | ||||
| 		<PackageReference Include="BitBadger.Npgsql.FSharp.Documents" Version="2.0.0" /> | ||||
| 		<PackageReference Include="Microsoft.Data.Sqlite" Version="8.0.0" /> | ||||
|  | ||||
| @ -1,5 +1,7 @@ | ||||
| namespace MyWebLog.Data | ||||
| 
 | ||||
| open System.Threading.Tasks | ||||
| open BitBadger.Sqlite.FSharp.Documents | ||||
| open Microsoft.Data.Sqlite | ||||
| open Microsoft.Extensions.Logging | ||||
| open MyWebLog | ||||
| @ -12,108 +14,98 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria | ||||
|      | ||||
|     let ensureTables () = backgroundTask { | ||||
| 
 | ||||
|         use cmd = conn.CreateCommand() | ||||
|          | ||||
|         let! tables = backgroundTask { | ||||
|             cmd.CommandText <- "SELECT name FROM sqlite_master WHERE type = 'table'" | ||||
|             let! rdr = cmd.ExecuteReaderAsync() | ||||
|             let mutable tableList = [] | ||||
|             while! rdr.ReadAsync() do | ||||
|                 tableList <- Map.getString "name" rdr :: tableList | ||||
|             do! rdr.CloseAsync() | ||||
|             return tableList | ||||
|         } | ||||
|         let! tables = Custom.list<string> "SELECT name FROM sqlite_master WHERE type = 'table'" None _.GetString(0) | ||||
|          | ||||
|         let needsTable table = | ||||
|             not (List.contains table tables) | ||||
|          | ||||
|         let jsonTable table = | ||||
|             $"CREATE TABLE {table} (data TEXT NOT NULL); | ||||
|               CREATE UNIQUE INDEX idx_{table}_key ON {table} ((data ->> 'Id'))" | ||||
|             $"{Definition.createTable table}; {Definition.createKey table}" | ||||
|          | ||||
|         seq { | ||||
|             // Theme tables | ||||
|             if needsTable Table.Theme then jsonTable Table.Theme | ||||
|             if needsTable Table.ThemeAsset then | ||||
|                 $"CREATE TABLE {Table.ThemeAsset} ( | ||||
|                     theme_id    TEXT NOT NULL, | ||||
|                     path        TEXT NOT NULL, | ||||
|                     updated_on  TEXT NOT NULL, | ||||
|                     data        BLOB NOT NULL, | ||||
|                     PRIMARY KEY (theme_id, path))" | ||||
|              | ||||
|             // Web log table | ||||
|             if needsTable Table.WebLog then jsonTable Table.WebLog | ||||
|              | ||||
|             // Category table | ||||
|             if needsTable Table.Category then | ||||
|                 $"{jsonTable Table.Category}; | ||||
|                   CREATE INDEX idx_{Table.Category}_web_log ON {Table.Category} ((data ->> 'WebLogId'))" | ||||
|              | ||||
|             // Web log user table | ||||
|             if needsTable Table.WebLogUser then | ||||
|                 $"{jsonTable Table.WebLogUser}; | ||||
|                   CREATE INDEX idx_{Table.WebLogUser}_email | ||||
|                     ON {Table.WebLogUser} ((data ->> 'WebLogId'), (data ->> 'Email'))" | ||||
|              | ||||
|             // Page tables | ||||
|             if needsTable Table.Page then | ||||
|                 $"{jsonTable Table.Page}; | ||||
|                   CREATE INDEX idx_{Table.Page}_author ON {Table.Page} ((data ->> 'AuthorId')); | ||||
|                   CREATE INDEX idx_{Table.Page}_permalink | ||||
|                     ON {Table.Page} ((data ->> 'WebLogId'), (data ->> 'Permalink'))" | ||||
|             if needsTable Table.PageRevision then | ||||
|                 "CREATE TABLE page_revision ( | ||||
|                     page_id        TEXT NOT NULL, | ||||
|                     as_of          TEXT NOT NULL, | ||||
|                     revision_text  TEXT NOT NULL, | ||||
|                     PRIMARY KEY (page_id, as_of))" | ||||
|              | ||||
|             // Post tables | ||||
|             if needsTable Table.Post then | ||||
|                 $"{jsonTable Table.Post}; | ||||
|                   CREATE INDEX idx_{Table.Post}_author ON {Table.Post} ((data ->> 'AuthorId')); | ||||
|                   CREATE INDEX idx_{Table.Post}_status | ||||
|                     ON {Table.Post} ((data ->> 'WebLogId'), (data ->> 'Status'), (data ->> 'UpdatedOn')); | ||||
|                   CREATE INDEX idx_{Table.Post}_permalink | ||||
|                     ON {Table.Post} ((data ->> 'WebLogId'), (data ->> 'Permalink'))" | ||||
|                   // TODO: index categories by post? | ||||
|             if needsTable Table.PostRevision then | ||||
|                 $"CREATE TABLE {Table.PostRevision} ( | ||||
|                     post_id        TEXT NOT NULL, | ||||
|                     as_of          TEXT NOT NULL, | ||||
|                     revision_text  TEXT NOT NULL, | ||||
|                     PRIMARY KEY (post_id, as_of))" | ||||
|             if needsTable Table.PostComment then | ||||
|                 $"{jsonTable Table.PostComment}; | ||||
|                   CREATE INDEX idx_{Table.PostComment}_post ON {Table.PostComment} ((data ->> 'PostId'))" | ||||
|              | ||||
|             // Tag map table | ||||
|             if needsTable Table.TagMap then | ||||
|                 $"{jsonTable Table.TagMap}; | ||||
|                   CREATE INDEX idx_{Table.TagMap}_tag ON {Table.TagMap} ((data ->> 'WebLogId'), (data ->> 'UrlValue'))" | ||||
|              | ||||
|             // Uploaded file table | ||||
|             if needsTable Table.Upload then | ||||
|                 $"CREATE TABLE {Table.Upload} ( | ||||
|                     id          TEXT PRIMARY KEY, | ||||
|                     web_log_id  TEXT NOT NULL, | ||||
|                     path        TEXT NOT NULL, | ||||
|                     updated_on  TEXT NOT NULL, | ||||
|                     data        BLOB NOT NULL); | ||||
|                   CREATE INDEX idx_{Table.Upload}_path ON {Table.Upload} (web_log_id, path)" | ||||
|              | ||||
|             // Database version table | ||||
|             if needsTable Table.DbVersion then | ||||
|                 $"CREATE TABLE {Table.DbVersion} (id TEXT PRIMARY KEY); | ||||
|                   INSERT INTO {Table.DbVersion} VALUES ('v2.1')" | ||||
|         } | ||||
|         |> Seq.map (fun sql -> | ||||
|             log.LogInformation $"Creating {(sql.Split ' ')[2]} table..." | ||||
|             cmd.CommandText <- sql | ||||
|             write cmd |> Async.AwaitTask |> Async.RunSynchronously) | ||||
|         |> List.ofSeq | ||||
|         |> ignore | ||||
|         let tasks = | ||||
|             seq { | ||||
|                 // Theme tables | ||||
|                 if needsTable Table.Theme then jsonTable Table.Theme | ||||
|                 if needsTable Table.ThemeAsset then | ||||
|                     $"CREATE TABLE {Table.ThemeAsset} ( | ||||
|                         theme_id    TEXT NOT NULL, | ||||
|                         path        TEXT NOT NULL, | ||||
|                         updated_on  TEXT NOT NULL, | ||||
|                         data        BLOB NOT NULL, | ||||
|                         PRIMARY KEY (theme_id, path))" | ||||
|                  | ||||
|                 // Web log table | ||||
|                 if needsTable Table.WebLog then jsonTable Table.WebLog | ||||
|                  | ||||
|                 // Category table | ||||
|                 if needsTable Table.Category then | ||||
|                     $"{jsonTable Table.Category}; | ||||
|                       CREATE INDEX idx_{Table.Category}_web_log ON {Table.Category} ((data ->> 'WebLogId'))" | ||||
|                  | ||||
|                 // Web log user table | ||||
|                 if needsTable Table.WebLogUser then | ||||
|                     $"{jsonTable Table.WebLogUser}; | ||||
|                       CREATE INDEX idx_{Table.WebLogUser}_email | ||||
|                         ON {Table.WebLogUser} ((data ->> 'WebLogId'), (data ->> 'Email'))" | ||||
|                  | ||||
|                 // Page tables | ||||
|                 if needsTable Table.Page then | ||||
|                     $"{jsonTable Table.Page}; | ||||
|                       CREATE INDEX idx_{Table.Page}_author ON {Table.Page} ((data ->> 'AuthorId')); | ||||
|                       CREATE INDEX idx_{Table.Page}_permalink | ||||
|                         ON {Table.Page} ((data ->> 'WebLogId'), (data ->> 'Permalink'))" | ||||
|                 if needsTable Table.PageRevision then | ||||
|                     $"CREATE TABLE {Table.PageRevision} ( | ||||
|                         page_id        TEXT NOT NULL, | ||||
|                         as_of          TEXT NOT NULL, | ||||
|                         revision_text  TEXT NOT NULL, | ||||
|                         PRIMARY KEY (page_id, as_of))" | ||||
|                  | ||||
|                 // Post tables | ||||
|                 if needsTable Table.Post then | ||||
|                     $"{jsonTable Table.Post}; | ||||
|                       CREATE INDEX idx_{Table.Post}_author ON {Table.Post} ((data ->> 'AuthorId')); | ||||
|                       CREATE INDEX idx_{Table.Post}_status | ||||
|                         ON {Table.Post} ((data ->> 'WebLogId'), (data ->> 'Status'), (data ->> 'UpdatedOn')); | ||||
|                       CREATE INDEX idx_{Table.Post}_permalink | ||||
|                         ON {Table.Post} ((data ->> 'WebLogId'), (data ->> 'Permalink'))" | ||||
|                       // TODO: index categories by post? | ||||
|                 if needsTable Table.PostRevision then | ||||
|                     $"CREATE TABLE {Table.PostRevision} ( | ||||
|                         post_id        TEXT NOT NULL, | ||||
|                         as_of          TEXT NOT NULL, | ||||
|                         revision_text  TEXT NOT NULL, | ||||
|                         PRIMARY KEY (post_id, as_of))" | ||||
|                 if needsTable Table.PostComment then | ||||
|                     $"{jsonTable Table.PostComment}; | ||||
|                       CREATE INDEX idx_{Table.PostComment}_post ON {Table.PostComment} ((data ->> 'PostId'))" | ||||
|                  | ||||
|                 // Tag map table | ||||
|                 if needsTable Table.TagMap then | ||||
|                     $"{jsonTable Table.TagMap}; | ||||
|                       CREATE INDEX idx_{Table.TagMap}_tag ON {Table.TagMap} ((data ->> 'WebLogId'), (data ->> 'UrlValue'))" | ||||
|                  | ||||
|                 // Uploaded file table | ||||
|                 if needsTable Table.Upload then | ||||
|                     $"CREATE TABLE {Table.Upload} ( | ||||
|                         id          TEXT PRIMARY KEY, | ||||
|                         web_log_id  TEXT NOT NULL, | ||||
|                         path        TEXT NOT NULL, | ||||
|                         updated_on  TEXT NOT NULL, | ||||
|                         data        BLOB NOT NULL); | ||||
|                       CREATE INDEX idx_{Table.Upload}_path ON {Table.Upload} (web_log_id, path)" | ||||
|                  | ||||
|                 // Database version table | ||||
|                 if needsTable Table.DbVersion then | ||||
|                     $"CREATE TABLE {Table.DbVersion} (id TEXT PRIMARY KEY); | ||||
|                       INSERT INTO {Table.DbVersion} VALUES ('v2.1')" | ||||
|             } | ||||
|             |> Seq.map (fun sql -> | ||||
|                 log.LogInformation $"""Creating {(sql.Replace("IF NOT EXISTS ", "").Split ' ')[2]} table...""" | ||||
|                 Custom.nonQuery sql None) | ||||
|          | ||||
|         let! _ = Task.WhenAll tasks | ||||
|         () | ||||
|     } | ||||
|      | ||||
|     /// Set the database version to the specified version | ||||
| @ -459,15 +451,6 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria | ||||
|     /// The connection for this instance | ||||
|     member _.Conn = conn | ||||
|      | ||||
|     /// Make a SQLite connection ready to execute commends | ||||
|     static member setUpConnection (conn: SqliteConnection) = backgroundTask { | ||||
|         do! conn.OpenAsync() | ||||
|         use cmd = conn.CreateCommand() | ||||
|         cmd.CommandText <- "PRAGMA foreign_keys = TRUE" | ||||
|         let! _ = cmd.ExecuteNonQueryAsync() | ||||
|         () | ||||
|     } | ||||
|      | ||||
|     interface IData with | ||||
|      | ||||
|         member _.Category   = SQLiteCategoryData   (conn, ser, log) | ||||
| @ -484,10 +467,6 @@ type SQLiteData(conn: SqliteConnection, log: ILogger<SQLiteData>, ser: JsonSeria | ||||
|          | ||||
|         member _.StartUp () = backgroundTask { | ||||
|             do! ensureTables () | ||||
|              | ||||
|             use cmd = conn.CreateCommand() | ||||
|             cmd.CommandText <- $"SELECT id FROM {Table.DbVersion}" | ||||
|             use! rdr = cmd.ExecuteReaderAsync() | ||||
|             let! isFound = rdr.ReadAsync() | ||||
|             do! migrate (if isFound then Some (Map.getString "id" rdr) else None) | ||||
|             let! version = Custom.single<string> $"SELECT id FROM {Table.DbVersion}" None _.GetString(0) | ||||
|             do! migrate version | ||||
|         } | ||||
|  | ||||
| @ -50,25 +50,30 @@ type RedirectRuleMiddleware(next: RequestDelegate, log: ILogger<RedirectRuleMidd | ||||
| 
 | ||||
| 
 | ||||
| open System | ||||
| open BitBadger.Npgsql.FSharp.Documents | ||||
| open Microsoft.Extensions.DependencyInjection | ||||
| open MyWebLog.Data | ||||
| open Newtonsoft.Json | ||||
| open Npgsql | ||||
| 
 | ||||
| // The PostgreSQL document library | ||||
| module Postgres = BitBadger.Npgsql.FSharp.Documents | ||||
| 
 | ||||
| // The SQLite document library | ||||
| module Sqlite = BitBadger.Sqlite.FSharp.Documents | ||||
| 
 | ||||
| /// Logic to obtain a data connection and implementation based on configured values | ||||
| module DataImplementation = | ||||
|      | ||||
|     open MyWebLog.Converters | ||||
|     open RethinkDb.Driver.FSharp | ||||
|     open RethinkDb.Driver.Net | ||||
| 
 | ||||
|      | ||||
|     /// Create an NpgsqlDataSource from the connection string, configuring appropriately | ||||
|     let createNpgsqlDataSource (cfg: IConfiguration) = | ||||
|         let builder = NpgsqlDataSourceBuilder(cfg.GetConnectionString "PostgreSQL") | ||||
|         let _ = builder.UseNodaTime() | ||||
|         // let _ = builder.UseLoggerFactory(LoggerFactory.Create(fun it -> it.AddConsole () |> ignore)) | ||||
|         (builder.Build >> Configuration.useDataSource) () | ||||
|         (builder.Build >> Postgres.Configuration.useDataSource) () | ||||
| 
 | ||||
|     /// Get the configured data implementation | ||||
|     let get (sp: IServiceProvider) : IData = | ||||
| @ -77,10 +82,10 @@ module DataImplementation = | ||||
|         let connStr    name = config.GetConnectionString name | ||||
|         let hasConnStr name = (connStr >> isNull >> not) name | ||||
|         let createSQLite connStr : IData = | ||||
|             Sqlite.Configuration.useConnectionString connStr | ||||
|             let log  = sp.GetRequiredService<ILogger<SQLiteData>>() | ||||
|             let conn = new SqliteConnection(connStr) | ||||
|             log.LogInformation $"Using SQLite database {conn.DataSource}" | ||||
|             await (SQLiteData.setUpConnection conn) | ||||
|             SQLiteData(conn, log, Json.configure (JsonSerializer.CreateDefault())) | ||||
|          | ||||
|         if hasConnStr "SQLite" then | ||||
| @ -93,7 +98,7 @@ module DataImplementation = | ||||
|             RethinkDbData(conn, rethinkCfg, log) | ||||
|         elif hasConnStr "PostgreSQL" then | ||||
|             createNpgsqlDataSource config | ||||
|             use conn = Configuration.dataSource().CreateConnection() | ||||
|             use conn = Postgres.Configuration.dataSource().CreateConnection() | ||||
|             let log  = sp.GetRequiredService<ILogger<PostgresData>>() | ||||
|             log.LogInformation $"Using PostgreSQL database {conn.Database}" | ||||
|             PostgresData(log, Json.configure (JsonSerializer.CreateDefault())) | ||||
| @ -170,13 +175,13 @@ let main args = | ||||
|                 opts.TableName  <- "Session" | ||||
|                 opts.Connection <- rethink.Conn) | ||||
|         () | ||||
|     | :? SQLiteData as sql -> | ||||
|     | :? SQLiteData -> | ||||
|         // ADO.NET connections are designed to work as per-request instantiation | ||||
|         let cfg  = sp.GetRequiredService<IConfiguration>() | ||||
|         let _ = | ||||
|             builder.Services.AddScoped<SqliteConnection>(fun sp -> | ||||
|                 let conn = new SqliteConnection(sql.Conn.ConnectionString) | ||||
|                 SQLiteData.setUpConnection conn |> Async.AwaitTask |> Async.RunSynchronously | ||||
|                 let conn = Sqlite.Configuration.dbConn () | ||||
|                 conn.OpenAsync() |> Async.AwaitTask |> Async.RunSynchronously | ||||
|                 conn) | ||||
|         let _ = builder.Services.AddScoped<IData, SQLiteData>() | ||||
|         // Use SQLite for caching as well | ||||
| @ -185,7 +190,7 @@ let main args = | ||||
|         () | ||||
|     | :? PostgresData as postgres -> | ||||
|         // ADO.NET Data Sources are designed to work as singletons | ||||
|         let _ = builder.Services.AddSingleton<NpgsqlDataSource>(Configuration.dataSource ()) | ||||
|         let _ = builder.Services.AddSingleton<NpgsqlDataSource>(Postgres.Configuration.dataSource ()) | ||||
|         let _ = builder.Services.AddSingleton<IData> postgres | ||||
|         let _ = | ||||
|             builder.Services.AddSingleton<IDistributedCache>(fun _ -> | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user