@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.
Files changed (94) hide show
  1. package/compliance/for_PIIManager/against_a_kernel/when_erasing_a_subject.integration.spec.ts +4 -4
  2. package/compliance/resolveEventSubject.ts +19 -0
  3. package/compliance/subject.ts +2 -2
  4. package/dist/compliance/resolveEventSubject.d.ts +3 -0
  5. package/dist/compliance/resolveEventSubject.d.ts.map +1 -0
  6. package/dist/compliance/resolveEventSubject.js +21 -0
  7. package/dist/compliance/resolveEventSubject.js.map +1 -0
  8. package/dist/compliance/subject.d.ts +2 -2
  9. package/dist/compliance/subject.js +2 -2
  10. package/dist/eventSequences/AppendOptions.d.ts +1 -1
  11. package/dist/eventSequences/AppendOptions.d.ts.map +1 -1
  12. package/dist/eventSequences/EventForEventSourceId.d.ts +1 -1
  13. package/dist/eventSequences/EventForEventSourceId.d.ts.map +1 -1
  14. package/dist/eventSequences/EventSequence.js +2 -2
  15. package/dist/eventSequences/EventSequence.js.map +1 -1
  16. package/dist/eventSequences/prepareBatchAppend.d.ts.map +1 -1
  17. package/dist/eventSequences/prepareBatchAppend.js +2 -1
  18. package/dist/eventSequences/prepareBatchAppend.js.map +1 -1
  19. package/dist/eventSequences/prepareSingleAppend.d.ts +1 -0
  20. package/dist/eventSequences/prepareSingleAppend.d.ts.map +1 -1
  21. package/dist/eventSequences/prepareSingleAppend.js +3 -1
  22. package/dist/eventSequences/prepareSingleAppend.js.map +1 -1
  23. package/dist/projections/captureProjectionProvenance.d.ts.map +1 -1
  24. package/dist/projections/captureProjectionProvenance.js +2 -0
  25. package/dist/projections/captureProjectionProvenance.js.map +1 -1
  26. package/dist/projections/declarative/FromAllBuilder.d.ts +18 -0
  27. package/dist/projections/declarative/FromAllBuilder.d.ts.map +1 -0
  28. package/dist/projections/declarative/FromAllBuilder.js +36 -0
  29. package/dist/projections/declarative/FromAllBuilder.js.map +1 -0
  30. package/dist/projections/declarative/IFromAllBuilder.d.ts +33 -0
  31. package/dist/projections/declarative/IFromAllBuilder.d.ts.map +1 -0
  32. package/dist/projections/declarative/IFromAllBuilder.js +4 -0
  33. package/dist/projections/declarative/IFromAllBuilder.js.map +1 -0
  34. package/dist/projections/declarative/IProjectionBuilderFor.d.ts +8 -0
  35. package/dist/projections/declarative/IProjectionBuilderFor.d.ts.map +1 -1
  36. package/dist/projections/declarative/ProjectionBuilderFor.d.ts +4 -0
  37. package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
  38. package/dist/projections/declarative/ProjectionBuilderFor.js +15 -0
  39. package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
  40. package/dist/projections/declarative/index.d.ts +2 -0
  41. package/dist/projections/declarative/index.d.ts.map +1 -1
  42. package/dist/projections/declarative/index.js +1 -0
  43. package/dist/projections/declarative/index.js.map +1 -1
  44. package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts +6 -0
  45. package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts.map +1 -0
  46. package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js +12 -0
  47. package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js.map +1 -0
  48. package/dist/projections/modelBound/fromAll.d.ts +2 -1
  49. package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
  50. package/dist/projections/modelBound/fromAll.js +2 -1
  51. package/dist/projections/modelBound/fromAll.js.map +1 -1
  52. package/dist/testing/InProcessEventSequence.d.ts.map +1 -1
  53. package/dist/testing/InProcessEventSequence.js +16 -6
  54. package/dist/testing/InProcessEventSequence.js.map +1 -1
  55. package/dist/testing/ReadModelScenario.d.ts.map +1 -1
  56. package/dist/testing/ReadModelScenario.js +4 -0
  57. package/dist/testing/ReadModelScenario.js.map +1 -1
  58. package/dist/testing/singleAppendRoute.d.ts +1 -1
  59. package/dist/testing/singleAppendRoute.d.ts.map +1 -1
  60. package/dist/testing/singleAppendRoute.js +8 -4
  61. package/dist/testing/singleAppendRoute.js.map +1 -1
  62. package/dist/tsconfig.tsbuildinfo +1 -1
  63. package/eventSequences/AppendOptions.ts +1 -1
  64. package/eventSequences/EventForEventSourceId.ts +1 -1
  65. package/eventSequences/EventSequence.ts +2 -2
  66. package/eventSequences/for_EventSequence/when_appending/subject_behaviors.fixture.ts +239 -0
  67. package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.spec.ts +34 -0
  68. package/eventSequences/for_EventSequence/when_appending/with_subject_annotations.standard.spec.ts +38 -0
  69. package/eventSequences/prepareBatchAppend.ts +2 -1
  70. package/eventSequences/prepareSingleAppend.ts +3 -1
  71. package/package.json +2 -2
  72. package/projections/captureProjectionProvenance.ts +1 -0
  73. package/projections/declarative/FromAllBuilder.ts +49 -0
  74. package/projections/declarative/IFromAllBuilder.ts +39 -0
  75. package/projections/declarative/IProjectionBuilderFor.ts +9 -0
  76. package/projections/declarative/ProjectionBuilderFor.ts +17 -0
  77. package/projections/declarative/for_ProjectionBuilderFor/when_subscribing_to_all_events.spec.ts +111 -0
  78. package/projections/declarative/index.ts +2 -0
  79. package/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.ts +15 -0
  80. package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.spec.ts +90 -0
  81. package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts +69 -0
  82. package/projections/modelBound/fromAll.ts +2 -1
  83. package/testing/EventScenario.batches.spec.ts +2 -2
  84. package/testing/EventScenario.fixtures.spec.ts +2 -2
  85. package/testing/EventScenario.rejections.spec.ts +2 -2
  86. package/testing/InProcessEventSequence.ts +17 -6
  87. package/testing/ReadModelScenario.spec.ts +31 -0
  88. package/testing/ReadModelScenario.ts +4 -0
  89. package/testing/for_EventScenario/when_appending_scoped_constraints/with_unproven_operations.spec.ts +1 -1
  90. package/testing/for_ReadModelScenario/when_seeding_with_options/with_route_validation.spec.ts +11 -1
  91. package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +5 -0
  92. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +2 -0
  93. package/testing/singleAppendRoute.spec.ts +19 -0
  94. package/testing/singleAppendRoute.ts +9 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cratis/chronicle",
3
- "version": "6.41.0",
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,
@@ -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; use whichever name reads better for the property being mapped.
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: 'bad subject' }, { tags: ['invalid|tag'] },
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: '' })], { subject: 'shared' })],
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 subject() { return 'different-subject'; }
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({}, 'subject', { value: 'different-subject', enumerable: false });
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 plain subject options', async () => {
236
+ it('should reject non-string subject options', async () => {
237
237
  const subject = scenario();
238
- await rejects(() => subject.append('A', new Recorded(), { subject: 'other' }), 'append.options');
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, this._correlationId ? { correlationId: this._correlationId() } : undefined);
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, subject: wire.Subject })) {
292
- const identifiers = name === 'subject' ? /^[A-Za-z0-9_-]+$/ : /^[A-Za-z0-9_-]*$/;
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
  }