Skip to content
This repository was archived by the owner on Sep 3, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/Directory.Build.props
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<Project>
<PropertyGroup>
<VersionPrefix>1.0.0-rc1</VersionPrefix>
<VersionPrefix>1.0.0-rc2</VersionPrefix>
<Authors>João P. Bragança</Authors>
<PackageProjectUrl>https://github.com/damianh/SqlStreamStore.HAL</PackageProjectUrl>
<PackageLicenseUrl>https://github.com/damianh/SqlStreamStore.HAL/blob/master/LICENSE</PackageLicenseUrl>
Expand Down
90 changes: 80 additions & 10 deletions src/SqlStreamStore.HAL.DevServer/DevServerStartup.cs
Original file line number Diff line number Diff line change
@@ -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<System.Threading.Tasks.Task>,
Expand All @@ -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
Expand All @@ -27,8 +33,10 @@ 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);

private static MidFunc CatchAndDisplayErrors => async (context, next) =>
Expand All @@ -43,19 +51,81 @@ 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;

response.Headers.AppendCommaSeparatedValues("Vary", "Accept");

return Task.CompletedTask;
}

context.Response.OnStarting(Vary, context.Response);

return next();
};

return Task.CompletedTask;
},
context.Response);
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 + context.Request.Path);
}
return next();
};

private MidFunc SqlStreamStreamBrowserHtml => (context, next)
=> GetAcceptHeaders(context.Request)
.Any(header => header == "text/html")
? 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);

return Task.CompletedTask;
}

private async Task ForwardToClientDevServer(HttpContext context, PathString path)
{
using(var request = new HttpRequestMessage(
new HttpMethod(context.Request.Method),
new UriBuilder
{
Port = 3000,
Host = "localhost",
Path = path.ToUriComponent(),
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);
}
}
}
}
9 changes: 6 additions & 3 deletions src/SqlStreamStore.HAL.Tests/StreamMetadataTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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}");
}
}

Expand Down Expand Up @@ -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}");
}
}

Expand Down Expand Up @@ -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}");
}
}

Expand Down
14 changes: 2 additions & 12 deletions src/SqlStreamStore.HAL/Constants.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,25 +31,15 @@ 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 const string Delete = "streamStore:delete";
}

public static class Streams
{
public const string All = "stream";
public const string Metadata = "metadata";
}

public static IReadOnlyDictionary<int, string> ReasonPhrases { get; }
= new ReadOnlyDictionary<int, string>(new Dictionary<int, string>
{
[200] = "OK",
[201] = "Created",
[307] = "Moved Temporarily",
[400] = "Bad Request",
[404] = "Not Found",
[405] = "Method Not Allowed",
[409] = "Conflict"
});

public static class ReadDirection
{
Expand Down
3 changes: 2 additions & 1 deletion src/SqlStreamStore.HAL/ExceptionHandlingMiddleware.cs
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,8 @@ private static readonly IDictionary<Type, Func<Exception, Response>> 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
};
Expand Down
13 changes: 12 additions & 1 deletion src/SqlStreamStore.HAL/HttpContextExtensions.cs
Original file line number Diff line number Diff line change
@@ -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
{
Expand Down Expand Up @@ -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);
}
}
8 changes: 7 additions & 1 deletion src/SqlStreamStore.HAL/Resources/AllStreamResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,9 @@ public async Task<Response> GetPage(
payload,
metadata = message.JsonMetadata
})
.AddLinks(Links.Message.Self(message)))));
.AddLinks(
Links.Message.Self(message),
Links.Message.Feed(message)))));

if(operation.FromPositionInclusive == Position.End)
{
Expand Down Expand Up @@ -157,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}");
}
}
}
Expand Down
32 changes: 21 additions & 11 deletions src/SqlStreamStore.HAL/Resources/AppendStreamOperation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>("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<string>("type");

if(type == null)
{
throw new InvalidAppendRequestException($"'{nameof(type)}' at index {index} was not set.");
}

return new NewStreamMessage(
messageId,
type,
newStreamMessage.Value<JToken>("jsonData").ToString(),
newStreamMessage.Value<JToken>("jsonMetadata")?.ToString());

return new NewStreamMessageDto
{
MessageId = messageId,
Type = type,
JsonData = newStreamMessage.Value<JToken>("jsonData"),
JsonMetadata = newStreamMessage.Value<JToken>("jsonMetadata")
};
}

public string StreamId { get; }
public int ExpectedVersion { get; }
public NewStreamMessage[] NewStreamMessages { get; }
public NewStreamMessageDto[] NewStreamMessages { get; }

public Task<AppendResult> Invoke(IStreamStore streamStore, CancellationToken ct)
=> streamStore.AppendToStream(StreamId, ExpectedVersion, NewStreamMessages, ct);
public Task<AppendResult> Invoke(IStreamStore streamStore, CancellationToken ct)
=> streamStore.AppendToStream(
StreamId,
ExpectedVersion,
Array.ConvertAll(NewStreamMessages, dto => dto.ToNewStreamMessage()),
ct);
}
}
17 changes: 17 additions & 0 deletions src/SqlStreamStore.HAL/Resources/NewStreamMessageDto.cs
Original file line number Diff line number Diff line change
@@ -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());
}
}
38 changes: 38 additions & 0 deletions src/SqlStreamStore.HAL/Resources/Schema/AppendToStream.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
{
"$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}$",
"x-schema-form": {
"key": "messageId",
"type": "uuid"
}
},
"type": {
"type": "string"
},
"jsonData": {
"type": "object",
"x-schema-form": {
"key": "jsonData",
"type": "textarea",
"rows": 30
}
},
"jsonMetadata": {
"type": "string",
"x-schema-form": {
"key": "jsonMetadata",
"type": "textarea",
"rows": 30
}
}
}
}
5 changes: 5 additions & 0 deletions src/SqlStreamStore.HAL/Resources/Schema/DeleteStream.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/hyper-schema#",
"title": "Delete Stream",
"type": "object"
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"$schema": "http://json-schema.org/draft-07/hyper-schema#",
"title": "Delete Stream Message",
"type": "object"
}
Loading