@cratis/chronicle 6.44.0 → 6.46.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/projections/declarative/CompositeKeyBuilder.d.ts +4 -1
  2. package/dist/projections/declarative/CompositeKeyBuilder.d.ts.map +1 -1
  3. package/dist/projections/declarative/CompositeKeyBuilder.js +15 -8
  4. package/dist/projections/declarative/CompositeKeyBuilder.js.map +1 -1
  5. package/dist/projections/declarative/ICompositeKeyBuilder.d.ts +8 -1
  6. package/dist/projections/declarative/ICompositeKeyBuilder.d.ts.map +1 -1
  7. package/dist/projections/declarative/ICompositeKeySetBuilder.d.ts +17 -0
  8. package/dist/projections/declarative/ICompositeKeySetBuilder.d.ts.map +1 -0
  9. package/dist/projections/declarative/ICompositeKeySetBuilder.js +4 -0
  10. package/dist/projections/declarative/ICompositeKeySetBuilder.js.map +1 -0
  11. package/dist/projections/declarative/index.d.ts +1 -0
  12. package/dist/projections/declarative/index.d.ts.map +1 -1
  13. package/dist/reactors/ReactorDelivery.d.ts +23 -0
  14. package/dist/reactors/ReactorDelivery.d.ts.map +1 -0
  15. package/dist/reactors/ReactorDelivery.js +36 -0
  16. package/dist/reactors/ReactorDelivery.js.map +1 -0
  17. package/dist/reactors/ReactorDispatcher.d.ts +2 -1
  18. package/dist/reactors/ReactorDispatcher.d.ts.map +1 -1
  19. package/dist/reactors/ReactorDispatcher.js +2 -2
  20. package/dist/reactors/ReactorDispatcher.js.map +1 -1
  21. package/dist/reactors/Reactors.d.ts.map +1 -1
  22. package/dist/reactors/Reactors.js +2 -1
  23. package/dist/reactors/Reactors.js.map +1 -1
  24. package/dist/reactors/for_Reactors/given/deliver_to_reactor.d.ts +6 -0
  25. package/dist/reactors/for_Reactors/given/deliver_to_reactor.d.ts.map +1 -0
  26. package/dist/reactors/for_Reactors/given/deliver_to_reactor.js +37 -0
  27. package/dist/reactors/for_Reactors/given/deliver_to_reactor.js.map +1 -0
  28. package/dist/reactors/index.d.ts +1 -0
  29. package/dist/reactors/index.d.ts.map +1 -1
  30. package/dist/reactors/index.js +1 -0
  31. package/dist/reactors/index.js.map +1 -1
  32. package/dist/testing/ReactorScenario.d.ts.map +1 -1
  33. package/dist/testing/ReactorScenario.js +2 -1
  34. package/dist/testing/ReactorScenario.js.map +1 -1
  35. package/dist/tsconfig.tsbuildinfo +1 -1
  36. package/package.json +2 -2
  37. package/projections/declarative/CompositeKeyBuilder.ts +26 -9
  38. package/projections/declarative/ICompositeKeyBuilder.ts +9 -1
  39. package/projections/declarative/ICompositeKeySetBuilder.ts +21 -0
  40. package/projections/declarative/for_CompositeKeyBuilder/when_matching_kernel_fixtures.spec.ts +108 -0
  41. package/projections/declarative/for_CompositeKeyBuilder/when_setting_a_duplicate_part.spec.ts +43 -0
  42. package/projections/declarative/for_CompositeKeyBuilder/when_setting_expression_parts.spec.ts +84 -0
  43. package/projections/declarative/for_CompositeKeyBuilder/when_typing_constant_parts.standard.spec.ts +43 -0
  44. package/projections/declarative/index.ts +1 -0
  45. package/reactors/ReactorDelivery.ts +32 -0
  46. package/reactors/ReactorDispatcher.ts +3 -2
  47. package/reactors/Reactors.ts +5 -1
  48. package/reactors/for_ReactorDelivery/when_comparing_deliveries.spec.ts +27 -0
  49. package/reactors/for_ReactorDelivery/when_rendering_a_receipt_key.spec.ts +33 -0
  50. package/reactors/for_Reactors/given/deliver_to_reactor.ts +47 -0
  51. package/reactors/for_Reactors/when_delivering_a_stable_identity.spec.ts +66 -0
  52. package/reactors/index.ts +1 -0
  53. package/testing/ReactorScenario.ts +5 -1
  54. package/testing/for_ReactorScenario/when_receiving_a_delivery_identity.spec.ts +68 -0
  55. package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +5 -0
  56. package/testing/projections/for_ProjectionCapabilities/when_rejecting_composite_expression_parts.spec.ts +40 -0
@@ -0,0 +1,43 @@
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
+ import { chai, describe, it } from 'vitest';
5
+ import { CompositeKeyBuilder, type ICompositeKeyBuilder } from '../index.js';
6
+
7
+ chai.should();
8
+
9
+ class Key {
10
+ number!: number;
11
+ text!: string;
12
+ nullable!: string | null;
13
+ sourceNumber!: number;
14
+ }
15
+ class Changed { number!: number; }
16
+
17
+ // Checked by test:standard-types. Do not execute invalid configurations at runtime.
18
+ function rejectMismatchedConstants(builder: ICompositeKeyBuilder<Key, Changed>): void {
19
+ // @ts-expect-error A number key part cannot receive a string constant.
20
+ builder.set(key => key.number).toValue('orders');
21
+ // @ts-expect-error A string key part cannot receive a number constant.
22
+ builder.set(key => key.text).toValue(42);
23
+ // @ts-expect-error Null requires a nullable key part.
24
+ builder.set(key => key.text).toValue(null);
25
+ }
26
+
27
+ function rejectOnConcreteBuilder(builder: CompositeKeyBuilder<Key, Changed>): void {
28
+ // @ts-expect-error The concrete overload must not fall back to an untyped accessor.
29
+ builder.set(key => key.number).toValue('orders');
30
+ }
31
+ void rejectMismatchedConstants;
32
+ void rejectOnConcreteBuilder;
33
+
34
+ describe('when typing constant composite parts', () => {
35
+ it('should accept selected property types and preserve fluent chaining', () => {
36
+ const builder: ICompositeKeyBuilder<Key, Changed> = new CompositeKeyBuilder<Key, Changed>();
37
+ builder.set(key => key.number).toValue(42)
38
+ .set(key => key.text).toValue('orders')
39
+ .set(key => key.nullable).toValue(null)
40
+ .set(key => key.sourceNumber, event => event.number);
41
+ builder.build().should.equal('$composite(number=$value(42),text=$value(orders),nullable=$null,sourceNumber=number)');
42
+ });
43
+ });
@@ -26,6 +26,7 @@ export type { IJoinBuilder } from './IJoinBuilder.js';
26
26
  export type { IFromEveryBuilder } from './IFromEveryBuilder.js';
27
27
  export type { IReadModelPropertiesBuilder } from './IReadModelPropertiesBuilder.js';
28
28
  export type { ICompositeKeyBuilder } from './ICompositeKeyBuilder.js';
29
+ export type { ICompositeKeySetBuilder } from './ICompositeKeySetBuilder.js';
29
30
  export type { ISetBuilder } from './ISetBuilder.js';
30
31
  export type { IAllSetBuilder } from './IAllSetBuilder.js';
31
32
  export type { IAddBuilder } from './IAddBuilder.js';
@@ -0,0 +1,32 @@
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
+ /**
5
+ * Identifies one event delivered to one reactor partition. Handlers receive this as their
6
+ * fourth argument, after the event, EventContext and ReactorServices.
7
+ *
8
+ * The identity is stable across retries and replay. It is not exactly-once delivery:
9
+ * Chronicle stores no receipt and does not know whether an application's side effect completed.
10
+ */
11
+ export class ReactorDelivery {
12
+ constructor(
13
+ readonly reactor: string,
14
+ readonly eventStore: string,
15
+ readonly namespace: string,
16
+ readonly eventSequence: string,
17
+ readonly partition: string,
18
+ readonly sequenceNumber: bigint
19
+ ) {
20
+ Object.freeze(this);
21
+ }
22
+
23
+ /**
24
+ * Application-owned receipt key, matching .NET's ReactorDelivery.Id.Value format.
25
+ * Components are joined with '#', without escaping or hashing. Persisted keys depend
26
+ * on stable reactor, store, namespace and event-sequence identifiers.
27
+ */
28
+ get id(): string {
29
+ return [this.reactor, this.eventStore, this.namespace, this.eventSequence,
30
+ this.partition, this.sequenceNumber.toString()].join('#');
31
+ }
32
+ }
@@ -14,6 +14,7 @@ import { getInstanceMethods } from '../observation/getInstanceMethods.js';
14
14
  import { isOnceOnly } from './onceOnly.js';
15
15
  import { getReplayEventType } from './replay.js';
16
16
  import type { ReactorServices } from './ReactorServices.js';
17
+ import type { ReactorDelivery } from './ReactorDelivery.js';
17
18
 
18
19
  /** Internal handler catalog shared by live observations and in-process deliveries. */
19
20
  export interface ReactorEventTypeEntry {
@@ -83,9 +84,9 @@ export function selectReactorHandler(entries: readonly ReactorEventTypeEntry[],
83
84
  /** Handler and returned effects execute in the same production activation boundary. */
84
85
  export async function invokeReactorHandler(artifact: ActivatedArtifact<Record<string, Function>>, methodName: string,
85
86
  content: Record<string, unknown>, context: EventContext, services: ReactorServices | undefined,
86
- handleResult: (value: unknown) => Promise<void>): Promise<void> {
87
+ handleResult: (value: unknown) => Promise<void>, delivery: ReactorDelivery): Promise<void> {
87
88
  await runActivated(artifact, async () => {
88
- const result = await artifact.instance[methodName](content, context, services);
89
+ const result = await artifact.instance[methodName](content, context, services, delivery);
89
90
  await handleResult(result);
90
91
  }, { delivery: ArtifactDelivery.Events, eventContext: context, methodName });
91
92
  }
@@ -24,6 +24,7 @@ import { getReactorEventTypes, selectReactorHandler, invokeReactorHandler } from
24
24
  import type { ReactorEventTypeEntry } from './ReactorDispatcher.js';
25
25
  import type { ReactorResultHandler } from './ReactorResultHandler.js';
26
26
  import type { ReactorServices } from './ReactorServices.js';
27
+ import { ReactorDelivery } from './ReactorDelivery.js';
27
28
  import type { IEventStore } from '../IEventStore.js';
28
29
  import type { ClientArtifactsActivator } from '../artifacts/ClientArtifactsActivator.js';
29
30
  import type { ActivatedArtifact } from '../artifacts/ActivatedArtifact.js';
@@ -375,7 +376,10 @@ export class Reactors implements IReactors {
375
376
 
376
377
  await invokeReactorHandler(artifact, methodName, content, context, services, result =>
377
378
  dispatchReactorSideEffects(this._eventLog, result, context, reactorType as Function,
378
- this._eventStoreName, this._namespace, this._resultHandler));
379
+ this._eventStoreName, this._namespace, this._resultHandler),
380
+ new ReactorDelivery(id, context.eventStore ?? this._eventStoreName,
381
+ context.namespace ?? this._namespace, eventSequenceId,
382
+ context.eventSourceId, context.sequenceNumber));
379
383
 
380
384
  lastSuccessfullyObservedEvent = event.Context!.SequenceNumber;
381
385
  } catch (err) {
@@ -0,0 +1,27 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { ReactorDelivery } from '../index.js';
6
+
7
+ chai.should();
8
+
9
+ const components = ['order-confirmations', 'orders', 'default', 'event-log', 'order-42', 7n] as const;
10
+ for (const [name, replacement] of [
11
+ ['reactor', 'shipment-notifications'], ['event store', 'billing'], ['namespace', 'another-tenant'],
12
+ ['event sequence', 'inbox-upstream'], ['partition', 'order-43'], ['sequence number', 8n]
13
+ ] as const) {
14
+ describe(`when comparing deliveries differing only in ${name}`, () => {
15
+ let original: ReactorDelivery;
16
+ let different: ReactorDelivery;
17
+ beforeEach(() => {
18
+ original = new ReactorDelivery(...components);
19
+ const changed: [string, string, string, string, string, bigint] = [...components];
20
+ const index = ['reactor', 'event store', 'namespace', 'event sequence', 'partition', 'sequence number'].indexOf(name);
21
+ if (typeof replacement === 'bigint') changed[5] = replacement;
22
+ else changed[index as 0 | 1 | 2 | 3 | 4] = replacement;
23
+ different = new ReactorDelivery(...changed);
24
+ });
25
+ it('should produce a distinct receipt key', () => { different.id.should.not.equal(original.id); });
26
+ });
27
+ }
@@ -0,0 +1,33 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { ReactorDelivery } from '../index.js';
6
+
7
+ chai.should();
8
+
9
+ describe('when rendering a delivery as an application receipt key', () => {
10
+ let delivery: ReactorDelivery;
11
+ beforeEach(() => {
12
+ delivery = new ReactorDelivery('Cratis.Chronicle.Reactors.for_ReactorDelivery.OrderConfirmations',
13
+ 'orders', 'default', 'event-log', 'order-42', 7n);
14
+ });
15
+ it('should match the .NET v19.26.2 receipt format', () => {
16
+ delivery.id.should.equal('Cratis.Chronicle.Reactors.for_ReactorDelivery.OrderConfirmations#orders#default#event-log#order-42#7');
17
+ });
18
+ it('should render the same key for equal components in a new instance', () => {
19
+ new ReactorDelivery(delivery.reactor, delivery.eventStore, delivery.namespace,
20
+ delivery.eventSequence, delivery.partition, delivery.sequenceNumber).id.should.equal(delivery.id);
21
+ });
22
+ it('should preserve sequence numbers above the JavaScript safe integer range', () => {
23
+ new ReactorDelivery('orders', 'store', 'tenant', 'event-log', 'order-42', 18446744073709551615n)
24
+ .id.should.equal('orders#store#tenant#event-log#order-42#18446744073709551615');
25
+ });
26
+ it('should preserve .NET delimiter behavior without escaping or hashing', () => {
27
+ new ReactorDelivery('orders', '', 'tenant#one', 'event-log', 'order-42', 0n)
28
+ .id.should.equal('orders##tenant#one#event-log#order-42#0');
29
+ });
30
+ it('should keep the identity immutable during handling', () => {
31
+ Object.isFrozen(delivery).should.be.true;
32
+ });
33
+ });
@@ -0,0 +1,47 @@
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
+ import type { Constructor } from '@cratis/fundamentals';
5
+ import { ReplayState, type EventContext, type ReactorMessage } from '@cratis/chronicle.contracts';
6
+ import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
7
+ import type { ClientArtifactsActivator } from '../../../artifacts/ClientArtifactsActivator.js';
8
+ import type { ChronicleConnection } from '../../../connection/index.js';
9
+ import { ConnectionLifecycle } from '../../../connection/ConnectionLifecycle.js';
10
+ import type { IEventLog } from '../../../eventSequences/IEventLog.js';
11
+ import type { IEventStore } from '../../../IEventStore.js';
12
+ import { Reactors } from '../../index.js';
13
+
14
+ /** Deliver through the production observation stream without a kernel. */
15
+ export async function deliverToReactor(reactorType: Constructor, eventType: Constructor,
16
+ contexts: readonly EventContext[], artifactActivator?: ClientArtifactsActivator,
17
+ eventStore = 'orders', namespace = 'default'): Promise<ReactorMessage[]> {
18
+ const lifecycle = new ConnectionLifecycle();
19
+ const results: ReactorMessage[] = [];
20
+ let finish!: () => void;
21
+ const finished = new Promise<void>(resolve => { finish = resolve; });
22
+ async function* stream(queue: AsyncIterable<ReactorMessage>) {
23
+ const messages = queue[Symbol.asyncIterator]();
24
+ await messages.next();
25
+ try {
26
+ for (const context of contexts) {
27
+ yield { Events: [{ Content: '{}', Context: context }], Partition: context.EventSourceId,
28
+ ReplayState: ReplayState.REPLAY_STATE_None, InitialState: '' };
29
+ results.push((await messages.next()).value!);
30
+ }
31
+ } finally {
32
+ await lifecycle.disconnected(error => { throw error; });
33
+ finish();
34
+ }
35
+ }
36
+ const reactors = new Reactors({ reactors: [reactorType], eventTypes: [eventType] } as unknown as IClientArtifactsProvider,
37
+ { reactors: { observe: stream } } as unknown as ChronicleConnection,
38
+ eventStore, namespace, lifecycle, {} as IEventLog, undefined,
39
+ { readModels: {} } as IEventStore, artifactActivator);
40
+ try {
41
+ await reactors.register();
42
+ await finished;
43
+ return results;
44
+ } finally {
45
+ reactors.dispose();
46
+ }
47
+ }
@@ -0,0 +1,66 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { EventContext as ContractEventContext, EventObservationState, EventType, ObservationState } from '@cratis/chronicle.contracts';
6
+ import type { EventContext } from '../../events/EventContext.js';
7
+ import { eventType } from '../../events/eventTypeDecorator.js';
8
+ import type { ClientArtifactsActivator } from '../../artifacts/ClientArtifactsActivator.js';
9
+ import { handles, reactor, ReactorDelivery, replay, type ReactorServices } from '../index.js';
10
+ import { deliverToReactor } from './given/deliver_to_reactor.js';
11
+
12
+ chai.should();
13
+
14
+ @eventType('delivery-order-placed')
15
+ class OrderPlaced {}
16
+
17
+ for (const activated of [false, true]) {
18
+ describe(`when a ${activated ? 'activated' : 'default'} reactor receives a retry and replay`, () => {
19
+ let deliveries: ReactorDelivery[];
20
+ let states: ObservationState[];
21
+ let contexts: EventContext[];
22
+ let services: ReactorServices[];
23
+ beforeEach(async () => {
24
+ deliveries = [];
25
+ contexts = [];
26
+ services = [];
27
+ @reactor('order-confirmations', 'inbox-upstream')
28
+ class OrderConfirmations {
29
+ @handles(OrderPlaced)
30
+ confirm(_event: OrderPlaced, context: EventContext, reactorServices: ReactorServices, delivery: ReactorDelivery) {
31
+ contexts.push(context);
32
+ services.push(reactorServices);
33
+ deliveries.push(delivery);
34
+ if (deliveries.length === 1) throw new Error('failed after side effect');
35
+ }
36
+ @replay(OrderPlaced)
37
+ restore(event: OrderPlaced, context: EventContext, reactorServices: ReactorServices, delivery: ReactorDelivery) {
38
+ this.confirm(event, context, reactorServices, delivery);
39
+ }
40
+ }
41
+ const context = ContractEventContext.fromPartial({ EventType: EventType.fromPartial({ Id: 'delivery-order-placed', Generation: 1 }),
42
+ EventStore: 'source-store', Namespace: 'source-tenant', EventSourceId: 'order-42',
43
+ SequenceNumber: 9007199254740993n, ObservationState: EventObservationState.Initial });
44
+ const activator: ClientArtifactsActivator | undefined = activated ? type => ({ instance: new type() }) : undefined;
45
+ const results = await deliverToReactor(OrderConfirmations, OrderPlaced,
46
+ [context, { ...context }, { ...context, ObservationState: EventObservationState.Replay }], activator);
47
+ states = results.map(result => result.Content!.Value1!.State);
48
+ });
49
+ it('should preserve the identity through failed delivery, retry and a separate replay handler', () => {
50
+ deliveries.map(delivery => delivery.id).should.deep.equal(Array(3).fill(
51
+ 'order-confirmations#source-store#source-tenant#inbox-upstream#order-42#9007199254740993'));
52
+ });
53
+ it('should expose all six components from the observed event and reactor', () => {
54
+ deliveries[0].should.deep.equal(new ReactorDelivery('order-confirmations', 'source-store', 'source-tenant',
55
+ 'inbox-upstream', 'order-42', 9007199254740993n));
56
+ });
57
+ it('should preserve existing context and services arguments', () => {
58
+ contexts.map(context => context.observationState).should.deep.equal([
59
+ EventObservationState.Initial, EventObservationState.Initial, EventObservationState.Replay]);
60
+ services.every(service => service.eventStore.readModels === service.readModels).should.be.true;
61
+ });
62
+ it('should still report the handler failure and subsequent successes', () => {
63
+ states.should.deep.equal([ObservationState.Failed, ObservationState.Success, ObservationState.Success]);
64
+ });
65
+ });
66
+ }
package/reactors/index.ts CHANGED
@@ -2,6 +2,7 @@
2
2
  // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
3
 
4
4
  export { ReactorId } from './ReactorId.js';
5
+ export { ReactorDelivery } from './ReactorDelivery.js';
5
6
  export { reactor, getReactorMetadata, isReactor } from './reactor.js';
6
7
  export { handles } from '../events/handles.js';
7
8
  export { onceOnly } from './onceOnly.js';
@@ -25,6 +25,7 @@ import { createHandlerInstanceValidator } from '../observation/createHandlerInst
25
25
  import { getReactorMetadata } from '../reactors/reactor.js';
26
26
  import { normalizeReactorSideEffects } from '../reactors/ReactorSideEffects.js';
27
27
  import type { ReactorServices } from '../reactors/ReactorServices.js';
28
+ import { ReactorDelivery } from '../reactors/ReactorDelivery.js';
28
29
  import { EventScenario } from './EventScenario.js';
29
30
  import type { ReactorDeliveryResult } from './ReactorDeliveryResult.js';
30
31
  import type { ReactorScenarioOptions } from './ReactorScenarioOptions.js';
@@ -239,7 +240,10 @@ export class ReactorScenario {
239
240
  // A swallowed unsupported call fails this event like a throwing handler: nothing is recorded.
240
241
  this.throwIfViolated(delivery);
241
242
  return this.record(result, event.context, selection.methodName!, deliveryIndex);
242
- });
243
+ }, new ReactorDelivery(getReactorMetadata(this._reactor)!.id.value,
244
+ event.context.eventStore ?? this._options.eventStore ?? 'test-event-store',
245
+ event.context.namespace ?? this._options.namespace ?? 'default', 'event-log',
246
+ event.context.eventSourceId, event.context.sequenceNumber));
243
247
  this.throwIfViolated(delivery);
244
248
  handled.push(event.context);
245
249
  }
@@ -0,0 +1,68 @@
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
+ import { beforeEach, chai, describe, it } from 'vitest';
5
+ import { field } from '@cratis/fundamentals';
6
+ import { EventContext as ContractEventContext, EventType as ContractEventType, ObservationState } from '@cratis/chronicle.contracts';
7
+ import type { EventContext } from '../../events/EventContext.js';
8
+ import { eventType } from '../../events/eventTypeDecorator.js';
9
+ import { ReactorDelivery, reactor, type ReactorServices } from '../../reactors/index.js';
10
+ import { deliverToReactor } from '../../reactors/for_Reactors/given/deliver_to_reactor.js';
11
+ import { ReactorScenario } from '../index.js';
12
+ import type { ClientArtifactsActivator } from '../../artifacts/ClientArtifactsActivator.js';
13
+
14
+ chai.should();
15
+
16
+ @eventType('scenario-delivery-order-placed')
17
+ class OrderPlaced {
18
+ @field(String) readonly name: string;
19
+ constructor(name = '') { this.name = name; }
20
+ }
21
+
22
+ for (const activated of [false, true]) {
23
+ describe(`when a ${activated ? 'activated' : 'default'} scenario reactor receives a delivery identity`, () => {
24
+ let scenarioDeliveries: ReactorDelivery[];
25
+ let productionDeliveries: ReactorDelivery[];
26
+ let contexts: EventContext[];
27
+ let states: ObservationState[];
28
+ beforeEach(async () => {
29
+ let deliveries: ReactorDelivery[] = [];
30
+ contexts = [];
31
+ @reactor('order-confirmations')
32
+ class OrderConfirmations {
33
+ orderPlaced(_event: OrderPlaced, context: EventContext, services: ReactorServices, delivery: ReactorDelivery) {
34
+ (services.eventStore.readModels === services.readModels).should.be.true;
35
+ contexts.push(context);
36
+ deliveries.push(delivery);
37
+ }
38
+ }
39
+ const activator: ClientArtifactsActivator | undefined = activated ? type => ({ instance: new type() }) : undefined;
40
+ const scenario = new ReactorScenario(OrderConfirmations, {
41
+ artifacts: { eventTypes: [OrderPlaced] }, constraints: 'disabled', artifactActivator: activator });
42
+ await scenario.given.forEventSource('order-42').events(new OrderPlaced('first'));
43
+ await scenario.when.forEventSource('order-43').events(new OrderPlaced('second'));
44
+ scenarioDeliveries = deliveries;
45
+ const wireContexts = contexts.map(context => ContractEventContext.fromPartial({
46
+ EventType: ContractEventType.fromPartial({ Id: 'scenario-delivery-order-placed', Generation: 1 }),
47
+ EventStore: context.eventStore, Namespace: context.namespace,
48
+ EventSourceId: context.eventSourceId, SequenceNumber: context.sequenceNumber,
49
+ ObservationState: context.observationState
50
+ }));
51
+ deliveries = [];
52
+ const results = await deliverToReactor(OrderConfirmations, OrderPlaced, wireContexts, activator,
53
+ 'test-event-store', 'default');
54
+ productionDeliveries = deliveries;
55
+ states = results.map(result => result.Content!.Value1!.State);
56
+ });
57
+ it('should supply the same identity as the production observation path', () => {
58
+ scenarioDeliveries.should.deep.equal(productionDeliveries);
59
+ scenarioDeliveries.map(delivery => delivery.id).should.deep.equal(productionDeliveries.map(delivery => delivery.id));
60
+ states.should.deep.equal([ObservationState.Success, ObservationState.Success]);
61
+ });
62
+ it('should use the fixture store, namespace, partition and committed sequence number', () => {
63
+ scenarioDeliveries.map(delivery => delivery.id).should.deep.equal([
64
+ 'order-confirmations#test-event-store#default#event-log#order-42#0',
65
+ 'order-confirmations#test-event-store#default#event-log#order-43#1']);
66
+ });
67
+ });
68
+ }
@@ -38,6 +38,11 @@ const allFixtures = readdirSync(directory).filter(name => name.endsWith('.json')
38
38
  .map(name => ({ name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture }));
39
39
  const fixtures = allFixtures.filter(({ fixture }) => fixture.kind === 'kernelSemantics');
40
40
  const guardErrors: Record<string, { type: string; message: string }> = {
41
+ 'composite-parent-key-without-constant.json': { type: 'UnsupportedProjectionOperation', message: 'only a lowercase id with a fixture-backed string, GUID, or number/double schema' },
42
+ 'composite-child-join-key-expression-parts.json': { type: 'UnsupportedProjectionOperation', message: 'only a lowercase id with a fixture-backed string, GUID, or number/double schema' },
43
+ 'composite-key-expression-parts.json': { type: 'UnsupportedProjectionOperation', message: 'only a lowercase id with a fixture-backed string, GUID, or number/double schema' },
44
+ 'composite-parent-key-expression-parts.json': { type: 'UnsupportedProjectionOperation', message: 'only a lowercase id with a fixture-backed string, GUID, or number/double schema' },
45
+ 'composite-join-key-expression-parts.json': { type: 'UnsupportedProjectionOperation', message: 'join keys other than $eventSourceId' },
41
46
  'from-all-only.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
42
47
  'from-all-mixed.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
43
48
  'from-all-exclude-children.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
@@ -0,0 +1,40 @@
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
+ import 'reflect-metadata';
5
+ import { chai, describe, it } from 'vitest';
6
+ import type { ICompositeKeyBuilder } from '../../../projections/declarative/ICompositeKeyBuilder.js';
7
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
8
+ import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
9
+ import { Changed } from './given/Changed.js';
10
+ import { Removed } from './given/Removed.js';
11
+ import { compileDeclarative } from './given/compile.js';
12
+
13
+ chai.should();
14
+
15
+ for (const source of ['event property', 'context', 'source id', 'constant'] as const) {
16
+ for (const kind of ['root', 'parent', 'join'] as const) {
17
+ describe(`when validating a ${kind} composite key with a ${source} part before replay`, () => {
18
+ it('should reject the composite key rather than evaluate unproven semantics', () => {
19
+ const configure = <TEvent>(key: ICompositeKeyBuilder<{ part: string }, TEvent>) => {
20
+ const part = key.set(target => target.part);
21
+ if (source === 'context') return part.toEventContextProperty('subject');
22
+ if (source === 'source id') return part.toEventSourceId();
23
+ if (source === 'constant') return part.toValue('fixed');
24
+ return part.to(event => (event as { name: string }).name);
25
+ };
26
+ const { compiled, definition } = compileDeclarative(builder => {
27
+ if (kind === 'root') builder.from(Changed, from => from.usingCompositeKey(configure));
28
+ if (kind === 'parent') builder.from(Changed, from => from.usingParentCompositeKey(configure));
29
+ if (kind === 'join') builder
30
+ .from(Changed, from => from.set(model => model.state).to(event => event.name))
31
+ .join(Removed, join => join.on(model => model.state).usingCompositeKey(configure));
32
+ });
33
+ const section = kind === 'join' ? 'Join[capability-removed:1].Key'
34
+ : kind === 'parent' ? 'From[capability-changed:1].ParentKey' : 'From[capability-changed:1].Key';
35
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
36
+ .with.property('message').that.includes(section);
37
+ });
38
+ });
39
+ }
40
+ }