From 87ce966ca193d414a8c13ae25ea5c6c532731576 Mon Sep 17 00:00:00 2001 From: "Daniel J. Summers" Date: Sun, 28 Jul 2019 22:21:05 -0500 Subject: [PATCH] Index tweaks also: - created modules for converters and indexes - ensure we use our JSON converters for both RavenDB and Giraffe - first cut at a composable function pipeline instead of method chaining for IWebHostBuilder --- src/MyPrayerJournal.Api/Data.fs | 110 +++++++++------- src/MyPrayerJournal.Api/Program.fs | 202 ++++++++++++++++------------- 2 files changed, 175 insertions(+), 137 deletions(-) diff --git a/src/MyPrayerJournal.Api/Data.fs b/src/MyPrayerJournal.Api/Data.fs index 4713613..be15843 100644 --- a/src/MyPrayerJournal.Api/Data.fs +++ b/src/MyPrayerJournal.Api/Data.fs @@ -9,65 +9,79 @@ open Raven.Client.Documents.Linq open System open System.Collections.Generic -/// JSON converter for request IDs -type RequestIdJsonConverter () = - inherit JsonConverter () - override __.WriteJson(writer : JsonWriter, value : RequestId, _ : JsonSerializer) = - (RequestId.toString >> writer.WriteValue) value - override __.ReadJson(reader: JsonReader, _ : Type, _ : RequestId, _ : bool, _ : JsonSerializer) = - (string >> RequestId.fromIdString) reader.Value +/// JSON converters for various DUs +module Converters = + + /// JSON converter for request IDs + type RequestIdJsonConverter () = + inherit JsonConverter () + override __.WriteJson(writer : JsonWriter, value : RequestId, _ : JsonSerializer) = + (RequestId.toString >> writer.WriteValue) value + override __.ReadJson(reader: JsonReader, _ : Type, _ : RequestId, _ : bool, _ : JsonSerializer) = + (string >> RequestId.fromIdString) reader.Value + /// JSON converter for user IDs + type UserIdJsonConverter () = + inherit JsonConverter () + override __.WriteJson(writer : JsonWriter, value : UserId, _ : JsonSerializer) = + (UserId.toString >> writer.WriteValue) value + override __.ReadJson(reader: JsonReader, _ : Type, _ : UserId, _ : bool, _ : JsonSerializer) = + (string >> UserId) reader.Value -/// JSON converter for user IDs -type UserIdJsonConverter () = - inherit JsonConverter () - override __.WriteJson(writer : JsonWriter, value : UserId, _ : JsonSerializer) = - (UserId.toString >> writer.WriteValue) value - override __.ReadJson(reader: JsonReader, _ : Type, _ : UserId, _ : bool, _ : JsonSerializer) = - (string >> UserId) reader.Value + /// JSON converter for Ticks + type TicksJsonConverter () = + inherit JsonConverter () + override __.WriteJson(writer : JsonWriter, value : Ticks, _ : JsonSerializer) = + (Ticks.toLong >> writer.WriteValue) value + override __.ReadJson(reader: JsonReader, _ : Type, _ : Ticks, _ : bool, _ : JsonSerializer) = + (string >> int64 >> Ticks) reader.Value + /// A sequence of all custom converters for myPrayerJournal + let all : JsonConverter seq = + seq { + yield RequestIdJsonConverter () + yield UserIdJsonConverter () + yield TicksJsonConverter () + } -/// JSON converter for Ticks -type TicksJsonConverter () = - inherit JsonConverter () - override __.WriteJson(writer : JsonWriter, value : Ticks, _ : JsonSerializer) = - (Ticks.toLong >> writer.WriteValue) value - override __.ReadJson(reader: JsonReader, _ : Type, _ : Ticks, _ : bool, _ : JsonSerializer) = - (string >> int64 >> Ticks) reader.Value +/// RavenDB index declarations +module Indexes = + + /// Index requests by user ID + type Requests_ByUserId () as this = + inherit AbstractJavaScriptIndexCreationTask () + do + this.Maps <- HashSet [ "docs.Requests.Select(req => new { userId = req.userId })" ] -/// Index requests by user ID -type Requests_ByUserId () as this = - inherit AbstractJavaScriptIndexCreationTask () - do - this.Maps <- HashSet [ "map('Requests', function (req) { return { userId : req.userId } })" ] + /// Index requests for a journal view + type Requests_AsJournal () as this = + inherit AbstractJavaScriptIndexCreationTask () + do + this.Maps <- HashSet [ + "docs.Requests.Select(req => new { + requestId = req.Id, + userId = req.userId, + text = req.history.Where(hist => hist.text != null).OrderByDescending(hist => hist.asOf).First().text, + asOf = req.history.OrderByDescending(hist => hist.asOf).First().asOf, + snoozedUntil = req.snoozedUntil, + showAfter = req.showAfter, + recurType = req.recurType, + recurCount = req.recurCount + })" + ] + this.Fields <- + [ "text", IndexFieldOptions (Storage = Nullable FieldStorage.Yes) + "asOf", IndexFieldOptions (Storage = Nullable FieldStorage.Yes) + ] + |> dict + |> Dictionary -/// Index requests for a journal view -type Requests_AsJournal () as this = - inherit AbstractJavaScriptIndexCreationTask () - do - this.Maps <- HashSet [ - "map('Requests', function (req) { - var hist = req.history - .filter(function (hist) { return hist.text !== null }) - .sort(function (a, b) { return b - a }) - return { - requestId : req.Id, - userId : req.userId, - text : hist[0].text, - asOf : req.history[req.history.length - 1].asOf, - snoozedUntil : req.snoozedUntil, - showAfter : req.showAfter, - recurType : req.recurType, - recurCount : req.recurCount - } - })" - - ] /// Extensions on the IAsyncDocumentSession interface to support our data manipulation needs [] module Extensions = + open Indexes open Raven.Client.Documents.Commands.Batches open Raven.Client.Documents.Operations open Raven.Client.Documents.Session diff --git a/src/MyPrayerJournal.Api/Program.fs b/src/MyPrayerJournal.Api/Program.fs index 75d4e96..b4fff2e 100644 --- a/src/MyPrayerJournal.Api/Program.fs +++ b/src/MyPrayerJournal.Api/Program.fs @@ -2,81 +2,110 @@ namespace MyPrayerJournal.Api open Microsoft.AspNetCore.Builder open Microsoft.AspNetCore.Hosting +open System.IO /// Configuration functions for the application module Configure = - open Microsoft.Extensions.Configuration - open Newtonsoft.Json + /// Configure the content root + let contentRoot root (bldr : IWebHostBuilder) = + bldr.UseContentRoot root - /// Set up the configuration for the app - let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) = - cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath) - .AddJsonFile("appsettings.json", optional = true, reloadOnChange = true) - .AddJsonFile(sprintf "appsettings.%s.json" ctx.HostingEnvironment.EnvironmentName) - .AddEnvironmentVariables () - |> ignore + open Microsoft.Extensions.Configuration + + /// Configure the application configuration + let appConfiguration (bldr : IWebHostBuilder) = + let configuration (ctx : WebHostBuilderContext) (cfg : IConfigurationBuilder) = + cfg.SetBasePath(ctx.HostingEnvironment.ContentRootPath) + .AddJsonFile("appsettings.json", optional = true, reloadOnChange = true) + .AddJsonFile(sprintf "appsettings.%s.json" ctx.HostingEnvironment.EnvironmentName) + .AddEnvironmentVariables () + |> ignore + bldr.ConfigureAppConfiguration configuration open Microsoft.AspNetCore.Server.Kestrel.Core /// Configure Kestrel from appsettings.json - let kestrel (ctx : WebHostBuilderContext) (opts : KestrelServerOptions) = - (ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel" + let kestrel (bldr : IWebHostBuilder) = + let kestrel (ctx : WebHostBuilderContext) (opts : KestrelServerOptions) = + (ctx.Configuration.GetSection >> opts.Configure >> ignore) "Kestrel" + bldr.ConfigureKestrel kestrel - open Giraffe.Serialization - open Microsoft.FSharpLu.Json - - /// Custom settings for the JSON serializer (uses compact representation for options and DUs) - let jsonSettings = - let x = NewtonsoftJsonSerializer.DefaultSettings - x.Converters.Add (CompactUnionJsonConverter (true)) - x.NullValueHandling <- NullValueHandling.Ignore - x.MissingMemberHandling <- MissingMemberHandling.Error - x.Formatting <- Formatting.Indented - x + /// Configure the web root directory + let webRoot pathSegments (bldr : IWebHostBuilder) = + (Path.Combine >> bldr.UseWebRoot) pathSegments open Giraffe + open Giraffe.Serialization open Giraffe.TokenRouter open Microsoft.AspNetCore.Authentication.JwtBearer open Microsoft.Extensions.DependencyInjection + open Microsoft.FSharpLu.Json open MyPrayerJournal + open MyPrayerJournal.Indexes + open Newtonsoft.Json open Raven.Client.Documents open Raven.Client.Documents.Indexes open System.Security.Cryptography.X509Certificates /// Configure dependency injection - let services (sc : IServiceCollection) = - use sp = sc.BuildServiceProvider () - let cfg = sp.GetRequiredService () - sc.AddGiraffe() - .AddAuthentication( - /// Use HTTP "Bearer" authentication with JWTs - fun opts -> - opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme - opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme) - .AddJwtBearer( - /// Configure JWT options with Auth0 options from configuration - fun opts -> - let jwtCfg = cfg.GetSection "Auth0" - opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"] - opts.Audience <- jwtCfg.["Id"]) - |> ignore - sc.AddSingleton (NewtonsoftJsonSerializer jsonSettings) - |> ignore - let config = sc.BuildServiceProvider().GetRequiredService().GetSection "RavenDB" - let store = new DocumentStore () - store.Urls <- [| config.["URLs"] |] - store.Database <- config.["Database"] - store.Certificate <- new X509Certificate2 (config.["Certificate"], config.["Password"]) - store.Conventions.CustomizeJsonSerializer <- (fun x -> - x.Converters.Add (RequestIdJsonConverter ()) - x.Converters.Add (TicksJsonConverter ()) - x.Converters.Add (UserIdJsonConverter ()) - x.Converters.Add (CompactUnionJsonConverter true)) - store.Initialize () |> sc.AddSingleton |> ignore - IndexCreation.CreateIndexes (typeof.Assembly, store) + let services (bldr : IWebHostBuilder) = + let svcs (sc : IServiceCollection) = + /// A set of JSON converters used for both Giraffe's request serialization and RavenDB's storage + let jsonConverters : JsonConverter seq = + seq { + yield! Converters.all + yield CompactUnionJsonConverter true + } + /// Custom settings for the JSON serializer (uses compact representation for options and DUs) + let jsonSettings = + let x = NewtonsoftJsonSerializer.DefaultSettings + jsonConverters |> List.ofSeq |> List.iter x.Converters.Add + x.NullValueHandling <- NullValueHandling.Ignore + x.MissingMemberHandling <- MissingMemberHandling.Error + x.Formatting <- Formatting.Indented + x + use sp = sc.BuildServiceProvider () + let cfg = sp.GetRequiredService () + sc.AddGiraffe() + .AddAuthentication( + /// Use HTTP "Bearer" authentication with JWTs + fun opts -> + opts.DefaultAuthenticateScheme <- JwtBearerDefaults.AuthenticationScheme + opts.DefaultChallengeScheme <- JwtBearerDefaults.AuthenticationScheme) + .AddJwtBearer( + /// Configure JWT options with Auth0 options from configuration + fun opts -> + let jwtCfg = cfg.GetSection "Auth0" + opts.Authority <- sprintf "https://%s/" jwtCfg.["Domain"] + opts.Audience <- jwtCfg.["Id"]) + |> ignore + sc.AddSingleton (NewtonsoftJsonSerializer jsonSettings) + |> ignore + let config = sc.BuildServiceProvider().GetRequiredService().GetSection "RavenDB" + let store = new DocumentStore () + store.Urls <- [| config.["URL"] |] + store.Database <- config.["Database"] + // store.Certificate <- new X509Certificate2 (config.["Certificate"], config.["Password"]) + store.Conventions.CustomizeJsonSerializer <- fun x -> jsonConverters |> List.ofSeq |> List.iter x.Converters.Add + store.Initialize () |> (sc.AddSingleton >> ignore) + IndexCreation.CreateIndexes (typeof.Assembly, store) + bldr.ConfigureServices svcs + open Microsoft.Extensions.Logging + + /// Configure logging + let logging (bldr : IWebHostBuilder) = + let logz (log : ILoggingBuilder) = + let env = log.Services.BuildServiceProvider().GetService () + match env.IsDevelopment () with + | true -> log + | false -> log.AddFilter(fun l -> l > LogLevel.Information) + |> function l -> l.AddConsole().AddDebug() + |> ignore + bldr.ConfigureLogging logz + /// Routes for the available URLs within myPrayerJournal let webApp = router Handlers.Error.notFound [ @@ -108,50 +137,45 @@ module Configure = ] ] + open System + /// Configure the web application - let application (app : IApplicationBuilder) = - let env = app.ApplicationServices.GetService () - match env.IsDevelopment () with - | true -> app.UseDeveloperExceptionPage () - | false -> app.UseGiraffeErrorHandler Handlers.Error.error - |> function - | a -> - a.UseAuthentication() - .UseStaticFiles() - .UseGiraffe webApp - |> ignore + let application (bldr : IWebHostBuilder) = + let appConfig = + Action ( + fun (app : IApplicationBuilder) -> + let env = app.ApplicationServices.GetService () + match env.IsDevelopment () with + | true -> app.UseDeveloperExceptionPage () + | false -> app.UseGiraffeErrorHandler Handlers.Error.error + |> function + | a -> + a.UseAuthentication() + .UseStaticFiles() + .UseGiraffe webApp + |> ignore) + bldr.Configure appConfig - open Microsoft.Extensions.Logging - - /// Configure logging - let logging (log : ILoggingBuilder) = - let env = log.Services.BuildServiceProvider().GetService () - match env.IsDevelopment () with - | true -> log - | false -> log.AddFilter(fun l -> l > LogLevel.Information) - |> function l -> l.AddConsole().AddDebug() - |> ignore + /// Compose all the configurations into one + let webHost appRoot pathSegments = + contentRoot appRoot + >> appConfiguration + >> kestrel + >> webRoot (Array.concat [ [| appRoot |]; pathSegments ]) + >> services + >> logging + >> application + /// Build the web host from the given configuration + let buildHost (bldr : IWebHostBuilder) = bldr.Build () module Program = - open System - open System.IO - let exitCode = 0 - let CreateWebHostBuilder _ = - let contentRoot = Directory.GetCurrentDirectory () - WebHostBuilder() - .UseContentRoot(contentRoot) - .ConfigureAppConfiguration(Configure.configuration) - .UseKestrel(Configure.kestrel) - .UseWebRoot(Path.Combine (contentRoot, "wwwroot")) - .ConfigureServices(Configure.services) - .ConfigureLogging(Configure.logging) - .Configure(Action Configure.application) - [] - let main args = - CreateWebHostBuilder(args).Build().Run() + let main _ = + let appRoot = Directory.GetCurrentDirectory () + use host = WebHostBuilder () |> (Configure.webHost appRoot [| "wwwroot" |] >> Configure.buildHost) + host.Run () exitCode