@cratis/chronicle 6.41.0 → 6.42.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.
- package/compliance/for_PIIManager/against_a_kernel/when_erasing_a_subject.integration.spec.ts +4 -4
- package/compliance/resolveEventSubject.ts +19 -0
- package/compliance/subject.ts +2 -2
- package/dist/compliance/resolveEventSubject.d.ts +3 -0
- package/dist/compliance/resolveEventSubject.d.ts.map +1 -0
- package/dist/compliance/resolveEventSubject.js +21 -0
- package/dist/compliance/resolveEventSubject.js.map +1 -0
- package/dist/compliance/subject.d.ts +2 -2
- package/dist/compliance/subject.js +2 -2
- package/dist/eventSequences/AppendOptions.d.ts +1 -1
- package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts +1 -1
- package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +2 -2
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareBatchAppend.js +2 -1
- package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
- package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
- package/dist/eventSequences/prepareSingleAppend.js +3 -1
- package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
- package/dist/projections/captureProjectionProvenance.d.ts.map +1 -1
- package/dist/projections/captureProjectionProvenance.js +2 -0
- package/dist/projections/captureProjectionProvenance.js.map +1 -1
- package/dist/projections/declarative/FromAllBuilder.d.ts +18 -0
- package/dist/projections/declarative/FromAllBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/FromAllBuilder.js +36 -0
- package/dist/projections/declarative/FromAllBuilder.js.map +1 -0
- package/dist/projections/declarative/IFromAllBuilder.d.ts +33 -0
- package/dist/projections/declarative/IFromAllBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/IFromAllBuilder.js +4 -0
- package/dist/projections/declarative/IFromAllBuilder.js.map +1 -0
- package/dist/projections/declarative/IProjectionBuilderFor.d.ts +8 -0
- package/dist/projections/declarative/IProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts +4 -0
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.js +15 -0
- package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
- package/dist/projections/declarative/index.d.ts +2 -0
- package/dist/projections/declarative/index.d.ts.map +1 -1
- package/dist/projections/declarative/index.js +1 -0
- package/dist/projections/declarative/index.js.map +1 -1
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts +6 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts.map +1 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js +12 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +2 -1
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +2 -1
- package/dist/projections/modelBound/fromAll.js.map +1 -1
- package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
- package/dist/testing/InProcessEventSequence.js +16 -6
- package/dist/testing/InProcessEventSequence.js.map +1 -1
- package/dist/testing/ReadModelScenario.d.ts.map +1 -1
- package/dist/testing/ReadModelScenario.js +4 -0
- package/dist/testing/ReadModelScenario.js.map +1 -1
- package/dist/testing/singleAppendRoute.d.ts +1 -1
- package/dist/testing/singleAppendRoute.d.ts.map +1 -1
- package/dist/testing/singleAppendRoute.js +8 -4
- package/dist/testing/singleAppendRoute.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/AppendOptions.ts +1 -1
- package/eventSequences/EventForEventSourceId.ts +1 -1
- package/eventSequences/EventSequence.ts +2 -2
- package/eventSequences/for_EventSequence/when_appending/subject_behaviors.fixture.ts +239 -0
- package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.spec.ts +34 -0
- package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts +38 -0
- package/eventSequences/prepareBatchAppend.ts +2 -1
- package/eventSequences/prepareSingleAppend.ts +3 -1
- package/package.json +2 -2
- package/projections/captureProjectionProvenance.ts +1 -0
- package/projections/declarative/FromAllBuilder.ts +49 -0
- package/projections/declarative/IFromAllBuilder.ts +39 -0
- package/projections/declarative/IProjectionBuilderFor.ts +9 -0
- package/projections/declarative/ProjectionBuilderFor.ts +17 -0
- package/projections/declarative/for_ProjectionBuilderFor/when_subscribing_to_all_events.spec.ts +111 -0
- package/projections/declarative/index.ts +2 -0
- package/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.ts +15 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.spec.ts +90 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts +69 -0
- package/projections/modelBound/fromAll.ts +2 -1
- package/testing/EventScenario.batches.spec.ts +2 -2
- package/testing/EventScenario.fixtures.spec.ts +2 -2
- package/testing/EventScenario.rejections.spec.ts +2 -2
- package/testing/InProcessEventSequence.ts +17 -6
- package/testing/ReadModelScenario.spec.ts +31 -0
- package/testing/ReadModelScenario.ts +4 -0
- package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts +1 -1
- package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts +11 -1
- package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +5 -0
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +2 -0
- package/testing/singleAppendRoute.spec.ts +19 -0
- package/testing/singleAppendRoute.ts +9 -4
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.42.0",
|
|
4
4
|
"description": "Event sourcing for TypeScript and Node.js — the idiomatic client for Cratis Chronicle, the open-source event-sourcing database",
|
|
5
5
|
"author": "Cratis",
|
|
6
6
|
"license": "MIT",
|
|
@@ -173,7 +173,7 @@
|
|
|
173
173
|
"clean": "rm -rf dist",
|
|
174
174
|
"test": "vitest run",
|
|
175
175
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
176
|
-
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts",
|
|
176
|
+
"test:standard-types": "tsc --ignoreConfig --noEmit --target es2022 --module es2022 --moduleResolution bundler --lib es2022,esnext.decorators --types node --skipLibCheck --strict schemas/decorators.standard.spec.ts projections/declarative/addChildCompatibility.standard.spec.ts projections/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts readModels/registration.standard.spec.ts for_ChronicleClient/when_registering_artifacts/with_standard_property_mappings.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts testing/for_EventScenario/when_appending_concept_fields/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_scoped_constraints/with_standard_decorators.standard.spec.ts schemas/for_JsonSchemaGenerator/when_generating_children/with_standard_decorators.standard.spec.ts events/for_handles/when_decorating_a_method.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_arithmetic/with_standard_decorators.standard.spec.ts testing/for_ReadModelScenario/when_evaluating_joins/with_standard_decorators.standard.spec.ts testing/for_EventScenario/when_appending_field_types/with_standard_decorators.standard.spec.ts eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts",
|
|
177
177
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
178
178
|
},
|
|
179
179
|
"dependencies": {
|
|
@@ -39,6 +39,7 @@ export function captureProjectionProvenance(definition: Record<string, unknown>,
|
|
|
39
39
|
add(`${section}.${property}`, modelBound ? (section === 'Children' ? '@childrenFrom' : '@nested') : `.${section.toLowerCase()}`);
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
|
+
if (definition.SubscribesToAllEvents === true) add('SubscribesToAllEvents', `${prefix}fromAll`);
|
|
42
43
|
const all = definition.All as { Properties?: Record<string, string>; IncludeChildren?: boolean } | undefined;
|
|
43
44
|
if (all?.IncludeChildren || Object.keys(all?.Properties ?? {}).length) add('All', modelBound ? '@fromEvery' : '.fromEvery');
|
|
44
45
|
for (const property of Object.keys(all?.Properties ?? {})) add(`All.Properties.${property}`, modelBound ? '@fromEvery' : '.fromEvery');
|
|
@@ -0,0 +1,49 @@
|
|
|
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 { PropertyAccessor, PropertyPathResolverProxyHandler } from '@cratis/fundamentals';
|
|
5
|
+
import { AllSetBuilder } from './AllSetBuilder.js';
|
|
6
|
+
import type { IAllSetBuilder } from './IAllSetBuilder.js';
|
|
7
|
+
import type { IFromAllBuilder } from './IFromAllBuilder.js';
|
|
8
|
+
|
|
9
|
+
/** Accumulates all-event mappings without restricting the event subscription to known types. */
|
|
10
|
+
export class FromAllBuilder<TReadModel> implements IFromAllBuilder<TReadModel> {
|
|
11
|
+
readonly properties: Record<string, string> = {};
|
|
12
|
+
|
|
13
|
+
/** @inheritdoc */
|
|
14
|
+
set(readModelPropertyAccessor: PropertyAccessor<TReadModel>): IAllSetBuilder<TReadModel, IFromAllBuilder<TReadModel>> {
|
|
15
|
+
return new AllSetBuilder<TReadModel, IFromAllBuilder<TReadModel>>(
|
|
16
|
+
this.targetProperty(readModelPropertyAccessor),
|
|
17
|
+
(property, expression) => { this.properties[property] = expression; },
|
|
18
|
+
this
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** @inheritdoc */
|
|
23
|
+
count(readModelPropertyAccessor: PropertyAccessor<TReadModel>, contextPropertyName?: string): IFromAllBuilder<TReadModel> {
|
|
24
|
+
return this.operation(readModelPropertyAccessor, '$count', contextPropertyName);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** @inheritdoc */
|
|
28
|
+
increment(readModelPropertyAccessor: PropertyAccessor<TReadModel>, contextPropertyName?: string): IFromAllBuilder<TReadModel> {
|
|
29
|
+
return this.operation(readModelPropertyAccessor, '$increment', contextPropertyName);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** @inheritdoc */
|
|
33
|
+
decrement(readModelPropertyAccessor: PropertyAccessor<TReadModel>, contextPropertyName?: string): IFromAllBuilder<TReadModel> {
|
|
34
|
+
return this.operation(readModelPropertyAccessor, '$decrement', contextPropertyName);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private operation(accessor: PropertyAccessor<TReadModel>, expression: string, contextPropertyName?: string): this {
|
|
38
|
+
const property = this.targetProperty(accessor);
|
|
39
|
+
const target = contextPropertyName === undefined ? property : `${property}.$eventContext.${contextPropertyName}`;
|
|
40
|
+
this.properties[target] = expression;
|
|
41
|
+
return this;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
private targetProperty(accessor: PropertyAccessor<TReadModel>): string {
|
|
45
|
+
const handler = new PropertyPathResolverProxyHandler();
|
|
46
|
+
accessor(new Proxy({}, handler) as TReadModel);
|
|
47
|
+
return handler.property;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
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 { PropertyAccessor } from '@cratis/fundamentals';
|
|
5
|
+
import type { IAllSetBuilder } from './IAllSetBuilder.js';
|
|
6
|
+
|
|
7
|
+
/** Configures mappings for every event, including types not otherwise declared by the projection. */
|
|
8
|
+
export interface IFromAllBuilder<TReadModel> {
|
|
9
|
+
/**
|
|
10
|
+
* Starts an assignment to a read-model property.
|
|
11
|
+
* @param readModelPropertyAccessor - Target property accessor.
|
|
12
|
+
* @returns The assignment builder, continuing with this all-events builder.
|
|
13
|
+
*/
|
|
14
|
+
set(readModelPropertyAccessor: PropertyAccessor<TReadModel>): IAllSetBuilder<TReadModel, IFromAllBuilder<TReadModel>>;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Counts every event, optionally under a dynamic dictionary key.
|
|
18
|
+
* @param readModelPropertyAccessor - Scalar or dictionary target accessor.
|
|
19
|
+
* @param contextPropertyName - Optional event-context path for the dictionary key, such as `eventType.id`.
|
|
20
|
+
* @returns This builder for chaining.
|
|
21
|
+
*/
|
|
22
|
+
count(readModelPropertyAccessor: PropertyAccessor<TReadModel>, contextPropertyName?: string): IFromAllBuilder<TReadModel>;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Increments a property for every event, optionally under a dynamic dictionary key.
|
|
26
|
+
* @param readModelPropertyAccessor - Scalar or dictionary target accessor.
|
|
27
|
+
* @param contextPropertyName - Optional event-context path for the dictionary key.
|
|
28
|
+
* @returns This builder for chaining.
|
|
29
|
+
*/
|
|
30
|
+
increment(readModelPropertyAccessor: PropertyAccessor<TReadModel>, contextPropertyName?: string): IFromAllBuilder<TReadModel>;
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Decrements a property for every event, optionally under a dynamic dictionary key.
|
|
34
|
+
* @param readModelPropertyAccessor - Scalar or dictionary target accessor.
|
|
35
|
+
* @param contextPropertyName - Optional event-context path for the dictionary key.
|
|
36
|
+
* @returns This builder for chaining.
|
|
37
|
+
*/
|
|
38
|
+
decrement(readModelPropertyAccessor: PropertyAccessor<TReadModel>, contextPropertyName?: string): IFromAllBuilder<TReadModel>;
|
|
39
|
+
}
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { PropertyAccessor } from '@cratis/fundamentals';
|
|
5
5
|
import { IProjectionBuilder } from './IProjectionBuilder.js';
|
|
6
|
+
import type { IFromAllBuilder } from './IFromAllBuilder.js';
|
|
6
7
|
|
|
7
8
|
/**
|
|
8
9
|
* Defines the top-level projection builder for a specific read model type.
|
|
@@ -11,6 +12,14 @@ import { IProjectionBuilder } from './IProjectionBuilder.js';
|
|
|
11
12
|
*/
|
|
12
13
|
export interface IProjectionBuilderFor<TReadModel>
|
|
13
14
|
extends IProjectionBuilder<TReadModel, IProjectionBuilderFor<TReadModel>> {
|
|
15
|
+
/**
|
|
16
|
+
* Subscribes to every event type, including types not explicitly declared.
|
|
17
|
+
* Mappings share the All block with fromEvery. Unlike fromEvery, this expands the subscription.
|
|
18
|
+
* @param builderCallback - Callback for configuring all-event assignments and counters.
|
|
19
|
+
* @returns This builder for fluent chaining.
|
|
20
|
+
*/
|
|
21
|
+
fromAll(builderCallback: (builder: IFromAllBuilder<TReadModel>) => void): IProjectionBuilderFor<TReadModel>;
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Specifies the event sequence this projection should read from.
|
|
16
25
|
* @param eventSequenceId - The identifier of the event sequence.
|
|
@@ -9,6 +9,8 @@ import { ChildrenBuilder } from './ChildrenBuilder.js';
|
|
|
9
9
|
import { IChildrenBuilder } from './IChildrenBuilder.js';
|
|
10
10
|
import { INestedBuilder } from './INestedBuilder.js';
|
|
11
11
|
import { IProjectionBuilderFor } from './IProjectionBuilderFor.js';
|
|
12
|
+
import type { IFromAllBuilder } from './IFromAllBuilder.js';
|
|
13
|
+
import { FromAllBuilder } from './FromAllBuilder.js';
|
|
12
14
|
import { NestedBuilder } from './NestedBuilder.js';
|
|
13
15
|
import { ChildrenDefinitionLike, ContractEventType, ProjectionBuilderCore } from './ProjectionBuilderCore.js';
|
|
14
16
|
|
|
@@ -22,6 +24,7 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
|
|
|
22
24
|
private _containerName: string | undefined;
|
|
23
25
|
private _rewindable: boolean = true;
|
|
24
26
|
private _active: boolean = true;
|
|
27
|
+
private _subscribesToAllEvents = false;
|
|
25
28
|
private _variantIdentity: Function | undefined;
|
|
26
29
|
private _variantKey: string | undefined;
|
|
27
30
|
private readonly _enteringEventTypes: ContractEventType[] = [];
|
|
@@ -31,6 +34,19 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
|
|
|
31
34
|
this._autoMap = AutoMap.Enabled;
|
|
32
35
|
}
|
|
33
36
|
|
|
37
|
+
/** @inheritdoc */
|
|
38
|
+
fromAll(builderCallback: (builder: IFromAllBuilder<TReadModel>) => void): this {
|
|
39
|
+
const builder = new FromAllBuilder<TReadModel>();
|
|
40
|
+
builderCallback(builder);
|
|
41
|
+
this._subscribesToAllEvents = true;
|
|
42
|
+
this._all = {
|
|
43
|
+
Properties: { ...this._all.Properties, ...builder.properties },
|
|
44
|
+
IncludeChildren: true,
|
|
45
|
+
AutoMap: AutoMap.Inherit
|
|
46
|
+
};
|
|
47
|
+
return this;
|
|
48
|
+
}
|
|
49
|
+
|
|
34
50
|
/** @inheritdoc */
|
|
35
51
|
fromEventSequence(eventSequenceId: string): this {
|
|
36
52
|
this._eventSequenceId = eventSequenceId;
|
|
@@ -146,6 +162,7 @@ export class ProjectionBuilderFor<TReadModel> extends ProjectionBuilderCore<TRea
|
|
|
146
162
|
Children: this._children,
|
|
147
163
|
FromEvery: [],
|
|
148
164
|
All: this._all,
|
|
165
|
+
...(this._subscribesToAllEvents ? { SubscribesToAllEvents: true } : {}),
|
|
149
166
|
FromEventProperty: undefined,
|
|
150
167
|
RemovedWith: this._removedWith,
|
|
151
168
|
RemovedWithJoin: this._removedWithJoin,
|
package/projections/declarative/for_ProjectionBuilderFor/when_subscribing_to_all_events.spec.ts
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
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 { readFileSync } from 'node:fs';
|
|
5
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
6
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
7
|
+
import { ProjectionBuilderFor, projection, type IFromAllBuilder, type IProjectionBuilderFor } from '../index.js';
|
|
8
|
+
import { ReadModelScenario } from '../../../testing/ReadModelScenario.js';
|
|
9
|
+
import { UnsupportedProjectionOperation } from '../../../testing/projections/UnsupportedProjectionOperation.js';
|
|
10
|
+
|
|
11
|
+
chai.should();
|
|
12
|
+
|
|
13
|
+
class Model {
|
|
14
|
+
title = '';
|
|
15
|
+
source = '';
|
|
16
|
+
lastSequence = '';
|
|
17
|
+
counted = 0;
|
|
18
|
+
incremented = 0;
|
|
19
|
+
decremented = 0;
|
|
20
|
+
countsByType: Record<string, number> = {};
|
|
21
|
+
}
|
|
22
|
+
class Declared { title = ''; }
|
|
23
|
+
class Removed {}
|
|
24
|
+
eventType('Declared')(Declared);
|
|
25
|
+
eventType('Removed')(Removed);
|
|
26
|
+
|
|
27
|
+
function mappings(all: IFromAllBuilder<Model>): void {
|
|
28
|
+
all.set(model => model.title).to(model => model.title)
|
|
29
|
+
.set(model => model.source).toEventSourceId()
|
|
30
|
+
.set(model => model.lastSequence).toEventContextProperty('sequenceNumber')
|
|
31
|
+
.count(model => model.counted)
|
|
32
|
+
.increment(model => model.incremented)
|
|
33
|
+
.decrement(model => model.decremented)
|
|
34
|
+
.count(model => model.countsByType, 'eventType.id');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function fixture(name: string) {
|
|
38
|
+
return JSON.parse(readFileSync(new URL(`../../../testing/projections/fixtures/${name}.json`, import.meta.url), 'utf8')) as {
|
|
39
|
+
wireDefinition: { All: object; SubscribesToAllEvents: boolean };
|
|
40
|
+
expected: Array<{ publicRead: Record<string, Record<string, unknown>> }>;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
for (const name of ['from-all-only', 'from-all-mixed', 'from-all-exclude-children', 'from-all-empty']) {
|
|
45
|
+
describe(`when subscribing to all events using ${name}`, () => {
|
|
46
|
+
let definition: Record<string, unknown>;
|
|
47
|
+
const captured = fixture(name);
|
|
48
|
+
beforeEach(() => {
|
|
49
|
+
const builder = new ProjectionBuilderFor<Model>().noAutoMap();
|
|
50
|
+
if (name === 'from-all-mixed' || name === 'from-all-exclude-children') {
|
|
51
|
+
builder.from(Declared, from => from.set(model => model.title).toValue('explicit')).removedWith(Removed);
|
|
52
|
+
}
|
|
53
|
+
builder.fromAll(name === 'from-all-empty' ? () => {} : mappings);
|
|
54
|
+
if (name === 'from-all-exclude-children') builder.fromEvery(every => every.excludeChildProjections());
|
|
55
|
+
definition = builder.build(name, 'OracleReadModel');
|
|
56
|
+
});
|
|
57
|
+
it('should emit the all-events subscription captured by the packaged kernel', () => {
|
|
58
|
+
definition.SubscribesToAllEvents!.should.equal(captured.wireDefinition.SubscribesToAllEvents);
|
|
59
|
+
});
|
|
60
|
+
it('should emit the mappings and child flag captured by the packaged kernel', () => {
|
|
61
|
+
definition.All!.should.deep.equal(captured.wireDefinition.All);
|
|
62
|
+
});
|
|
63
|
+
it('should have kernel evidence that an undeclared event creates a read model', () => {
|
|
64
|
+
captured.expected[0].publicRead['record-a'].id!.should.equal('record-a');
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
describe('when creating a declarative all-event scenario', () => {
|
|
70
|
+
it('should reject an empty all-event subscription before any events can be seeded', () => {
|
|
71
|
+
class AllProjection {
|
|
72
|
+
define(builder: IProjectionBuilderFor<Model>): void { builder.fromAll(() => {}); }
|
|
73
|
+
}
|
|
74
|
+
projection('all-scenario', Model)(AllProjection);
|
|
75
|
+
(() => new ReadModelScenario(Model, { readModels: [Model], eventTypes: [], projections: [AllProjection], reducers: [] }))
|
|
76
|
+
.should.throw(UnsupportedProjectionOperation, 'SubscribesToAllEvents (.fromAll)');
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('when combining fromAll and fromEvery mappings', () => {
|
|
81
|
+
it('should retain every mapping and never narrow the subscription', () => {
|
|
82
|
+
const builder = new ProjectionBuilderFor<Model>();
|
|
83
|
+
builder.fromEvery(every => every.set(model => model.title).to(model => model.title));
|
|
84
|
+
builder.fromAll(all => all.count(model => model.counted));
|
|
85
|
+
builder.fromAll(all => all.increment(model => model.incremented, 'eventType.id').decrement(model => model.decremented, 'eventType.id'));
|
|
86
|
+
builder.fromEvery(every => every.set(model => model.source).toEventSourceId().excludeChildProjections());
|
|
87
|
+
const definition = builder.build('mixed', 'Model');
|
|
88
|
+
definition.SubscribesToAllEvents!.should.equal(true);
|
|
89
|
+
definition.All!.should.deep.equal({ Properties: {
|
|
90
|
+
title: 'title', counted: '$count', 'incremented.$eventContext.eventType.id': '$increment',
|
|
91
|
+
'decremented.$eventContext.eventType.id': '$decrement', source: '$eventSourceId'
|
|
92
|
+
}, IncludeChildren: false, AutoMap: 0 });
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('should leave fromEvery subscription behavior unchanged', () => {
|
|
96
|
+
const builder = new ProjectionBuilderFor<Model>();
|
|
97
|
+
builder.from(Declared).fromEvery(every => every.set(model => model.title).to(model => model.title));
|
|
98
|
+
(builder.build('every', 'Model').SubscribesToAllEvents === undefined).should.be.true;
|
|
99
|
+
const captured = fixture('from-every-restricted');
|
|
100
|
+
captured.expected[0].publicRead.should.deep.equal({});
|
|
101
|
+
captured.expected[3].publicRead['record-a'].counted!.should.equal(1);
|
|
102
|
+
captured.expected[5].publicRead.should.deep.equal({});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should hash the subscription flag independently of identical mappings', () => {
|
|
106
|
+
const all = new ProjectionBuilderFor<Model>().fromAll(builder => builder.set(model => model.title).to(model => model.title));
|
|
107
|
+
const every = new ProjectionBuilderFor<Model>().fromEvery(builder => builder.set(model => model.title).to(model => model.title));
|
|
108
|
+
all.build('same', 'Model').LastUpdated!.should.not.deep.equal(every.build('same', 'Model').LastUpdated);
|
|
109
|
+
all.build('same', 'Model').LastUpdated!.should.deep.equal(all.build('same', 'Model').LastUpdated);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -11,6 +11,8 @@ export { AddChildBuilder } from './AddChildBuilder.js';
|
|
|
11
11
|
export type { ChildAdditionEntry } from './AddChildBuilder.js';
|
|
12
12
|
export { CompositeKeyBuilder } from './CompositeKeyBuilder.js';
|
|
13
13
|
export { FromBuilder } from './FromBuilder.js';
|
|
14
|
+
export { FromAllBuilder } from './FromAllBuilder.js';
|
|
15
|
+
export type { IFromAllBuilder } from './IFromAllBuilder.js';
|
|
14
16
|
export { JoinBuilder } from './JoinBuilder.js';
|
|
15
17
|
export { SetBuilder } from './SetBuilder.js';
|
|
16
18
|
export { AddBuilder } from './AddBuilder.js';
|
|
@@ -0,0 +1,15 @@
|
|
|
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 type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
6
|
+
import { ProjectionDefinitionCompiler } from '../../ProjectionDefinitionCompiler.js';
|
|
7
|
+
|
|
8
|
+
export function compileModelBound(model: Constructor) {
|
|
9
|
+
const artifacts: IClientArtifactsProvider = {
|
|
10
|
+
readModels: [model], projections: [], eventTypes: [], globalForHandlers: [], reducers: [],
|
|
11
|
+
reactors: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
12
|
+
};
|
|
13
|
+
const compiled = new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([], [model]);
|
|
14
|
+
return { compiled, definition: compiled.definitions[0] };
|
|
15
|
+
}
|
|
@@ -0,0 +1,90 @@
|
|
|
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 { field } from '@cratis/fundamentals';
|
|
6
|
+
import { beforeEach, chai, describe, it } from 'vitest';
|
|
7
|
+
import { fromAll } from '../modelBound/fromAll.js';
|
|
8
|
+
import { fromEvery } from '../modelBound/fromEvery.js';
|
|
9
|
+
import { ReadModelScenario } from '../../testing/ReadModelScenario.js';
|
|
10
|
+
import { UnsupportedProjectionOperation } from '../../testing/projections/UnsupportedProjectionOperation.js';
|
|
11
|
+
import { compileModelBound } from './given/compile_model_bound.js';
|
|
12
|
+
|
|
13
|
+
chai.should();
|
|
14
|
+
|
|
15
|
+
class Model {
|
|
16
|
+
id = '';
|
|
17
|
+
title = '';
|
|
18
|
+
source = '';
|
|
19
|
+
lastType = '';
|
|
20
|
+
}
|
|
21
|
+
for (const property of ['id', 'title', 'source', 'lastType']) field(String)(Model.prototype, property);
|
|
22
|
+
|
|
23
|
+
describe('when compiling legacy fromAll without explicit event subscriptions', () => {
|
|
24
|
+
let result: ReturnType<typeof compileModelBound>;
|
|
25
|
+
beforeEach(() => {
|
|
26
|
+
class Candidate extends Model {}
|
|
27
|
+
fromAll()(Candidate.prototype, 'title');
|
|
28
|
+
fromAll(undefined, 'sequenceNumber')(Candidate.prototype, 'lastType');
|
|
29
|
+
fromEvery(undefined, 'eventSourceId')(Candidate.prototype, 'source');
|
|
30
|
+
result = compileModelBound(Candidate);
|
|
31
|
+
});
|
|
32
|
+
it('should keep the alias from expanding the event subscription', () => {
|
|
33
|
+
(result.definition.SubscribesToAllEvents === undefined).should.be.true;
|
|
34
|
+
result.definition.From.should.deep.equal([]);
|
|
35
|
+
result.definition.FromEvery.should.deep.equal([]);
|
|
36
|
+
});
|
|
37
|
+
it('should merge content and context mappings with fromEvery into All', () => {
|
|
38
|
+
result.definition.All!.Properties.should.deep.equal({ title: 'title', lastType: '$eventContext(SequenceNumber)', source: '$eventContext(EventSourceId)' });
|
|
39
|
+
result.definition.All!.IncludeChildren.should.be.false;
|
|
40
|
+
});
|
|
41
|
+
it('should preserve the decorator in the mapping diagnostic', () => {
|
|
42
|
+
result.compiled.provenance.get(result.definition)!.should.include.deep.members([
|
|
43
|
+
{ contractPath: 'All.Properties.title', declaration: '@fromAll' }
|
|
44
|
+
]);
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
describe('when using legacy fromAll in a read-model scenario', () => {
|
|
49
|
+
it('should reject the all-event mapping before events can be seeded', () => {
|
|
50
|
+
class Candidate extends Model {}
|
|
51
|
+
fromAll('title')(Candidate.prototype, 'title');
|
|
52
|
+
(() => new ReadModelScenario(Candidate, { readModels: [Candidate], eventTypes: [], projections: [], reducers: [] }))
|
|
53
|
+
.should.throw(UnsupportedProjectionOperation, 'All (@fromAll)');
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('when compiling legacy fromEvery without fromAll', () => {
|
|
58
|
+
it('should preserve the existing sparse definition and child flag', () => {
|
|
59
|
+
class Candidate extends Model {}
|
|
60
|
+
fromEvery('name')(Candidate.prototype, 'title');
|
|
61
|
+
const { definition } = compileModelBound(Candidate);
|
|
62
|
+
(definition.SubscribesToAllEvents === undefined).should.be.true;
|
|
63
|
+
definition.All!.should.deep.equal({ Properties: { title: 'name' }, IncludeChildren: false, AutoMap: 0 });
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('when inheriting legacy fromAll metadata', () => {
|
|
68
|
+
it('should inherit the restricted mapping without changing a sibling', () => {
|
|
69
|
+
class AllModel extends Model {}
|
|
70
|
+
fromAll('name')(AllModel.prototype, 'title');
|
|
71
|
+
class Derived extends AllModel {}
|
|
72
|
+
const { definition } = compileModelBound(Derived);
|
|
73
|
+
(definition.SubscribesToAllEvents === undefined).should.be.true;
|
|
74
|
+
definition.All!.should.deep.equal({ Properties: { title: 'name' }, IncludeChildren: false, AutoMap: 0 });
|
|
75
|
+
class EveryModel extends Model {}
|
|
76
|
+
fromEvery('name')(EveryModel.prototype, 'title');
|
|
77
|
+
(compileModelBound(EveryModel).definition.SubscribesToAllEvents === undefined).should.be.true;
|
|
78
|
+
});
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
describe('when both all-event decorators target the same legacy property', () => {
|
|
82
|
+
it('should preserve fromEvery mapping precedence', () => {
|
|
83
|
+
class Candidate extends Model {}
|
|
84
|
+
fromEvery('oldName')(Candidate.prototype, 'title');
|
|
85
|
+
fromAll('newName')(Candidate.prototype, 'title');
|
|
86
|
+
const { definition } = compileModelBound(Candidate);
|
|
87
|
+
(definition.SubscribesToAllEvents === undefined).should.be.true;
|
|
88
|
+
definition.All!.Properties.title.should.equal('oldName');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
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 { field } from '@cratis/fundamentals';
|
|
5
|
+
import { chai, describe, it, type Assertion } from 'vitest';
|
|
6
|
+
import { fromAll } from '../modelBound/fromAll.js';
|
|
7
|
+
import { fromEvery } from '../modelBound/fromEvery.js';
|
|
8
|
+
import { ReadModelScenario } from '../../testing/ReadModelScenario.js';
|
|
9
|
+
import { UnsupportedProjectionOperation } from '../../testing/projections/UnsupportedProjectionOperation.js';
|
|
10
|
+
import { compileModelBound } from './given/compile_model_bound.js';
|
|
11
|
+
|
|
12
|
+
chai.should();
|
|
13
|
+
function should(value: unknown): Assertion { return (value as { should: Assertion }).should; }
|
|
14
|
+
|
|
15
|
+
class AllModel {
|
|
16
|
+
@field(String) id = '';
|
|
17
|
+
@field(String) @fromAll('name') title = '';
|
|
18
|
+
@field(String) @fromAll(undefined, 'sequenceNumber') lastType = '';
|
|
19
|
+
@field(String) @fromEvery(undefined, 'eventSourceId') source = '';
|
|
20
|
+
}
|
|
21
|
+
class EveryModel {
|
|
22
|
+
@field(String) id = '';
|
|
23
|
+
@field(String) @fromEvery('name') title = '';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
describe('when compiling standard fromAll without explicit event subscriptions', () => {
|
|
27
|
+
it('should emit restricted shared mappings without constructing the model', () => {
|
|
28
|
+
const { definition } = compileModelBound(AllModel);
|
|
29
|
+
should(definition.SubscribesToAllEvents === undefined).be.true;
|
|
30
|
+
should(definition.From).deep.equal([]);
|
|
31
|
+
should(definition.FromEvery).deep.equal([]);
|
|
32
|
+
should(definition.All).deep.equal({ Properties: {
|
|
33
|
+
title: 'name', lastType: '$eventContext(SequenceNumber)', source: '$eventContext(EventSourceId)'
|
|
34
|
+
}, IncludeChildren: false, AutoMap: 0 });
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should leave fromEvery subscriptions and child behavior unchanged', () => {
|
|
38
|
+
const { definition } = compileModelBound(EveryModel);
|
|
39
|
+
should(definition.SubscribesToAllEvents === undefined).be.true;
|
|
40
|
+
should(definition.All).deep.equal({ Properties: { title: 'name' }, IncludeChildren: false, AutoMap: 0 });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should inherit the restricted mappings without expanding subscriptions', () => {
|
|
44
|
+
class Derived extends AllModel {}
|
|
45
|
+
const { definition } = compileModelBound(Derived);
|
|
46
|
+
should(definition.SubscribesToAllEvents === undefined).be.true;
|
|
47
|
+
should(definition.All).deep.equal(compileModelBound(AllModel).definition.All);
|
|
48
|
+
should(compileModelBound(EveryModel).definition.SubscribesToAllEvents === undefined).be.true;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it('should reject the shared mappings before replay with the standard decorator declaration', () => {
|
|
52
|
+
class Candidate {
|
|
53
|
+
@field(String) id = '';
|
|
54
|
+
@field(String) @fromAll('name') title = '';
|
|
55
|
+
}
|
|
56
|
+
should(() => new ReadModelScenario(Candidate, { readModels: [Candidate], eventTypes: [], projections: [], reducers: [] }))
|
|
57
|
+
.throw(UnsupportedProjectionOperation, 'All (@fromAll)');
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should preserve fromEvery precedence when both decorators target one property', () => {
|
|
61
|
+
class Both {
|
|
62
|
+
@field(String) id = '';
|
|
63
|
+
@field(String) @fromEvery('oldName') @fromAll('newName') title = '';
|
|
64
|
+
}
|
|
65
|
+
const { definition } = compileModelBound(Both);
|
|
66
|
+
should(definition.SubscribesToAllEvents === undefined).be.true;
|
|
67
|
+
should(definition.All!.Properties.title).equal('oldName');
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -20,7 +20,8 @@ const METADATA_KEY = 'chronicle:projection:fromAll';
|
|
|
20
20
|
* Property decorator that sets the decorated read model property from a property present on every
|
|
21
21
|
* event type the projection is built from - the convention-based counterpart to declaring the same
|
|
22
22
|
* mapping on every individual `fromEvent`. Equivalent to {@link fromEvery} for model-bound
|
|
23
|
-
* projections
|
|
23
|
+
* projections.
|
|
24
|
+
* @deprecated In the next major, @fromAll subscribes to every event type like fluent fromAll() and .NET [FromAll]; use @fromEvery to keep the current behavior.
|
|
24
25
|
* @param property - Optional event property name. If not specified, uses the model property name.
|
|
25
26
|
* @param contextProperty - Optional event context property name.
|
|
26
27
|
* @returns A property decorator.
|
|
@@ -265,7 +265,7 @@ describe('when appending batches against committed kernel fixtures', () => {
|
|
|
265
265
|
error => { (error instanceof UnsupportedEventSequenceOperation).should.be.true; }
|
|
266
266
|
);
|
|
267
267
|
await first;
|
|
268
|
-
for (const options of [{ subject:
|
|
268
|
+
for (const options of [{ subject: 42 as unknown as string }, { tags: ['invalid|tag'] },
|
|
269
269
|
{ concurrencyScope: {} as never }]) {
|
|
270
270
|
await scenario.appendMany('B', [new OracleEventRecorded('second', false)], options).then(
|
|
271
271
|
() => { throw new Error('Unproven metadata accepted'); },
|
|
@@ -450,7 +450,7 @@ describe('when appending batches against committed kernel fixtures', () => {
|
|
|
450
450
|
['appendMany.correlationId', () => scenario.appendMany('B', [event()], { correlationId: 'bad-guid' })],
|
|
451
451
|
['appendMany.entry', () => scenario.appendMany([entry({ unknown: 'value' })])],
|
|
452
452
|
['appendMany.sourceType', () => scenario.appendMany([entry({ eventSourceType: 'bad source' })])],
|
|
453
|
-
['appendMany.subject', () => scenario.appendMany([entry({ subject:
|
|
453
|
+
['appendMany.subject', () => scenario.appendMany([entry({ subject: 42 })], { subject: 'shared' })],
|
|
454
454
|
['appendMany.streamType', () => scenario.appendMany([entry({ eventStreamType: 'bad stream' })])],
|
|
455
455
|
['appendMany.streamId', () => scenario.appendMany([entry({ eventStreamId: 'bad stream' })])],
|
|
456
456
|
['appendMany.event', () => scenario.appendMany('B', [new UnregisteredRecorded()])],
|
|
@@ -219,11 +219,11 @@ describe('when appending against committed kernel event fixtures', () => {
|
|
|
219
219
|
it('should reject prototype getters and non-enumerable append metadata without mutation', async () => {
|
|
220
220
|
const scenario = makeScenario();
|
|
221
221
|
class OptionsWithGetters {
|
|
222
|
-
get
|
|
222
|
+
get occurred() { return new Date(); }
|
|
223
223
|
get streamId() { return 'different-stream'; }
|
|
224
224
|
}
|
|
225
225
|
await unsupportedAsync(() => scenario.append('A', new OracleEventRecorded('one', true), new OptionsWithGetters()), 'append.options');
|
|
226
|
-
const hidden = Object.defineProperty({}, '
|
|
226
|
+
const hidden = Object.defineProperty({}, 'occurred', { value: new Date(), enumerable: false });
|
|
227
227
|
await unsupportedAsync(() => scenario.append('A', new OracleEventRecorded('one', true), hidden), 'append.options');
|
|
228
228
|
scenario.appendedEvents.length.should.equal(0);
|
|
229
229
|
scenario.results.length.should.equal(0);
|
|
@@ -233,9 +233,9 @@ describe('when the fixture-bounded event sequence encounters unproven operations
|
|
|
233
233
|
await rejects(() => subject.append('A', new Recorded(), { correlationId: '00000000-0000-0000-0000-000000000001' }), 'append.options');
|
|
234
234
|
clean(subject);
|
|
235
235
|
});
|
|
236
|
-
it('should reject
|
|
236
|
+
it('should reject non-string subject options', async () => {
|
|
237
237
|
const subject = scenario();
|
|
238
|
-
await rejects(() => subject.append('A', new Recorded(), { subject:
|
|
238
|
+
await rejects(() => subject.append('A', new Recorded(), { subject: 42 as unknown as string }), 'append.subject');
|
|
239
239
|
clean(subject);
|
|
240
240
|
});
|
|
241
241
|
it('should reject empty source/type-read event types', async () => {
|
|
@@ -37,6 +37,11 @@ import { UnsupportedEventSequenceOperation } from './UnsupportedEventSequenceOpe
|
|
|
37
37
|
// JS trim() omits U+0085, which the kernel trims. Reject unproven non-ASCII whitespace and controls.
|
|
38
38
|
const unprovenFilterCharacters = /[\u007f-\u009f]|(?=[^\x00-\x7f])\p{White_Space}/u;
|
|
39
39
|
|
|
40
|
+
function resolveStoredSubject(subject: string | undefined, eventSourceId: string): string {
|
|
41
|
+
// Match the kernel's string.IsNullOrWhiteSpace, including U+0085 but not U+FEFF.
|
|
42
|
+
return subject && !/^\p{White_Space}*$/u.test(subject) ? subject : eventSourceId;
|
|
43
|
+
}
|
|
44
|
+
|
|
40
45
|
function matchesConstrainedValue(value: unknown, content: unknown): boolean {
|
|
41
46
|
// Use production serialization for concepts, not their wrapper object's runtime type.
|
|
42
47
|
// Keep rejecting other unproven conversions, such as a Date assigned to a string field.
|
|
@@ -168,13 +173,13 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
168
173
|
try {
|
|
169
174
|
if (!event || typeof event !== 'object') throw this.unsupported('append.event', this.id.value, 'Only registered event instances are supported.');
|
|
170
175
|
if (!/^[A-Za-z0-9_-]+$/.test(eventSourceId)) throw this.unsupported('append.source', eventSourceId, 'Only simple source identifiers are fixture-backed.');
|
|
171
|
-
const route = singleAppendRoute(options, event.constructor.name, (operation, artifact, reason) => this.unsupported(operation, artifact, reason));
|
|
176
|
+
const route = singleAppendRoute(options, event.constructor.name, (operation, artifact, reason) => this.unsupported(operation, artifact, reason), true);
|
|
172
177
|
const metadata = this._catalog.get(event.constructor);
|
|
173
178
|
if (!metadata) throw this.unsupported('append.event', event.constructor.name, 'Event is not in the selected, validated catalog.');
|
|
174
179
|
let prepared: ReturnType<typeof prepareSingleAppend>;
|
|
175
180
|
let content: Record<string, unknown>;
|
|
176
181
|
try {
|
|
177
|
-
prepared = prepareSingleAppend(event,
|
|
182
|
+
prepared = prepareSingleAppend(event, { subject: options?.subject, correlationId: this._correlationId?.() });
|
|
178
183
|
content = this.checkedContent(event, prepared.content, 'append.content',
|
|
179
184
|
'Content differs from the event schema; null, missing and extra values are not proven.');
|
|
180
185
|
} catch (error) {
|
|
@@ -207,7 +212,7 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
207
212
|
eventStore: this._store, namespace: this._namespace,
|
|
208
213
|
sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
209
214
|
eventSourceType: route.sourceType, eventStreamType: route.streamType, eventStreamId: route.streamId,
|
|
210
|
-
subject: eventSourceId, hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
|
|
215
|
+
subject: resolveStoredSubject(prepared.subject, eventSourceId), hash, causedBy: prepared.identity, observationState: EventObservationState.Initial,
|
|
211
216
|
eventType: prepared.eventType, occurred, correlationId: prepared.correlationId.toString(),
|
|
212
217
|
causation: prepared.causationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
213
218
|
tags: prepared.tags.map(tag => new Tag(tag))
|
|
@@ -254,6 +259,9 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
254
259
|
}
|
|
255
260
|
const individual = entry as EventForEventSourceId;
|
|
256
261
|
if (individual.occurred !== undefined) this.checkedDate(individual.occurred, 'appendMany.occurred');
|
|
262
|
+
if (individual.subject !== undefined && individual.subject !== null && typeof individual.subject !== 'string') {
|
|
263
|
+
throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
|
|
264
|
+
}
|
|
257
265
|
const tags = individual.tags;
|
|
258
266
|
if (tags && (!Array.isArray(tags) || tags.some(tag => typeof tag !== 'string'))) {
|
|
259
267
|
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
@@ -288,8 +296,8 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
288
296
|
this.validateSource(eventSourceId, 'appendMany.source');
|
|
289
297
|
const wire = eventsToAppend[index];
|
|
290
298
|
for (const [name, value] of Object.entries({ sourceType: wire.EventSourceType, streamType: wire.EventStreamType,
|
|
291
|
-
streamId: wire.EventStreamId
|
|
292
|
-
const identifiers =
|
|
299
|
+
streamId: wire.EventStreamId })) {
|
|
300
|
+
const identifiers = /^[A-Za-z0-9_-]*$/;
|
|
293
301
|
if (value !== undefined && (typeof value !== 'string' || !identifiers.test(value))) {
|
|
294
302
|
throw this.unsupported(`appendMany.${name}`, String(value), 'Only simple metadata identifiers are fixture-backed.');
|
|
295
303
|
}
|
|
@@ -307,7 +315,7 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
307
315
|
const stored: AppendedEvent = { eventType, content, context: {
|
|
308
316
|
eventStore: this._store, namespace: this._namespace, sequenceNumber: sequenceNumber.value, eventSourceId,
|
|
309
317
|
eventSourceType: wire.EventSourceType || 'Default', eventStreamType: wire.EventStreamType || 'All',
|
|
310
|
-
eventStreamId: wire.EventStreamId || 'Default', subject: wire.Subject, hash, causedBy: identity,
|
|
318
|
+
eventStreamId: wire.EventStreamId || 'Default', subject: resolveStoredSubject(wire.Subject, eventSourceId), hash, causedBy: identity,
|
|
311
319
|
observationState: EventObservationState.Initial, eventType, occurred, correlationId: correlationId.toString(),
|
|
312
320
|
causation: batchCausationChain.map(item => ({ type: item.type.name, occurred: item.occurred, properties: { ...item.properties } })),
|
|
313
321
|
tags: wire.Tags.map(tag => new Tag(tag))
|
|
@@ -408,6 +416,9 @@ export class InProcessEventSequence implements IEventSequence {
|
|
|
408
416
|
options.concurrencyScope !== undefined || options.concurrencyScopes !== undefined || options.eventSourceId !== undefined) {
|
|
409
417
|
throw this.unsupported('appendMany.options', this.id.value, 'Concurrency and unrecognized metadata are not fixture-backed.');
|
|
410
418
|
}
|
|
419
|
+
if (options.subject !== undefined && options.subject !== null && typeof options.subject !== 'string') {
|
|
420
|
+
throw this.unsupported('appendMany.subject', this.id.value, 'Subject must be a string.');
|
|
421
|
+
}
|
|
411
422
|
if (options.tags && (!Array.isArray(options.tags) || options.tags.some(tag => typeof tag !== 'string'))) {
|
|
412
423
|
throw this.unsupported('appendMany.tags', this.id.value, 'Only plain string tags are fixture-backed.');
|
|
413
424
|
}
|