Skip to content

Commit b2666d6

Browse files
authored
Merge pull request #2339 from Cratis:fix/projections-related-things
Fix/projections-related-things
2 parents 3fd2dea + 290b9db commit b2666d6

24 files changed

+477
-70
lines changed
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Cratis.Chronicle.Properties.for_PropertyPath;
5+
6+
public class when_property_path_has_value : Specification
7+
{
8+
PropertyPath _result;
9+
10+
void Because() => _result = "some.property.path";
11+
12+
[Fact] void should_be_considered_set() => _result.IsSet.ShouldBeTrue();
13+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
namespace Cratis.Chronicle.Properties.for_PropertyPath;
5+
6+
public class when_property_path_is_empty_string : Specification
7+
{
8+
PropertyPath _result;
9+
10+
void Because() => _result = string.Empty;
11+
12+
[Fact] void should_be_considered_not_set() => _result.IsSet.ShouldBeFalse();
13+
}

Source/Infrastructure/Properties/PropertyPath.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ public PropertyPath(string path)
7878
/// <summary>
7979
/// Gets whether or not the value is set.
8080
/// </summary>
81-
public bool IsSet => Path?.Equals(NotSetValue) == false;
81+
public bool IsSet => !string.IsNullOrEmpty(Path) && !Path.Equals(NotSetValue);
8282

8383
static Regex ArrayIndexRegex => _arrayIndexRegex ??= ArrayIndexRegexGenerator();
8484

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Cratis.Chronicle.Concepts.Events;
5+
using Cratis.Chronicle.Concepts.Identities;
6+
using Cratis.Chronicle.Concepts.Keys;
7+
using Cratis.Chronicle.Concepts.Projections;
8+
using Cratis.Chronicle.Dynamic;
9+
using Cratis.Chronicle.Properties;
10+
using Cratis.Chronicle.Storage.EventSequences;
11+
using Cratis.Chronicle.Storage.Sinks;
12+
using Cratis.Monads;
13+
using Microsoft.Extensions.Logging.Abstractions;
14+
15+
namespace Cratis.Chronicle.Projections.for_KeyResolvers;
16+
17+
public class when_identifying_read_model_key_from_parent_hierarchy_with_empty_string_children_property_path : Specification
18+
{
19+
AppendedEvent _childEvent;
20+
Key _result;
21+
IProjection _parentProjection;
22+
IProjection _childProjection;
23+
IEventSequenceStorage _storage;
24+
ISink _sink;
25+
KeyResolvers _keyResolvers;
26+
27+
static readonly EventType _parentEventType = new("5f4f4368-6989-4d9d-a84e-7393e0b41cfd", 1);
28+
static readonly EventType _childEventType = new("02405794-91e7-4e4f-8ad1-f043070ca297", 1);
29+
const string RootKey = "root-key-123";
30+
const string ParentKey = "parent-key-456";
31+
const string ChildKey = "child-key-789";
32+
const string ChildEventSourceId = "different-event-source-id";
33+
34+
PropertyPath _queriedChildPropertyPath;
35+
36+
void Establish()
37+
{
38+
_keyResolvers = new KeyResolvers(NullLogger<KeyResolvers>.Instance);
39+
40+
_childEvent = new(
41+
new(
42+
_childEventType,
43+
EventSourceType.Default,
44+
ChildEventSourceId,
45+
EventStreamType.All,
46+
EventStreamId.Default,
47+
1,
48+
DateTimeOffset.UtcNow,
49+
"123b8935-a1a4-410d-aace-e340d48f0aa0",
50+
"41f18595-4748-4b01-88f7-4c0d0907aa90",
51+
CorrelationId.New(),
52+
[],
53+
Identity.System),
54+
new
55+
{
56+
parentId = ParentKey,
57+
childId = ChildKey,
58+
name = "Test Child"
59+
}.AsExpandoObject());
60+
61+
_parentProjection = Substitute.For<IProjection>();
62+
_parentProjection.EventTypes.Returns([_parentEventType]);
63+
_parentProjection.OwnEventTypes.Returns([_parentEventType]);
64+
_parentProjection.IdentifiedByProperty.Returns((PropertyPath)"id");
65+
_parentProjection.Path.Returns((ProjectionPath)"configurations");
66+
67+
// Set ChildrenPropertyPath to empty string instead of NotSet
68+
_parentProjection.ChildrenPropertyPath.Returns((PropertyPath)string.Empty);
69+
_parentProjection.HasParent.Returns(false);
70+
_parentProjection.Parent.Returns((IProjection)null!);
71+
72+
_childProjection = Substitute.For<IProjection>();
73+
_childProjection.HasParent.Returns(true);
74+
_childProjection.Parent.Returns(_parentProjection);
75+
_childProjection.ChildrenPropertyPath.Returns((PropertyPath)"hubs");
76+
_childProjection.IdentifiedByProperty.Returns((PropertyPath)"hubId");
77+
_childProjection.Path.Returns((ProjectionPath)"configurations -> hubs");
78+
79+
_storage = Substitute.For<IEventSequenceStorage>();
80+
_sink = Substitute.For<ISink>();
81+
82+
// Parent event is NOT found by EventSourceId (returns empty)
83+
// This forces the code to use sink lookup
84+
_storage.TryGetLastInstanceOfAny(ParentKey, Arg.Any<IEnumerable<EventTypeId>>())
85+
.Returns(Option<AppendedEvent>.None());
86+
87+
// Sink lookup returns the root key
88+
// Capture the property path used in the query to verify it's correct
89+
_sink.When(x => x.TryFindRootKeyByChildValue(Arg.Any<PropertyPath>(), ParentKey))
90+
.Do(callInfo => _queriedChildPropertyPath = callInfo.ArgAt<PropertyPath>(0));
91+
92+
_sink.TryFindRootKeyByChildValue(Arg.Any<PropertyPath>(), ParentKey)
93+
.Returns(new Option<Key>(new Key(RootKey, ArrayIndexers.NoIndexers)));
94+
95+
_sink.TryFindRootKeyByChildValue(Arg.Any<PropertyPath>(), RootKey)
96+
.Returns(Option<Key>.None());
97+
}
98+
99+
async Task Because()
100+
{
101+
try
102+
{
103+
var keyResult = await _keyResolvers.FromParentHierarchy(
104+
_childProjection,
105+
_keyResolvers.FromEventValueProvider(EventValueProviders.EventContent("childId")),
106+
_keyResolvers.FromEventValueProvider(EventValueProviders.EventContent("parentId")),
107+
"hubId")(_storage, _sink, _childEvent);
108+
_result = (keyResult as ResolvedKey)?.Key;
109+
}
110+
catch
111+
{
112+
// Ignore exceptions - we're only testing that the sink was called with the correct path
113+
}
114+
}
115+
116+
[Fact] void should_treat_empty_string_as_not_set_and_query_sink_with_child_projection_children_property_path() =>
117+
_queriedChildPropertyPath.ShouldEqual((PropertyPath)"hubs.id");
118+
119+
[Fact] void should_not_incorrectly_use_parent_empty_string_as_prefix() =>
120+
_queriedChildPropertyPath.ShouldNotEqual((PropertyPath)".id");
121+
}

Source/Kernel/Projections/Pipelines/Steps/ResolveKey.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,12 @@ public async ValueTask<ProjectionEventContext> Perform(EngineProjection projecti
2626
{
2727
logger.ResolvingKey(context.Event.Context.SequenceNumber);
2828
var keyResolver = projection.GetKeyResolverFor(context.Event.Context.EventType);
29+
30+
if (context.EventType.Id == "SimulationRunEnded")
31+
{
32+
Console.WriteLine("Debug");
33+
}
34+
2935
var keyResult = await keyResolver(eventSequenceStorage, sink, context.Event);
3036

3137
// Handle deferred key resolution - this means the parent wasn't found in Sink yet
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using MongoDB.Bson;
5+
6+
namespace Cratis.Chronicle.Storage.MongoDB.for_BsonValueExtensions;
7+
8+
public class when_converting_time_span_to_bson_value : Specification
9+
{
10+
TimeSpan _timeSpan;
11+
BsonValue _result;
12+
13+
void Establish() => _timeSpan = new TimeSpan(1, 2, 3, 4, 5);
14+
15+
void Because() => _result = _timeSpan.ToBsonValue();
16+
17+
[Fact] void should_return_bson_string() => _result.ShouldBeOfExactType<BsonString>();
18+
[Fact] void should_store_time_span_as_string() => _result.AsString.ShouldEqual(_timeSpan.ToString());
19+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using MongoDB.Bson;
5+
6+
namespace Cratis.Chronicle.Storage.MongoDB.for_BsonValueExtensions.when_converting_to_time_span;
7+
8+
public class from_bson_string : Specification
9+
{
10+
TimeSpan _expected;
11+
BsonValue _bsonValue;
12+
object? _result;
13+
14+
void Establish()
15+
{
16+
_expected = new TimeSpan(1, 2, 3, 4, 5);
17+
_bsonValue = new BsonString(_expected.ToString());
18+
}
19+
20+
void Because() => _result = _bsonValue.ToTargetType(typeof(TimeSpan));
21+
22+
[Fact] void should_return_time_span() => _result.ShouldBeOfExactType<TimeSpan>();
23+
[Fact] void should_have_correct_value() => ((TimeSpan)_result!).ShouldEqual(_expected);
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Cratis.Chronicle.Properties;
5+
6+
namespace Cratis.Chronicle.Storage.MongoDB.for_PropertyExtensions.when_converting_to_mongo_db;
7+
8+
public class with_array_and_child_property : Specification
9+
{
10+
PropertyPath _propertyPath;
11+
string _result;
12+
13+
void Establish() => _propertyPath = new PropertyPath("[Configurations].ConfigurationId");
14+
15+
void Because() => _result = _propertyPath.ToMongoDB();
16+
17+
[Fact] void should_remove_brackets_and_preserve_casing() => _result.ShouldEqual("Configurations.ConfigurationId");
18+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Cratis.Chronicle.Properties;
5+
6+
namespace Cratis.Chronicle.Storage.MongoDB.for_PropertyExtensions.when_converting_to_mongo_db;
7+
8+
public class with_array_and_id_child_property : Specification
9+
{
10+
PropertyPath _propertyPath;
11+
string _result;
12+
13+
void Establish() => _propertyPath = new PropertyPath("[Configurations].Id");
14+
15+
void Because() => _result = _propertyPath.ToMongoDB();
16+
17+
[Fact] void should_convert_id_to_underscore_id() => _result.ShouldEqual("Configurations._id");
18+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) Cratis. All rights reserved.
2+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
3+
4+
using Cratis.Chronicle.Properties;
5+
6+
namespace Cratis.Chronicle.Storage.MongoDB.for_PropertyExtensions.when_converting_to_mongo_db;
7+
8+
public class with_array_property : Specification
9+
{
10+
PropertyPath _propertyPath;
11+
string _result;
12+
13+
void Establish() => _propertyPath = new PropertyPath("[Configurations]");
14+
15+
void Because() => _result = _propertyPath.ToMongoDB();
16+
17+
[Fact] void should_remove_brackets_and_preserve_casing() => _result.ShouldEqual("Configurations");
18+
}

0 commit comments

Comments
 (0)