From 20e11e648754f4093290cd5ff1beec259bf64cb9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Fri, 22 Jun 2018 11:55:20 +0200 Subject: [PATCH 01/13] serving up hyper shcma + schema forms embedded in the resource --- src/SqlStreamStore.HAL/Constants.cs | 1 + .../Resources/AllStreamResource.cs | 3 +- .../Resources/AppendStreamOperation.cs | 32 ++++++++----- .../Resources/NewStreamMessageDto.cs | 17 +++++++ .../Resources/Schema/AppendToStream.json | 46 +++++++++++++++++++ .../Resources/Schema/SetStreamMetadata.json | 17 +++++++ src/SqlStreamStore.HAL/Resources/Schemas.cs | 38 +++++++++++++++ .../Resources/SetStreamMetadataDto.cs | 11 +++++ .../Resources/StreamResource.cs | 3 ++ .../SqlStreamStore.HAL.csproj | 3 ++ 10 files changed, 159 insertions(+), 12 deletions(-) create mode 100644 src/SqlStreamStore.HAL/Resources/NewStreamMessageDto.cs create mode 100644 src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json create mode 100644 src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json create mode 100644 src/SqlStreamStore.HAL/Resources/Schemas.cs create mode 100644 src/SqlStreamStore.HAL/Resources/SetStreamMetadataDto.cs diff --git a/src/SqlStreamStore.HAL/Constants.cs b/src/SqlStreamStore.HAL/Constants.cs index 0a341a6..f957618 100644 --- a/src/SqlStreamStore.HAL/Constants.cs +++ b/src/SqlStreamStore.HAL/Constants.cs @@ -31,6 +31,7 @@ public static class Relations public const string Feed = "streamStore:feed"; public const string Message = "streamStore:message"; public const string Metadata = "streamStore:metadata"; + public const string AppendToStream = "streamStore:append"; } public static class Streams diff --git a/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs b/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs index 7211339..214bbd7 100644 --- a/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs +++ b/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs @@ -66,7 +66,8 @@ public async Task GetPage( payload, metadata = message.JsonMetadata }) - .AddLinks(Links.Message.Self(message))))); + .AddLinks(Links.Message.Self(message)))) + ); if(operation.FromPositionInclusive == Position.End) { diff --git a/src/SqlStreamStore.HAL/Resources/AppendStreamOperation.cs b/src/SqlStreamStore.HAL/Resources/AppendStreamOperation.cs index 2d88f11..11cde5b 100644 --- a/src/SqlStreamStore.HAL/Resources/AppendStreamOperation.cs +++ b/src/SqlStreamStore.HAL/Resources/AppendStreamOperation.cs @@ -51,34 +51,44 @@ private AppendStreamOperation(HttpRequest request, JObject body) : this(request, new JArray { body }) { } - private static NewStreamMessage ParseNewStreamMessage(JToken newStreamMessage, int index) + private static NewStreamMessageDto ParseNewStreamMessage(JToken newStreamMessage, int index) { if(!Guid.TryParse(newStreamMessage.Value("messageId"), out var messageId)) { - throw new InvalidAppendRequestException($"'{nameof(messageId)}' at index {index} was improperly formatted."); + throw new InvalidAppendRequestException( + $"'{nameof(messageId)}' at index {index} was improperly formatted."); } + if(messageId == Guid.Empty) { throw new InvalidAppendRequestException($"'{nameof(messageId)}' at index {index} was empty."); } + var type = newStreamMessage.Value("type"); if(type == null) { throw new InvalidAppendRequestException($"'{nameof(type)}' at index {index} was not set."); } - - return new NewStreamMessage( - messageId, - type, - newStreamMessage.Value("jsonData").ToString(), - newStreamMessage.Value("jsonMetadata")?.ToString()); + + return new NewStreamMessageDto + { + MessageId = messageId, + Type = type, + JsonData = newStreamMessage.Value("jsonData"), + JsonMetadata = newStreamMessage.Value("jsonMetadata") + }; } + public string StreamId { get; } public int ExpectedVersion { get; } - public NewStreamMessage[] NewStreamMessages { get; } + public NewStreamMessageDto[] NewStreamMessages { get; } - public Task Invoke(IStreamStore streamStore, CancellationToken ct) - => streamStore.AppendToStream(StreamId, ExpectedVersion, NewStreamMessages, ct); + public Task Invoke(IStreamStore streamStore, CancellationToken ct) + => streamStore.AppendToStream( + StreamId, + ExpectedVersion, + Array.ConvertAll(NewStreamMessages, dto => dto.ToNewStreamMessage()), + ct); } } \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/NewStreamMessageDto.cs b/src/SqlStreamStore.HAL/Resources/NewStreamMessageDto.cs new file mode 100644 index 0000000..dc53635 --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/NewStreamMessageDto.cs @@ -0,0 +1,17 @@ +namespace SqlStreamStore.HAL.Resources +{ + using System; + using Newtonsoft.Json.Linq; + using SqlStreamStore.Streams; + + internal class NewStreamMessageDto + { + public Guid MessageId { get; set; } + public string Type { get; set; } + public JToken JsonData { get; set; } + public JToken JsonMetadata { get; set; } + + public NewStreamMessage ToNewStreamMessage() + => new NewStreamMessage(MessageId, Type, JsonData.ToString(), JsonMetadata?.ToString()); + } +} \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json new file mode 100644 index 0000000..4a7ad8e --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json @@ -0,0 +1,46 @@ +{ + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "title": "Append to Stream", + "type": "object", + "required": [ + "messageId", + "type" + ], + "properties": { + "messageId": { + "type": "string", + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + }, + "type": { + "type": "string" + }, + "jsonData": { + "type": "object", + "x-schema-form": { + "key": "jsonData", + "type": "textarea", + "rows": 30 + } + }, + "jsonMetadata": { + "type": "object", + "x-schema-form": { + "key": "jsonMetadata", + "type": "textarea", + "rows": 30 + } + } + }, + "$form": [ + "messageId", + "type", + { + "key": "jsonData", + "type": "textarea" + }, + { + "key": "jsonMetadata", + "type": "textarea" + } + ] +} \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json b/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json new file mode 100644 index 0000000..0231093 --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json @@ -0,0 +1,17 @@ +{ + "type": "object", + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "properties": { + "maxCount": { + "type": "integer", + "minimum": 1 + }, + "maxAge": { + "type": "integer", + "minimum": 1 + }, + "metadataJson": { + "type": "object" + } + } +} diff --git a/src/SqlStreamStore.HAL/Resources/Schemas.cs b/src/SqlStreamStore.HAL/Resources/Schemas.cs new file mode 100644 index 0000000..2429d0e --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/Schemas.cs @@ -0,0 +1,38 @@ +namespace SqlStreamStore.HAL.Resources +{ + using System; + using System.Collections.Concurrent; + using System.IO; + using System.Reflection; + using Newtonsoft.Json; + using Newtonsoft.Json.Linq; + + internal static class Schemas + { + private static readonly ConcurrentDictionary s_schemas + = new ConcurrentDictionary(); + + public static JObject AppendToStream => GetSchema(nameof(AppendToStream)); + public static JObject SetStreamMetadata => GetSchema(nameof(SetStreamMetadata)); + + private static JObject GetSchema(string name) => s_schemas.GetOrAdd(name, ReadSchema); + + private static JObject ReadSchema(string name) + { + using(Stream stream = typeof(Schemas) + .GetTypeInfo().Assembly + .GetManifestResourceStream(typeof(Schemas), $"Schema.{name}.json")) + { + if(stream == null) + { + throw new Exception($"Embedded resource, {name}, not found. BUG!"); + } + + using(var reader = new JsonTextReader(new StreamReader(stream))) + { + return JObject.Load(reader); + } + } + } + } +} \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/SetStreamMetadataDto.cs b/src/SqlStreamStore.HAL/Resources/SetStreamMetadataDto.cs new file mode 100644 index 0000000..379a2af --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/SetStreamMetadataDto.cs @@ -0,0 +1,11 @@ +namespace SqlStreamStore.HAL.Resources +{ + using Newtonsoft.Json.Linq; + + internal class SetStreamMetadataDto + { + public JToken MetadataJson { get; set; } + public int? MaxCount { get; set; } + public int? MaxAge { get; set; } + } +} \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/StreamResource.cs b/src/SqlStreamStore.HAL/Resources/StreamResource.cs index 2c4e785..b1d3421 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamResource.cs @@ -75,6 +75,9 @@ public async Task GetPage(ReadStreamOperation operation, CancellationT .AddLinks(Links.Navigation(page, operation)) .AddLinks(Links.Feed(operation)) .AddLinks(Links.Metadata(operation)) + .AddEmbeddedResource( + Constants.Relations.AppendToStream, + new HALResponse(Schemas.AppendToStream)) .AddEmbeddedCollection( Constants.Relations.Message, streamMessages.Zip( diff --git a/src/SqlStreamStore.HAL/SqlStreamStore.HAL.csproj b/src/SqlStreamStore.HAL/SqlStreamStore.HAL.csproj index b925c23..7d6105e 100644 --- a/src/SqlStreamStore.HAL/SqlStreamStore.HAL.csproj +++ b/src/SqlStreamStore.HAL/SqlStreamStore.HAL.csproj @@ -12,4 +12,7 @@ + + + \ No newline at end of file From a21279270d3b51a837939a8ef7638e44ea41cc8e Mon Sep 17 00:00:00 2001 From: thefringeninja Date: Sun, 24 Jun 2018 20:54:45 +0200 Subject: [PATCH 02/13] forward html and javascript requests to devserver --- .../DevServerStartup.cs | 52 +++++++++++++++++++ .../HttpContextExtensions.cs | 13 ++++- .../SqlStreamStoreHalMiddleware.cs | 10 ++-- 3 files changed, 67 insertions(+), 8 deletions(-) diff --git a/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs b/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs index 8789643..f2588fd 100644 --- a/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs +++ b/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs @@ -1,11 +1,15 @@ namespace SqlStreamStore.HAL.DevServer { using System; + using System.Linq; + using System.Net.Http; + using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Hosting; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Primitives; using MidFunc = System.Func< Microsoft.AspNetCore.Http.HttpContext, System.Func, @@ -15,10 +19,12 @@ internal class DevServerStartup : IStartup { private readonly IStreamStore _streamStore; + private readonly HttpClient _httpClient; public DevServerStartup(IStreamStore streamStore) { _streamStore = streamStore; + _httpClient = new HttpClient(); } public IServiceProvider ConfigureServices(IServiceCollection services) => services @@ -29,6 +35,8 @@ public void Configure(IApplicationBuilder app) => app .UseResponseCompression() .Use(CatchAndDisplayErrors) .Use(AllowAllOrigins) + .Use(SqlStreamStreamBrowserJavascript) + .Use(SqlStreamStreamBrowserHtml) .UseSqlStreamStoreHal(_streamStore); private static MidFunc CatchAndDisplayErrors => async (context, next) => @@ -57,5 +65,49 @@ public void Configure(IApplicationBuilder app) => app return next(); }; + + private static string[] GetAcceptHeaders(HttpRequest contextRequest) + => Array.ConvertAll( + contextRequest.Headers.GetCommaSeparatedValues("Accept"), + value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header) + ? header.MediaType + : null); + + private MidFunc SqlStreamStreamBrowserJavascript => (context, next) + => context.Request.Path.Value?.EndsWith(".js") ?? false + ? ForwardToClientDevServer( + context, + $"{context.Request.PathBase.ToUriComponent()}{context.Request.Path.ToUriComponent()}") + : next(); + + private MidFunc SqlStreamStreamBrowserHtml => (context, next) + => GetAcceptHeaders(context.Request) + .Any(header => header == "text/html") + ? ForwardToClientDevServer(context, context.Request.PathBase.ToUriComponent()) + : next(); + + private async Task ForwardToClientDevServer(HttpContext context, string path) + { + using(var request = new HttpRequestMessage( + new HttpMethod(context.Request.Method), + new UriBuilder + { + Port = 3000, + Host = "localhost", + Path = path, + Query = context.Request.QueryString.ToUriComponent() + }.Uri)) + using(var response = await _httpClient.SendAsync(request)) + using(var stream = await response.Content.ReadAsStreamAsync()) + { + context.Response.StatusCode = (int) response.StatusCode; + foreach(var header in response.Headers.Concat(response.Content.Headers)) + { + context.Response.Headers.Add(header.Key, new StringValues(header.Value.ToArray())); + } + + await stream.CopyToAsync(context.Response.Body, 8192, context.RequestAborted); + } + } } } \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/HttpContextExtensions.cs b/src/SqlStreamStore.HAL/HttpContextExtensions.cs index b7d6123..4ccc9e1 100644 --- a/src/SqlStreamStore.HAL/HttpContextExtensions.cs +++ b/src/SqlStreamStore.HAL/HttpContextExtensions.cs @@ -1,12 +1,15 @@ namespace SqlStreamStore.HAL { + using System; using System.IO; using System.Linq; using System.Net.Http; + using System.Net.Http.Headers; using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Newtonsoft.Json; using Newtonsoft.Json.Serialization; + using SqlStreamStore.Streams; internal static class HttpContextExtensions { @@ -76,6 +79,14 @@ public static int GetExpectedVersion(this HttpRequest request) request.Headers[Constants.Headers.ExpectedVersion], out var expectedVersion) ? expectedVersion - : Streams.ExpectedVersion.Any; + : ExpectedVersion.Any; + + public static string[] GetAcceptHeaders(this HttpRequest contextRequest) + => Array.ConvertAll( + contextRequest.Headers + .GetCommaSeparatedValues("Accept"), + value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header) + ? header.MediaType + : null); } } \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/SqlStreamStoreHalMiddleware.cs b/src/SqlStreamStore.HAL/SqlStreamStoreHalMiddleware.cs index 117de03..c542ddb 100644 --- a/src/SqlStreamStore.HAL/SqlStreamStoreHalMiddleware.cs +++ b/src/SqlStreamStore.HAL/SqlStreamStoreHalMiddleware.cs @@ -2,7 +2,6 @@ { using System; using System.Linq; - using System.Net.Http.Headers; using System.Threading.Tasks; using Halcyon.HAL; using Microsoft.AspNetCore.Builder; @@ -27,12 +26,9 @@ private static MidFunc MethodsNotAllowed(params string[] methods) => (context, n return Task.CompletedTask; }; - private static MidFunc AcceptOnlyHalJson => (context, next) => + private static MidFunc AcceptHalJson => (context, next) => { - var accept = context.Request.Headers.GetCommaSeparatedValues("Accept") - .Select(value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header) - ? header.MediaType - : null); + var accept = context.Request.GetAcceptHeaders(); return accept.Any(header => header == Constants.Headers.ContentTypes.HalJson || header == Constants.Headers.ContentTypes.Any) @@ -72,7 +68,7 @@ public static IApplicationBuilder UseSqlStreamStoreHal( return builder .Use(ExceptionHandlingMiddleware.HandleExceptions) - .Use(AcceptOnlyHalJson) + .Use(AcceptHalJson) .Use(Index) .Map("/stream", UseAllStream(streamStore)) .Map("/streams", UseStream(streamStore)); From e6ad8efaafa2a13cf7800095d49dc46e5fcc0c7d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Mon, 25 Jun 2018 14:00:43 +0200 Subject: [PATCH 03/13] metadata is a string --- src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json index 4a7ad8e..cdf0115 100644 --- a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json +++ b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json @@ -23,7 +23,7 @@ } }, "jsonMetadata": { - "type": "object", + "type": "string", "x-schema-form": { "key": "jsonMetadata", "type": "textarea", From d8df1fae40ef19b1dafd275b5516275cce441f82 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Fri, 29 Jun 2018 15:30:53 +0200 Subject: [PATCH 04/13] enabling additional forms --- src/SqlStreamStore.HAL/Constants.cs | 1 + .../Resources/Schema/AppendToStream.json | 14 +------------- .../Resources/Schema/DeleteStream.json | 5 +++++ .../Resources/Schema/DeleteStreamMessage.json | 5 +++++ .../Resources/Schema/SetStreamMetadata.json | 7 ++++++- src/SqlStreamStore.HAL/Resources/Schemas.cs | 17 ++++++++++------- .../Resources/StreamMessageResource.cs | 3 +++ .../Resources/StreamMetadataResource.cs | 18 +++++++++++------- .../Resources/StreamResource.cs | 7 +++++-- 9 files changed, 47 insertions(+), 30 deletions(-) create mode 100644 src/SqlStreamStore.HAL/Resources/Schema/DeleteStream.json create mode 100644 src/SqlStreamStore.HAL/Resources/Schema/DeleteStreamMessage.json diff --git a/src/SqlStreamStore.HAL/Constants.cs b/src/SqlStreamStore.HAL/Constants.cs index f957618..9a608d2 100644 --- a/src/SqlStreamStore.HAL/Constants.cs +++ b/src/SqlStreamStore.HAL/Constants.cs @@ -32,6 +32,7 @@ public static class Relations public const string Message = "streamStore:message"; public const string Metadata = "streamStore:metadata"; public const string AppendToStream = "streamStore:append"; + public const string Delete = "streamStore:delete"; } public static class Streams diff --git a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json index cdf0115..7771379 100644 --- a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json +++ b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json @@ -30,17 +30,5 @@ "rows": 30 } } - }, - "$form": [ - "messageId", - "type", - { - "key": "jsonData", - "type": "textarea" - }, - { - "key": "jsonMetadata", - "type": "textarea" - } - ] + } } \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/Schema/DeleteStream.json b/src/SqlStreamStore.HAL/Resources/Schema/DeleteStream.json new file mode 100644 index 0000000..85fd65d --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/Schema/DeleteStream.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "title": "Delete Stream", + "type": "object" +} \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/Schema/DeleteStreamMessage.json b/src/SqlStreamStore.HAL/Resources/Schema/DeleteStreamMessage.json new file mode 100644 index 0000000..ce184c1 --- /dev/null +++ b/src/SqlStreamStore.HAL/Resources/Schema/DeleteStreamMessage.json @@ -0,0 +1,5 @@ +{ + "$schema": "http://json-schema.org/draft-07/hyper-schema#", + "title": "Delete Stream Message", + "type": "object" +} \ No newline at end of file diff --git a/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json b/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json index 0231093..0811622 100644 --- a/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json +++ b/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json @@ -11,7 +11,12 @@ "minimum": 1 }, "metadataJson": { - "type": "object" + "type": "object", + "x-schema-form": { + "key": "metadataJson", + "type": "textarea", + "rows": 30 + } } } } diff --git a/src/SqlStreamStore.HAL/Resources/Schemas.cs b/src/SqlStreamStore.HAL/Resources/Schemas.cs index 2429d0e..b487462 100644 --- a/src/SqlStreamStore.HAL/Resources/Schemas.cs +++ b/src/SqlStreamStore.HAL/Resources/Schemas.cs @@ -4,20 +4,23 @@ using System.Collections.Concurrent; using System.IO; using System.Reflection; + using Halcyon.HAL; using Newtonsoft.Json; using Newtonsoft.Json.Linq; internal static class Schemas { - private static readonly ConcurrentDictionary s_schemas - = new ConcurrentDictionary(); + private static readonly ConcurrentDictionary s_schemas + = new ConcurrentDictionary(); - public static JObject AppendToStream => GetSchema(nameof(AppendToStream)); - public static JObject SetStreamMetadata => GetSchema(nameof(SetStreamMetadata)); + public static HALResponse AppendToStream => GetSchema(nameof(AppendToStream)); + public static HALResponse SetStreamMetadata => GetSchema(nameof(SetStreamMetadata)); + public static HALResponse DeleteStream => GetSchema(nameof(DeleteStream)); + public static HALResponse DeleteStreamMessage => GetSchema(nameof(DeleteStreamMessage)); - private static JObject GetSchema(string name) => s_schemas.GetOrAdd(name, ReadSchema); + private static HALResponse GetSchema(string name) => s_schemas.GetOrAdd(name, ReadSchema); - private static JObject ReadSchema(string name) + private static HALResponse ReadSchema(string name) { using(Stream stream = typeof(Schemas) .GetTypeInfo().Assembly @@ -30,7 +33,7 @@ private static JObject ReadSchema(string name) using(var reader = new JsonTextReader(new StreamReader(stream))) { - return JObject.Load(reader); + return new HALResponse(JObject.Load(reader)); } } } diff --git a/src/SqlStreamStore.HAL/Resources/StreamMessageResource.cs b/src/SqlStreamStore.HAL/Resources/StreamMessageResource.cs index 9cb8aef..2f546af 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamMessageResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamMessageResource.cs @@ -72,6 +72,9 @@ public async Task GetMessage( payload, metadata = message.JsonMetadata }) + .AddEmbeddedResource( + Constants.Relations.Delete, + Schemas.DeleteStreamMessage) .AddLinks(Links.Self(operation)) .AddLinks(Links.Navigation(operation, message)) .AddLinks(Links.Message(operation))); diff --git a/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs b/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs index 077c474..f300ae1 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs @@ -39,13 +39,17 @@ public async Task GetStreamMetadata( var result = await operation.Invoke(_streamStore, cancellationToken); var response = new Response(new HALResponse(new - { - result.StreamId, - result.MetadataStreamVersion, - result.MaxAge, - result.MaxCount, - result.MetadataJson - }).AddLinks(s_links), + { + result.StreamId, + result.MetadataStreamVersion, + result.MaxAge, + result.MaxCount, + result.MetadataJson + }) + .AddLinks(s_links) + .AddEmbeddedResource( + Constants.Relations.Metadata, + Schemas.SetStreamMetadata), result.MetadataStreamVersion >= 0 ? 200 : 404); return response; diff --git a/src/SqlStreamStore.HAL/Resources/StreamResource.cs b/src/SqlStreamStore.HAL/Resources/StreamResource.cs index b1d3421..788c73e 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamResource.cs @@ -76,8 +76,11 @@ public async Task GetPage(ReadStreamOperation operation, CancellationT .AddLinks(Links.Feed(operation)) .AddLinks(Links.Metadata(operation)) .AddEmbeddedResource( - Constants.Relations.AppendToStream, - new HALResponse(Schemas.AppendToStream)) + Constants.Relations.AppendToStream, + Schemas.AppendToStream) + .AddEmbeddedResource( + Constants.Relations.Delete, + Schemas.DeleteStream) .AddEmbeddedCollection( Constants.Relations.Message, streamMessages.Zip( From d825f562d37efc92e2357f8f33232cad676979ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Fri, 29 Jun 2018 15:32:43 +0200 Subject: [PATCH 05/13] delete shouldn't have a response body --- src/SqlStreamStore.HAL/HttpContextExtensions.cs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/SqlStreamStore.HAL/HttpContextExtensions.cs b/src/SqlStreamStore.HAL/HttpContextExtensions.cs index 4ccc9e1..f547cf1 100644 --- a/src/SqlStreamStore.HAL/HttpContextExtensions.cs +++ b/src/SqlStreamStore.HAL/HttpContextExtensions.cs @@ -31,6 +31,11 @@ public static async Task WriteHalResponse(this HttpContext context, Response res context.Response.Headers.Append(header.Key, header.Value); } + if(context.IsDelete()) + { + return; + } + using(var writer = new JsonTextWriter(new StreamWriter(context.Response.Body)) { CloseOutput = false From 5e16ccad13e86f1df307e2850ae763a02f3985e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Fri, 29 Jun 2018 15:36:18 +0200 Subject: [PATCH 06/13] make it easier to navigate to the stream from the message --- src/SqlStreamStore.HAL/Resources/AllStreamResource.cs | 9 +++++++-- src/SqlStreamStore.HAL/Resources/StreamResource.cs | 8 +++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs b/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs index 214bbd7..e66a70d 100644 --- a/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs +++ b/src/SqlStreamStore.HAL/Resources/AllStreamResource.cs @@ -66,8 +66,9 @@ public async Task GetPage( payload, metadata = message.JsonMetadata }) - .AddLinks(Links.Message.Self(message)))) - ); + .AddLinks( + Links.Message.Self(message), + Links.Message.Feed(message))))); if(operation.FromPositionInclusive == Position.End) { @@ -158,6 +159,10 @@ public static class Message public static Link Self(StreamMessage message) => new Link( Constants.Relations.Self, $"streams/{message.StreamId}/{message.StreamVersion}"); + + public static Link Feed(StreamMessage message) => new Link( + Constants.Relations.Feed, + $"streams/{message.StreamId}"); } } } diff --git a/src/SqlStreamStore.HAL/Resources/StreamResource.cs b/src/SqlStreamStore.HAL/Resources/StreamResource.cs index 788c73e..fbd7cee 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamResource.cs @@ -96,7 +96,9 @@ public async Task GetPage(ReadStreamOperation operation, CancellationT payload, metadata = message.JsonMetadata }) - .AddLinks(Links.Message.Self(message)))), + .AddLinks( + Links.Message.Self(message), + Links.Message.Feed(message)))), page.Status == PageReadStatus.StreamNotFound ? 404 : 200); } @@ -201,6 +203,10 @@ public static class Message public static Link Self(StreamMessage message) => new Link( Constants.Relations.Self, $"{message.StreamId}/{message.StreamVersion}"); + + public static Link Feed(StreamMessage message) => new Link( + Constants.Relations.Feed, + message.StreamId); } } } From cb9accf11ed55113f2253a38a414ea7a7546508a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Fri, 29 Jun 2018 16:21:10 +0200 Subject: [PATCH 07/13] redirect js requests back to the root for dev mode --- .../DevServerStartup.cs | 31 ++++++++++++++----- 1 file changed, 24 insertions(+), 7 deletions(-) diff --git a/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs b/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs index f2588fd..cfced2d 100644 --- a/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs +++ b/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs @@ -73,12 +73,22 @@ private static string[] GetAcceptHeaders(HttpRequest contextRequest) ? header.MediaType : null); - private MidFunc SqlStreamStreamBrowserJavascript => (context, next) - => context.Request.Path.Value?.EndsWith(".js") ?? false - ? ForwardToClientDevServer( + private MidFunc SqlStreamStreamBrowserJavascript => (context, next) => + { + if(context.Request.Path.Value?.EndsWith(".js") ?? false) + { + var segments = context.Request.Path.ToUriComponent().Split('/'); + if(segments.Length > 2) + { + return RedirectToPathBase(context, $"/{segments.Last()}"); + } + + return ForwardToClientDevServer( context, - $"{context.Request.PathBase.ToUriComponent()}{context.Request.Path.ToUriComponent()}") - : next(); + context.Request.PathBase + context.Request.Path); + } + return next(); + }; private MidFunc SqlStreamStreamBrowserHtml => (context, next) => GetAcceptHeaders(context.Request) @@ -86,7 +96,14 @@ private static string[] GetAcceptHeaders(HttpRequest contextRequest) ? ForwardToClientDevServer(context, context.Request.PathBase.ToUriComponent()) : next(); - private async Task ForwardToClientDevServer(HttpContext context, string path) + private Task RedirectToPathBase(HttpContext context, PathString path) + { + context.Response.Redirect(context.Request.PathBase + path); + + return Task.CompletedTask; + } + + private async Task ForwardToClientDevServer(HttpContext context, PathString path) { using(var request = new HttpRequestMessage( new HttpMethod(context.Request.Method), @@ -94,7 +111,7 @@ private async Task ForwardToClientDevServer(HttpContext context, string path) { Port = 3000, Host = "localhost", - Path = path, + Path = path.ToUriComponent(), Query = context.Request.QueryString.ToUriComponent() }.Uri)) using(var response = await _httpClient.SendAsync(request)) From e04bca85e1af8d01028e2a897ff4559d68649ea3 Mon Sep 17 00:00:00 2001 From: thefringeninja Date: Sun, 1 Jul 2018 14:54:45 +0200 Subject: [PATCH 08/13] vary accept --- .../DevServerStartup.cs | 35 ++++++++++--------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs b/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs index cfced2d..0919391 100644 --- a/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs +++ b/src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs @@ -33,8 +33,8 @@ public IServiceProvider ConfigureServices(IServiceCollection services) => servic public void Configure(IApplicationBuilder app) => app .UseResponseCompression() + .Use(VaryAccept) .Use(CatchAndDisplayErrors) - .Use(AllowAllOrigins) .Use(SqlStreamStreamBrowserJavascript) .Use(SqlStreamStreamBrowserHtml) .UseSqlStreamStoreHal(_streamStore); @@ -51,28 +51,22 @@ public void Configure(IApplicationBuilder app) => app } }; - // don't actually do this in production - private static MidFunc AllowAllOrigins => (context, next) => + private static MidFunc VaryAccept => (context, next) => { - context.Response.OnStarting(_ => - { - var response = (HttpResponse) _; - response.Headers["Access-Control-Allow-Origin"] = "*"; + Task Vary(object state) + { + var response = (HttpResponse)state; - return Task.CompletedTask; - }, - context.Response); + response.Headers.AppendCommaSeparatedValues("Vary", "Accept"); + return Task.CompletedTask; + } + + context.Response.OnStarting(Vary, context.Response); + return next(); }; - private static string[] GetAcceptHeaders(HttpRequest contextRequest) - => Array.ConvertAll( - contextRequest.Headers.GetCommaSeparatedValues("Accept"), - value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header) - ? header.MediaType - : null); - private MidFunc SqlStreamStreamBrowserJavascript => (context, next) => { if(context.Request.Path.Value?.EndsWith(".js") ?? false) @@ -96,6 +90,13 @@ private static string[] GetAcceptHeaders(HttpRequest contextRequest) ? ForwardToClientDevServer(context, context.Request.PathBase.ToUriComponent()) : next(); + private static string[] GetAcceptHeaders(HttpRequest contextRequest) + => Array.ConvertAll( + contextRequest.Headers.GetCommaSeparatedValues("Accept"), + value => MediaTypeWithQualityHeaderValue.TryParse(value, out var header) + ? header.MediaType + : null); + private Task RedirectToPathBase(HttpContext context, PathString path) { context.Response.Redirect(context.Request.PathBase + path); From 24ee2720d850886305676eb44e5d357760e5e936 Mon Sep 17 00:00:00 2001 From: thefringeninja Date: Sun, 1 Jul 2018 14:55:08 +0200 Subject: [PATCH 09/13] custom field type for uuid --- src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json index 7771379..f933fe8 100644 --- a/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json +++ b/src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json @@ -9,7 +9,11 @@ "properties": { "messageId": { "type": "string", - "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$" + "pattern": "^[0-9A-Fa-f]{8}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{4}-[0-9A-Fa-f]{12}$", + "x-schema-form": { + "key": "messageId", + "type": "uuid" + } }, "type": { "type": "string" From ba1f46dc4408b858c31502ceedadf58073808134 Mon Sep 17 00:00:00 2001 From: thefringeninja Date: Sun, 1 Jul 2018 14:55:24 +0200 Subject: [PATCH 10/13] cleanup --- src/SqlStreamStore.HAL/Constants.cs | 12 ------------ .../ExceptionHandlingMiddleware.cs | 3 ++- src/SqlStreamStore.HAL/HttpContextExtensions.cs | 5 ----- 3 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/SqlStreamStore.HAL/Constants.cs b/src/SqlStreamStore.HAL/Constants.cs index 9a608d2..db007f6 100644 --- a/src/SqlStreamStore.HAL/Constants.cs +++ b/src/SqlStreamStore.HAL/Constants.cs @@ -40,18 +40,6 @@ public static class Streams public const string All = "stream"; public const string Metadata = "metadata"; } - - public static IReadOnlyDictionary ReasonPhrases { get; } - = new ReadOnlyDictionary(new Dictionary - { - [200] = "OK", - [201] = "Created", - [307] = "Moved Temporarily", - [400] = "Bad Request", - [404] = "Not Found", - [405] = "Method Not Allowed", - [409] = "Conflict" - }); public static class ReadDirection { diff --git a/src/SqlStreamStore.HAL/ExceptionHandlingMiddleware.cs b/src/SqlStreamStore.HAL/ExceptionHandlingMiddleware.cs index ecbae99..22e391e 100644 --- a/src/SqlStreamStore.HAL/ExceptionHandlingMiddleware.cs +++ b/src/SqlStreamStore.HAL/ExceptionHandlingMiddleware.cs @@ -39,7 +39,8 @@ private static readonly IDictionary> s_exception [typeof(InvalidAppendRequestException)] = ex => new Response(new HALResponse(new { type = ex.GetType().Name, - title = "Bad format." + title = "Bad format.", + detail = ex.Message }), 400), [typeof(Exception)] = s_defaultExceptionHandler }; diff --git a/src/SqlStreamStore.HAL/HttpContextExtensions.cs b/src/SqlStreamStore.HAL/HttpContextExtensions.cs index f547cf1..4ccc9e1 100644 --- a/src/SqlStreamStore.HAL/HttpContextExtensions.cs +++ b/src/SqlStreamStore.HAL/HttpContextExtensions.cs @@ -31,11 +31,6 @@ public static async Task WriteHalResponse(this HttpContext context, Response res context.Response.Headers.Append(header.Key, header.Value); } - if(context.IsDelete()) - { - return; - } - using(var writer = new JsonTextWriter(new StreamWriter(context.Response.Body)) { CloseOutput = false From 5ffdc7adc09ec7613971dc0ff8c7feefcfb5a7dd Mon Sep 17 00:00:00 2001 From: thefringeninja Date: Sun, 1 Jul 2018 23:43:33 +0200 Subject: [PATCH 11/13] escape hatch back to index from stream --- src/SqlStreamStore.HAL/Resources/StreamResource.cs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/SqlStreamStore.HAL/Resources/StreamResource.cs b/src/SqlStreamStore.HAL/Resources/StreamResource.cs index fbd7cee..4bf1dc2 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamResource.cs @@ -75,6 +75,7 @@ public async Task GetPage(ReadStreamOperation operation, CancellationT .AddLinks(Links.Navigation(page, operation)) .AddLinks(Links.Feed(operation)) .AddLinks(Links.Metadata(operation)) + .AddLinks(Links.Index()) .AddEmbeddedResource( Constants.Relations.AppendToStream, Schemas.AppendToStream) @@ -181,6 +182,11 @@ public static Link Metadata(ReadStreamOperation operation) Constants.Relations.Metadata, $"{operation.StreamId}/metadata"); + public static Link Index() + => new Link( + Constants.Relations.Index, + ".."); + public static IEnumerable Navigation(ReadStreamPage page, ReadStreamOperation operation) { var first = First(page, operation); From f7249f447a1ef69a7936be42efbc601be2636004 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Fri, 29 Jun 2018 16:53:08 +0200 Subject: [PATCH 12/13] fixin metadata links --- .../StreamMetadataTests.cs | 9 ++++-- .../Resources/Schema/SetStreamMetadata.json | 1 + .../Resources/StreamMetadataResource.cs | 28 +++++++++++++------ 3 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/SqlStreamStore.HAL.Tests/StreamMetadataTests.cs b/src/SqlStreamStore.HAL.Tests/StreamMetadataTests.cs index 05a98c4..fd9a1e0 100644 --- a/src/SqlStreamStore.HAL.Tests/StreamMetadataTests.cs +++ b/src/SqlStreamStore.HAL.Tests/StreamMetadataTests.cs @@ -40,7 +40,8 @@ await _fixture.HttpClient.SendAsync( ((string) resource.State.metadataJson).ShouldBeNull(); resource.ShouldLink(Constants.Relations.Self, "metadata"); - resource.ShouldLink(Constants.Relations.Feed, "../"); + resource.ShouldLink(Constants.Relations.Metadata, "metadata"); + resource.ShouldLink(Constants.Relations.Feed, $"../{StreamId}"); } } @@ -85,7 +86,8 @@ await _fixture.HttpClient.SendAsync( })).ShouldBeTrue(); resource.ShouldLink(Constants.Relations.Self, "metadata"); - resource.ShouldLink(Constants.Relations.Feed, "../"); + resource.ShouldLink(Constants.Relations.Metadata, "metadata"); + resource.ShouldLink(Constants.Relations.Feed, $"../{StreamId}"); } } @@ -126,7 +128,8 @@ public async Task set_metadata() })).ShouldBeTrue(); resource.ShouldLink(Constants.Relations.Self, "metadata"); - resource.ShouldLink(Constants.Relations.Feed, "../"); + resource.ShouldLink(Constants.Relations.Metadata, "metadata"); + resource.ShouldLink(Constants.Relations.Feed, $"../{StreamId}"); } } diff --git a/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json b/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json index 0811622..5bb72ae 100644 --- a/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json +++ b/src/SqlStreamStore.HAL/Resources/Schema/SetStreamMetadata.json @@ -1,4 +1,5 @@ { + "title": "Set Stream Metadata", "type": "object", "$schema": "http://json-schema.org/draft-07/hyper-schema#", "properties": { diff --git a/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs b/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs index f300ae1..a4871d6 100644 --- a/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs +++ b/src/SqlStreamStore.HAL/Resources/StreamMetadataResource.cs @@ -8,13 +8,6 @@ namespace SqlStreamStore.HAL.Resources internal class StreamMetadataResource : IResource { - private static readonly Link[] s_links = - { - new Link(Constants.Relations.Self, Constants.Streams.Metadata), - new Link(Constants.Relations.Metadata, Constants.Streams.Metadata), - new Link(Constants.Relations.Feed, "../") - }; - private readonly IStreamStore _streamStore; public HttpMethod[] Options { get; } = @@ -46,7 +39,10 @@ public async Task GetStreamMetadata( result.MaxCount, result.MetadataJson }) - .AddLinks(s_links) + .AddLinks( + Links.Self(), + Links.Metadata(), + Links.Feed(operation)) .AddEmbeddedResource( Constants.Relations.Metadata, Schemas.SetStreamMetadata), @@ -68,9 +64,23 @@ public async Task SetStreamMetadata( operation.MaxCount, operation.MetadataJson }) - .AddLinks(s_links)); + .AddLinks( + Links.Self(), + Links.Metadata(), + Links.Feed(operation))); return response; } + + private static class Links + { + public static Link Self() => new Link(Constants.Relations.Self, Constants.Streams.Metadata); + public static Link Metadata() => new Link(Constants.Relations.Metadata, Constants.Streams.Metadata); + public static Link Feed(GetStreamMetadataOperation operation) => Link(operation.StreamId); + public static Link Feed(SetStreamMetadataOperation operation) => Link(operation.StreamId); + + private static Link Link(string streamId) + => new Link(Constants.Relations.Feed, $"../{streamId}"); + } } } \ No newline at end of file From 3482c1dfcbd8c309c02d83bdf8b9823cf6f3f416 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Bragan=C3=A7a?= Date: Mon, 2 Jul 2018 12:58:26 +0200 Subject: [PATCH 13/13] rc2 --- src/Directory.Build.props | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Directory.Build.props b/src/Directory.Build.props index 57681dd..b268719 100644 --- a/src/Directory.Build.props +++ b/src/Directory.Build.props @@ -1,7 +1,7 @@ - 1.0.0-rc1 + 1.0.0-rc2 João P. Bragança https://github.com/damianh/SqlStreamStore.HAL https://github.com/damianh/SqlStreamStore.HAL/blob/master/LICENSE