@cratis/chronicle 6.16.1 → 6.18.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 (141) hide show
  1. package/artifacts/ActivatedArtifact.ts +5 -1
  2. package/artifacts/ArtifactCompletionFailed.ts +20 -0
  3. package/artifacts/ArtifactInvocationContext.ts +21 -0
  4. package/artifacts/for_withActivatedArtifact/when_completing/after_processing_failure.spec.ts +34 -0
  5. package/artifacts/index.ts +2 -0
  6. package/artifacts/withActivatedArtifact.ts +20 -3
  7. package/dist/artifacts/ActivatedArtifact.d.ts +4 -1
  8. package/dist/artifacts/ActivatedArtifact.d.ts.map +1 -1
  9. package/dist/artifacts/ArtifactCompletionFailed.d.ts +9 -0
  10. package/dist/artifacts/ArtifactCompletionFailed.d.ts.map +1 -0
  11. package/dist/artifacts/ArtifactCompletionFailed.js +19 -0
  12. package/dist/artifacts/ArtifactCompletionFailed.js.map +1 -0
  13. package/dist/artifacts/ArtifactInvocationContext.d.ts +18 -0
  14. package/dist/artifacts/ArtifactInvocationContext.d.ts.map +1 -0
  15. package/dist/artifacts/ArtifactInvocationContext.js +4 -0
  16. package/dist/artifacts/ArtifactInvocationContext.js.map +1 -0
  17. package/dist/artifacts/index.d.ts +2 -0
  18. package/dist/artifacts/index.d.ts.map +1 -1
  19. package/dist/artifacts/index.js +1 -0
  20. package/dist/artifacts/index.js.map +1 -1
  21. package/dist/artifacts/withActivatedArtifact.d.ts +2 -1
  22. package/dist/artifacts/withActivatedArtifact.d.ts.map +1 -1
  23. package/dist/artifacts/withActivatedArtifact.js +22 -3
  24. package/dist/artifacts/withActivatedArtifact.js.map +1 -1
  25. package/dist/projections/CompiledProjectionDefinitions.d.ts +6 -0
  26. package/dist/projections/CompiledProjectionDefinitions.d.ts.map +1 -1
  27. package/dist/projections/ProjectionCapabilityProvenance.d.ts +8 -0
  28. package/dist/projections/ProjectionCapabilityProvenance.d.ts.map +1 -0
  29. package/dist/projections/ProjectionCapabilityProvenance.js +4 -0
  30. package/dist/projections/ProjectionCapabilityProvenance.js.map +1 -0
  31. package/dist/projections/ProjectionDefinitionCompiler.d.ts +1 -0
  32. package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
  33. package/dist/projections/ProjectionDefinitionCompiler.js +130 -4
  34. package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
  35. package/dist/projections/ProjectionEventSchema.d.ts +10 -0
  36. package/dist/projections/ProjectionEventSchema.d.ts.map +1 -0
  37. package/dist/projections/ProjectionEventSchema.js +4 -0
  38. package/dist/projections/ProjectionEventSchema.js.map +1 -0
  39. package/dist/projections/VariantReclassifier.d.ts +2 -0
  40. package/dist/projections/VariantReclassifier.d.ts.map +1 -1
  41. package/dist/projections/VariantReclassifier.js.map +1 -1
  42. package/dist/projections/captureProjectionProvenance.d.ts +4 -0
  43. package/dist/projections/captureProjectionProvenance.d.ts.map +1 -0
  44. package/dist/projections/captureProjectionProvenance.js +56 -0
  45. package/dist/projections/captureProjectionProvenance.js.map +1 -0
  46. package/dist/projections/declarative/AddChildBuilder.d.ts +13 -2
  47. package/dist/projections/declarative/AddChildBuilder.d.ts.map +1 -1
  48. package/dist/projections/declarative/AddChildBuilder.js +27 -1
  49. package/dist/projections/declarative/AddChildBuilder.js.map +1 -1
  50. package/dist/projections/declarative/FromBuilder.d.ts +4 -0
  51. package/dist/projections/declarative/FromBuilder.d.ts.map +1 -1
  52. package/dist/projections/declarative/FromBuilder.js +10 -2
  53. package/dist/projections/declarative/FromBuilder.js.map +1 -1
  54. package/dist/projections/declarative/IAddChildBuilder.d.ts +12 -0
  55. package/dist/projections/declarative/IAddChildBuilder.d.ts.map +1 -1
  56. package/dist/projections/declarative/IReadModelPropertiesBuilder.d.ts +16 -1
  57. package/dist/projections/declarative/IReadModelPropertiesBuilder.d.ts.map +1 -1
  58. package/dist/projections/declarative/JoinBuilder.d.ts +4 -0
  59. package/dist/projections/declarative/JoinBuilder.d.ts.map +1 -1
  60. package/dist/projections/declarative/JoinBuilder.js +1 -2
  61. package/dist/projections/declarative/JoinBuilder.js.map +1 -1
  62. package/dist/projections/declarative/ProjectionBuilderCore.d.ts.map +1 -1
  63. package/dist/projections/declarative/ProjectionBuilderCore.js +9 -1
  64. package/dist/projections/declarative/ProjectionBuilderCore.js.map +1 -1
  65. package/dist/projections/declarative/projectionBuilderProvenance.d.ts +16 -0
  66. package/dist/projections/declarative/projectionBuilderProvenance.d.ts.map +1 -0
  67. package/dist/projections/declarative/projectionBuilderProvenance.js +28 -0
  68. package/dist/projections/declarative/projectionBuilderProvenance.js.map +1 -0
  69. package/dist/projections/eventContractPath.d.ts +4 -0
  70. package/dist/projections/eventContractPath.d.ts.map +1 -0
  71. package/dist/projections/eventContractPath.js +7 -0
  72. package/dist/projections/eventContractPath.js.map +1 -0
  73. package/dist/projections/modelBound/childrenAndNestedBuilder.d.ts.map +1 -1
  74. package/dist/projections/modelBound/childrenAndNestedBuilder.js +41 -10
  75. package/dist/projections/modelBound/childrenAndNestedBuilder.js.map +1 -1
  76. package/dist/reactors/Reactors.d.ts.map +1 -1
  77. package/dist/reactors/Reactors.js +12 -3
  78. package/dist/reactors/Reactors.js.map +1 -1
  79. package/dist/reducers/Reducers.d.ts.map +1 -1
  80. package/dist/reducers/Reducers.js +12 -3
  81. package/dist/reducers/Reducers.js.map +1 -1
  82. package/dist/testing/projections/ProjectionCapabilities.d.ts +17 -0
  83. package/dist/testing/projections/ProjectionCapabilities.d.ts.map +1 -0
  84. package/dist/testing/projections/ProjectionCapabilities.js +233 -0
  85. package/dist/testing/projections/ProjectionCapabilities.js.map +1 -0
  86. package/dist/testing/projections/UnsupportedProjectionOperation.d.ts +11 -0
  87. package/dist/testing/projections/UnsupportedProjectionOperation.d.ts.map +1 -0
  88. package/dist/testing/projections/UnsupportedProjectionOperation.js +16 -0
  89. package/dist/testing/projections/UnsupportedProjectionOperation.js.map +1 -0
  90. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts +7 -0
  91. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.d.ts.map +1 -0
  92. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js +16 -0
  93. package/dist/testing/projections/for_ProjectionCapabilities/given/Changed.js.map +1 -0
  94. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts +10 -0
  95. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.d.ts.map +1 -0
  96. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js +20 -0
  97. package/dist/testing/projections/for_ProjectionCapabilities/given/Model.js.map +1 -0
  98. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts +3 -0
  99. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.d.ts.map +1 -0
  100. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js +7 -0
  101. package/dist/testing/projections/for_ProjectionCapabilities/given/Removed.js.map +1 -0
  102. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts +12 -0
  103. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.d.ts.map +1 -0
  104. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js +34 -0
  105. package/dist/testing/projections/for_ProjectionCapabilities/given/compile.js.map +1 -0
  106. package/dist/tsconfig.tsbuildinfo +1 -1
  107. package/package.json +2 -2
  108. package/projections/CompiledProjectionDefinitions.ts +6 -0
  109. package/projections/ProjectionCapabilityProvenance.ts +10 -0
  110. package/projections/ProjectionDefinitionCompiler.registration.spec.ts +4 -2
  111. package/projections/ProjectionDefinitionCompiler.ts +127 -4
  112. package/projections/ProjectionEventSchema.ts +13 -0
  113. package/projections/Projections.childrenAndNested.spec.ts +250 -1
  114. package/projections/VariantReclassifier.ts +2 -0
  115. package/projections/captureProjectionProvenance.ts +50 -0
  116. package/projections/declarative/AddChildBuilder.ts +33 -2
  117. package/projections/declarative/FromBuilder.ts +18 -1
  118. package/projections/declarative/IAddChildBuilder.ts +14 -0
  119. package/projections/declarative/IReadModelPropertiesBuilder.ts +18 -1
  120. package/projections/declarative/JoinBuilder.ts +9 -1
  121. package/projections/declarative/ProjectionBuilderCore.ts +8 -1
  122. package/projections/declarative/addChildCompatibility.standard.spec.ts +80 -0
  123. package/projections/declarative/projectionBuilderProvenance.ts +40 -0
  124. package/projections/eventContextExpressions.spec.ts +20 -0
  125. package/projections/eventContractPath.ts +9 -0
  126. package/projections/modelBound/childrenAndNestedBuilder.ts +45 -11
  127. package/reactors/Reactors.ts +13 -3
  128. package/reactors/for_Reactors/when_delivering/with_activation.spec.ts +93 -2
  129. package/reducers/Reducers.ts +14 -3
  130. package/reducers/for_Reducers/when_delivering/with_activation.spec.ts +67 -1
  131. package/testing/projections/ProjectionCapabilities.ts +213 -0
  132. package/testing/projections/UnsupportedProjectionOperation.ts +16 -0
  133. package/testing/projections/for_ProjectionCapabilities/given/Changed.ts +12 -0
  134. package/testing/projections/for_ProjectionCapabilities/given/Model.ts +13 -0
  135. package/testing/projections/for_ProjectionCapabilities/given/Removed.ts +7 -0
  136. package/testing/projections/for_ProjectionCapabilities/given/compile.ts +39 -0
  137. package/testing/projections/for_ProjectionCapabilities/when_compiling_capability_evidence.spec.ts +68 -0
  138. package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +230 -0
  139. package/testing/projections/for_ProjectionCapabilities/when_validating_numeric_schemas.spec.ts +73 -0
  140. package/testing/projections/for_ProjectionCapabilities/when_validating_schema_boundaries.spec.ts +118 -0
  141. package/testing/projections/for_ProjectionCapabilities/when_validating_supported_operations.spec.ts +103 -0
@@ -0,0 +1,68 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { chai, describe, it } from 'vitest';
6
+ import { childrenFrom } from '../../../projections/modelBound/childrenFrom.js';
7
+ import { entersOn } from '../../../projections/modelBound/entersOn.js';
8
+ import { fromEvent } from '../../../projections/modelBound/fromEvent.js';
9
+ import { variantOf } from '../../../projections/modelBound/variantOf.js';
10
+ import { getEventTypeJsonSchemaFor } from '../../../events/eventTypeDecorator.js';
11
+ import { ProjectionDefinitionCompiler } from '../../../projections/ProjectionDefinitionCompiler.js';
12
+ import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
13
+ import { Changed } from './given/Changed.js';
14
+ import { Removed } from './given/Removed.js';
15
+ import { Model } from './given/Model.js';
16
+ import { compileDeclarative, compileModelBound } from './given/compile.js';
17
+
18
+ chai.should();
19
+
20
+ describe('when compiling capability evidence', () => {
21
+ it('should retain model-bound variant declarations before joins and sibling removal obscure them', () => {
22
+ class Identity {}
23
+ class First extends Model {}
24
+ class Second extends Model {}
25
+ fromEvent(Changed)(First);
26
+ fromEvent(Removed)(Second);
27
+ variantOf(Identity, 'id')(First);
28
+ variantOf(Identity, 'id')(Second);
29
+ entersOn(Changed)(First);
30
+ entersOn(Removed)(Second);
31
+ const artifacts: IClientArtifactsProvider = {
32
+ readModels: [First, Second], projections: [], eventTypes: [Changed, Removed], globalForHandlers: [],
33
+ reactors: [], reducers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
34
+ };
35
+ const compiled = new ProjectionDefinitionCompiler(artifacts, 'test-sink').compile([], [First, Second]);
36
+ const first = compiled.definitions[0];
37
+ compiled.provenance.get(first)!.should.deep.include({ contractPath: 'Variant', declaration: '@variantOf' });
38
+ compiled.provenance.get(first)!.should.deep.include({ contractPath: 'EntersOn[capability-changed:1]', declaration: '@entersOn' });
39
+ compiled.eventSchemas.get(first)!.has('capability-removed:1:0').should.be.true;
40
+ (first.RemovedWith?.length ?? 0).should.equal(1);
41
+ });
42
+
43
+ it('should retain declarative variant provenance before update-only joins replace From', () => {
44
+ class Identity {}
45
+ const { compiled, definition } = compileDeclarative(builder => builder
46
+ .variantOf(Identity, model => model.id).entersOn(Changed).from(Changed).from(Removed));
47
+ compiled.provenance.get(definition)!.should.deep.include({ contractPath: 'Variant', declaration: '.variantOf' });
48
+ compiled.provenance.get(definition)!.should.deep.include({ contractPath: 'From[capability-removed:1]', declaration: '.from' });
49
+ (definition.Join?.length ?? 0).should.equal(1);
50
+ compiled.eventSchemas.get(definition)!.has('capability-removed:1:0').should.be.true;
51
+ });
52
+
53
+ it('should catalog child events even though no root From entry refers to them', () => {
54
+ const { compiled, definition } = compileModelBound(model => childrenFrom(Removed)(model.prototype, 'name'));
55
+ compiled.eventSchemas.get(definition)!.has('capability-removed:1:0').should.be.true;
56
+ compiled.provenance.get(definition)!.should.deep.include({ contractPath: 'Children.name', declaration: '@childrenFrom' });
57
+ });
58
+
59
+ it('should isolate event schemas from later compiles and registration metadata', () => {
60
+ const first = compileModelBound();
61
+ const second = compileModelBound();
62
+ const firstSchema = first.compiled.eventSchemas.get(first.definition)!.get('capability-changed:1:0')!.schema;
63
+ const secondSchema = second.compiled.eventSchemas.get(second.definition)!.get('capability-changed:1:0')!.schema;
64
+ firstSchema.properties!.name.type = 'boolean';
65
+ secondSchema.properties!.name.type.should.equal('string');
66
+ getEventTypeJsonSchemaFor(Changed).properties!.name.type.should.equal('string');
67
+ });
68
+ });
@@ -0,0 +1,230 @@
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 { chai, describe, it } from 'vitest';
7
+ import { eventType } from '../../../events/eventTypeDecorator.js';
8
+ import type { IProjectionBuilderFor } from '../../../projections/declarative/IProjectionBuilderFor.js';
9
+ import { childrenFrom } from '../../../projections/modelBound/childrenFrom.js';
10
+ import { entersOn } from '../../../projections/modelBound/entersOn.js';
11
+ import { eventSequence } from '../../../projections/modelBound/eventSequence.js';
12
+ import { fromEvent } from '../../../projections/modelBound/fromEvent.js';
13
+ import { fromAll } from '../../../projections/modelBound/fromAll.js';
14
+ import { fromEvery } from '../../../projections/modelBound/fromEvery.js';
15
+ import { join } from '../../../projections/modelBound/join.js';
16
+ import { nested } from '../../../projections/modelBound/nested.js';
17
+ import { addFrom } from '../../../projections/modelBound/addFrom.js';
18
+ import { passive } from '../../../projections/modelBound/passive.js';
19
+ import { removedWithJoin } from '../../../projections/modelBound/removedWithJoin.js';
20
+ import { removedWith } from '../../../projections/modelBound/removedWith.js';
21
+ import { variantOf } from '../../../projections/modelBound/variantOf.js';
22
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
23
+ import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
24
+ import { Changed } from './given/Changed.js';
25
+ import { Removed } from './given/Removed.js';
26
+ import { compileDeclarative, compileModelBound } from './given/compile.js';
27
+ import { Model } from './given/Model.js';
28
+
29
+ chai.should();
30
+
31
+ describe('when rejecting unsupported operations before any event is seeded', () => {
32
+ const modelBound = [
33
+ { name: 'children', configure: (model: Function) => childrenFrom(Removed)(model.prototype, 'name'), path: 'Children.name (@childrenFrom)', reason: 'children projections' },
34
+ { name: 'nested', configure: (model: Function) => nested(model.prototype, 'name'), path: 'Nested.name (@nested)', reason: 'nested projections' },
35
+ { name: 'join', configure: (model: Function) => join(Removed, 'name')(model.prototype, 'name'), path: 'Join[capability-removed:1] (@join)', reason: 'joins' },
36
+ { name: 'removedWithJoin', configure: (model: Function) => removedWithJoin(Removed)(model), path: 'RemovedWithJoin[capability-removed:1] (@removedWithJoin)', reason: 'removedWithJoin' },
37
+ { name: 'fromEvery', configure: (model: Function) => fromEvery('name')(model.prototype, 'name'), path: 'All (@fromEvery)', reason: 'fromEvery/all' },
38
+ { name: 'fromAll', configure: (model: Function) => fromAll('name')(model.prototype, 'name'), path: 'All (@fromAll)', reason: 'fromEvery/all' },
39
+ { name: 'passive', configure: (model: Function) => passive(model), path: 'IsActive (@passive)', reason: 'passive projections' },
40
+ { name: 'event sequence', configure: (model: Function) => eventSequence('custom')(model), path: 'EventSequenceId (@eventSequence)', reason: 'non-default event sequences' },
41
+ { name: 'custom key', configure: (model: Function) => fromEvent(Removed, { key: 'name' })(model), path: 'From[capability-removed:1].Key (@fromEvent)', reason: 'only $eventSourceId' },
42
+ { name: 'constant key', configure: (model: Function) => fromEvent(Removed, { constantKey: 'fixed' })(model), path: 'From[capability-removed:1].Key (@fromEvent)', reason: 'only $eventSourceId' },
43
+ { name: 'parent key', configure: (model: Function) => fromEvent(Removed, { parentKey: 'name' })(model), path: 'From[capability-removed:1].ParentKey (@fromEvent)', reason: 'parent keys' },
44
+ { name: 'removal key', configure: (model: Function) => removedWith(Removed, 'name')(model), path: 'RemovedWith[capability-removed:1].Key (@removedWith)', reason: 'only $eventSourceId' },
45
+ { name: 'removal parent key', configure: (model: Function) => removedWith(Removed, undefined, 'parent')(model), path: 'RemovedWith[capability-removed:1].ParentKey (@removedWith)', reason: 'parent keys' }
46
+ ];
47
+ for (const testCase of modelBound) {
48
+ it(`should reject model-bound ${testCase.name} with declaration and path`, () => {
49
+ const { compiled, definition } = compileModelBound(testCase.configure);
50
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
51
+ .with.property('message').that.includes(testCase.path).and.includes(testCase.reason);
52
+ });
53
+ }
54
+
55
+ it('should reject model-bound variants even before cross-wired removal is visible', () => {
56
+ class Identity {}
57
+ const { compiled, definition } = compileModelBound(model => {
58
+ variantOf(Identity, 'id')(model);
59
+ entersOn(Changed)(model);
60
+ });
61
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation,
62
+ 'Candidate, Variant (@variantOf): variants require a kernel-backed test');
63
+ });
64
+
65
+ it('should reject declarative variants with their original declaration', () => {
66
+ class Identity {}
67
+ const { compiled, definition } = compileDeclarative(builder => builder.variantOf(Identity, model => model.id).entersOn(Changed).from(Changed));
68
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation,
69
+ 'Candidate, Variant (.variantOf): variants require a kernel-backed test');
70
+ });
71
+
72
+ const declarative: Array<{ name: string; define: (builder: IProjectionBuilderFor<Model>) => void; path: string }> = [
73
+ { name: 'custom key', define: builder => { builder.from(Changed, from => from.usingKey(event => event.name)); }, path: 'From[capability-changed:1].Key (.from().usingKey)' },
74
+ { name: 'constant key', define: builder => { builder.from(Changed, from => from.usingConstantKey('fixed')); }, path: 'From[capability-changed:1].Key (.from().usingConstantKey)' },
75
+ { name: 'context key', define: builder => { builder.from(Changed, from => from.usingKeyFromContext('eventSourceId')); }, path: 'From[capability-changed:1].Key (.from().usingKeyFromContext)' },
76
+ { name: 'context parent key', define: builder => { builder.from(Changed, from => from.usingParentKeyFromContext('eventSourceId')); }, path: 'From[capability-changed:1].ParentKey (.from().usingParentKeyFromContext)' },
77
+ { name: 'composite key', define: builder => { builder.from(Changed, from => from.usingCompositeKey<{ name: string }>(key => key.set(target => target.name, event => event.name))); }, path: 'From[capability-changed:1].Key (.from().usingCompositeKey)' },
78
+ { name: 'parent key', define: builder => { builder.from(Changed, from => from.usingParentKey(event => event.name)); }, path: 'From[capability-changed:1].ParentKey (.from().usingParentKey)' },
79
+ { name: 'join', define: builder => { builder.from(Changed).join(Removed, join => join.on(model => model.id)); }, path: 'Join[capability-removed:1] (.join)' },
80
+ { name: 'children', define: builder => { builder.from(Changed).children(model => model.labels, child => child.from(Removed)); }, path: 'Children.labels (.children)' },
81
+ { name: 'nested', define: builder => { builder.from(Changed).nested(model => model.details, child => child.clearWith(Removed)); }, path: 'Nested.details (.nested)' },
82
+ { name: 'removedWithJoin', define: builder => { builder.from(Changed).removedWithJoin(Removed); }, path: 'RemovedWithJoin[capability-removed:1] (.removedWithJoin)' },
83
+ { name: 'removal key', define: builder => { builder.from(Changed).removedWith(Changed, removal => removal.usingKey(event => event.name)); }, path: 'RemovedWith[capability-changed:1].Key (.removedWith)' },
84
+ { name: 'fromEvery', define: builder => { builder.from(Changed).fromEvery(all => all.set(model => model.name).toEventSourceId()); }, path: 'All (.fromEvery)' },
85
+ { name: 'empty fromEvery', define: builder => { builder.from(Changed).fromEvery(all => all.excludeChildProjections()); }, path: 'All (.fromEvery)' },
86
+ { name: 'passive', define: builder => { builder.from(Changed).passive(); }, path: 'IsActive (.passive)' },
87
+ { name: 'event sequence', define: builder => { builder.from(Changed).fromEventSequence('custom'); }, path: 'EventSequenceId (.fromEventSequence)' }
88
+ ];
89
+ for (const testCase of declarative) {
90
+ it(`should reject declarative ${testCase.name} with declaration and path`, () => {
91
+ const { compiled, definition } = compileDeclarative(testCase.define);
92
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
93
+ .with.property('message').that.includes(testCase.path);
94
+ });
95
+ }
96
+
97
+ it('should reject the same event-type id with multiple declared generations', () => {
98
+ class NewGeneration { name!: string; }
99
+ field(String)(NewGeneration.prototype, 'name');
100
+ eventType('capability-changed', 2)(NewGeneration);
101
+ const { compiled, definition } = compileModelBound(model => fromEvent(NewGeneration)(model), [NewGeneration]);
102
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation,
103
+ 'multiple generations of the same event-type id');
104
+ });
105
+
106
+ it('should reject a literal legacy $context. wire expression as unknown with its declaration', () => {
107
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.state).toEventContextProperty('eventType')));
108
+ definition.From[0].Value.Properties.state = '$context.eventType';
109
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
110
+ .with.property('message').that.includes('(.from().setFromContext)')
111
+ .and.includes("expression '$context.eventType'").and.includes('$eventContext(...)');
112
+ });
113
+
114
+ it('should reject a client-emitted derived context function at the phase-one boundary', () => {
115
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from =>
116
+ from.set(model => model.state).toEventContextProperty('occurred.week()')));
117
+ definition.From[0].Value.Properties.state.should.equal('$eventContext(Occurred.Week())');
118
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
119
+ .with.property('message').that.includes('(.from().setFromContext)')
120
+ .and.includes("expression '$eventContext(Occurred.Week())': derived event-context functions require a kernel-backed test");
121
+ });
122
+
123
+ for (const expression of [
124
+ '$eventContext(Occurred.Date)', '$eventContext(EventSourceId.Part)', '$eventContext(CausedBy.Unknown)',
125
+ '$eventContext(CausedBy.OnBehalfOf.UserName)', '$eventContext(Causation.Type)', '$eventContext(Tags.Name)',
126
+ '$eventContext(Occurred.ISOWeek())', '$eventContext(Occurred1)', '$eventContext(_Occurred)',
127
+ '$eventContext(occurred)'
128
+ ]) {
129
+ it(`should reject unsupported event-context expression ${expression}`, () => {
130
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.state).to(event => event.name)));
131
+ definition.From[0].Value.Properties.state = expression;
132
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
133
+ .with.property('message').that.includes(`expression '${expression}'`);
134
+ });
135
+ }
136
+
137
+ it('should reject multiple generations on a declarative projection', () => {
138
+ class NewGeneration { name!: string; }
139
+ field(String)(NewGeneration.prototype, 'name');
140
+ eventType('capability-changed', 2)(NewGeneration);
141
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed).from(NewGeneration), [NewGeneration]);
142
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation,
143
+ 'multiple generations of the same event-type id');
144
+ });
145
+
146
+ for (const format of ['float', 'decimal', 'duration', 'int64']) {
147
+ it(`should reject ${format} arithmetic rather than approximate the kernel`, () => {
148
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
149
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
150
+ schema.properties.total.format = format;
151
+ compiled.readModels[0].Schema = JSON.stringify(schema);
152
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
153
+ .with.property('message').that.includes('From[capability-changed:1].Properties.total');
154
+ });
155
+ }
156
+
157
+ for (const format of ['float', 'decimal', 'duration', 'int64']) {
158
+ it(`should reject model-bound ${format} arithmetic`, () => {
159
+ const { compiled, definition } = compileModelBound(model => addFrom(Changed, 'quantity')(model.prototype, 'total'));
160
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { format?: string } } };
161
+ schema.properties.total.format = format;
162
+ compiled.readModels[0].Schema = JSON.stringify(schema);
163
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
164
+ .with.property('message').that.includes('(@addFrom)').and.includes("expression '$add(quantity)'");
165
+ });
166
+ }
167
+
168
+ it('should reject an inferred AutoMap target with an unsupported format', () => {
169
+ const { compiled, definition } = compileModelBound();
170
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { quantity: { format?: string } } };
171
+ schema.properties.quantity.format = 'float';
172
+ compiled.readModels[0].Schema = JSON.stringify(schema);
173
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
174
+ .with.property('message').that.includes('From[capability-changed:1].AutoMap.quantity');
175
+ });
176
+
177
+ it('should reject declarative inferred AutoMap with its real source declaration', () => {
178
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
179
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { quantity: { format?: string } } };
180
+ schema.properties.quantity.format = 'float';
181
+ compiled.readModels[0].Schema = JSON.stringify(schema);
182
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
183
+ .with.property('message').that.includes('From[capability-changed:1].AutoMap.quantity (.from (AutoMap))');
184
+ });
185
+
186
+ it('should validate AutoMap against every participating event, including the second source', () => {
187
+ class Second { state!: string; }
188
+ field(String)(Second.prototype, 'state');
189
+ eventType('second-source')(Second);
190
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed).from(Second), [Second]);
191
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { state: { format?: string } } };
192
+ schema.properties.state.format = 'float';
193
+ compiled.readModels[0].Schema = JSON.stringify(schema);
194
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
195
+ .with.property('message').that.includes('From[second-source:1].AutoMap.state (.from (AutoMap))');
196
+ });
197
+
198
+ it('should reject ambiguous AutoMap sources that differ only by case', () => {
199
+ class Ambiguous { name!: string; Name!: string; }
200
+ field(String)(Ambiguous.prototype, 'name');
201
+ field(String)(Ambiguous.prototype, 'Name');
202
+ eventType('ambiguous-source')(Ambiguous);
203
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Ambiguous), [Ambiguous]);
204
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
205
+ .with.property('message').that.includes('From[ambiguous-source:1].AutoMap.name (.from (AutoMap))').and.includes('name, Name');
206
+ });
207
+
208
+ it('should reject identifiers with unsupported schemas', () => {
209
+ const { compiled, definition } = compileModelBound();
210
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { id: { type: string } } };
211
+ schema.properties.id.type = 'boolean';
212
+ compiled.readModels[0].Schema = JSON.stringify(schema);
213
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
214
+ .with.property('message').that.includes('ReadModel.Schema.id (@fromEvent)');
215
+ });
216
+
217
+ it('should reject dynamically addressed destinations and unsupported expressions', () => {
218
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.setThisValue().to(event => event.name)));
219
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
220
+ .with.property('message').that.includes('From[capability-changed:1].Properties.$this');
221
+ });
222
+
223
+ it('should reject participating event types missing from discovered registration artifacts', () => {
224
+ class Undiscovered {}
225
+ eventType()(Undiscovered);
226
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Undiscovered));
227
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
228
+ .with.property('message').that.includes('From[').and.includes('(.from)').and.includes('participating event schema is unavailable');
229
+ });
230
+ });
@@ -0,0 +1,73 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { chai, describe, it } from 'vitest';
6
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
7
+ import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
8
+ import { Changed } from './given/Changed.js';
9
+ import { compileDeclarative } from './given/compile.js';
10
+
11
+ chai.should();
12
+
13
+ describe('when validating the numeric capability matrix', () => {
14
+ for (const format of ['int32', 'uint32']) {
15
+ it(`should accept ${format} arithmetic with a matching event operand and literal`, () => {
16
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
17
+ .add(model => model.total).with(event => event.quantity)
18
+ .set(model => model.quantity).toValue(12)));
19
+ const modelSchema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string }; quantity: { type: string; format?: string } } };
20
+ modelSchema.properties.total = { type: 'integer', format };
21
+ modelSchema.properties.quantity = { type: 'integer', format };
22
+ compiled.readModels[0].Schema = JSON.stringify(modelSchema);
23
+ const eventSchema = compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema;
24
+ eventSchema.properties!.quantity = { type: 'integer', format };
25
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
26
+ });
27
+ }
28
+
29
+ it('should accept GUID identifiers and finite double arithmetic', () => {
30
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
31
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { id: { type: string; format?: string }; total: { format?: string } } };
32
+ schema.properties.id.format = 'guid';
33
+ schema.properties.total.format = 'double';
34
+ compiled.readModels[0].Schema = JSON.stringify(schema);
35
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
36
+ });
37
+
38
+ it('should accept numeric identifiers with a declared number schema', () => {
39
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
40
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { id: { type: string } } };
41
+ schema.properties.id.type = 'number';
42
+ compiled.readModels[0].Schema = JSON.stringify(schema);
43
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
44
+ });
45
+
46
+ for (const value of [12.5, 2147483648, 9007199254740992]) {
47
+ it(`should reject an int32 $value literal outside the integer range (${value})`, () => {
48
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.total).toValue(value)));
49
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
50
+ schema.properties.total = { type: 'integer', format: 'int32' };
51
+ compiled.readModels[0].Schema = JSON.stringify(schema);
52
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
53
+ .with.property('message').that.includes('$value numeric literal is outside');
54
+ });
55
+ }
56
+
57
+ it('should reject an overflowing initial integer value', () => {
58
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed)
59
+ .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 2147483648, state: '', labels: [], details: {} })));
60
+ const schema = JSON.parse(compiled.readModels[0].Schema) as { properties: { total: { type: string; format?: string } } };
61
+ schema.properties.total = { type: 'integer', format: 'int32' };
62
+ compiled.readModels[0].Schema = JSON.stringify(schema);
63
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
64
+ .with.property('message').that.includes('InitialModelState.total (.withInitialValues)');
65
+ });
66
+
67
+ it('should reject unsafe int64 operands instead of losing precision', () => {
68
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
69
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.quantity.format = 'int64';
70
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
71
+ .with.property('message').that.includes('arithmetic operand requires a supported numeric event schema');
72
+ });
73
+ });
@@ -0,0 +1,118 @@
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 { ConceptAs, field } from '@cratis/fundamentals';
6
+ import { chai, describe, it } from 'vitest';
7
+ import { setFrom } from '../../../projections/modelBound/setFrom.js';
8
+ import { JsonSchemaGenerator } from '../../../schemas/JsonSchemaGenerator.js';
9
+ import type { JsonSchema } from '../../../schemas/JsonSchema.js';
10
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
11
+ import { UnsupportedProjectionOperation } from '../UnsupportedProjectionOperation.js';
12
+ import { Changed } from './given/Changed.js';
13
+ import { compileDeclarative, compileModelBound } from './given/compile.js';
14
+
15
+ chai.should();
16
+
17
+ class Timestamp extends ConceptAs<Date> { static readonly valueType = Date; }
18
+ class Details { created!: Date; }
19
+ class Dated { created!: Date; timestamp!: Timestamp; details!: Details; }
20
+ field(Date)(Details.prototype, 'created');
21
+ field(Date)(Dated.prototype, 'created');
22
+ field(Timestamp)(Dated.prototype, 'timestamp');
23
+ field(Details)(Dated.prototype, 'details');
24
+
25
+ function setModelSchema(compiled: ReturnType<typeof compileDeclarative>['compiled'], change: (properties: Record<string, JsonSchema>) => void): void {
26
+ const schema = JSON.parse(compiled.readModels[0].Schema) as JsonSchema;
27
+ change(schema.properties!);
28
+ compiled.readModels[0].Schema = JSON.stringify(schema);
29
+ }
30
+
31
+ describe('when validating schema-bound projection operations', () => {
32
+ it('should accept generated date, date concept, and nested date formats for pass-through and initial values', () => {
33
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
34
+ .set(model => model.name).to(event => event.name)
35
+ .set(model => model.state).to(event => event.name)
36
+ .set(model => model.details).to(event => event.details))
37
+ .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 0, state: new Date('2026-01-01') as unknown as string,
38
+ labels: [], details: { created: new Date('2026-01-01') } })));
39
+ const dates = JsonSchemaGenerator.generate(Dated).properties!;
40
+ setModelSchema(compiled, properties => {
41
+ properties.name = dates.created;
42
+ properties.state = dates.timestamp;
43
+ properties.details = dates.details;
44
+ });
45
+ const event = compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema;
46
+ event.properties!.name = dates.created;
47
+ event.properties!.details = dates.details;
48
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
49
+ });
50
+
51
+ it('should accept model-bound date pass-through and date AutoMap', () => {
52
+ const { compiled, definition } = compileModelBound(model => setFrom(Changed, 'name')(model.prototype, 'state'));
53
+ const date = JsonSchemaGenerator.generate(Dated).properties!.created;
54
+ setModelSchema(compiled, properties => { properties.name = date; properties.state = date; });
55
+ compiled.eventSchemas.get(definition)!.get('capability-changed:1:0')!.schema.properties!.name = date;
56
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
57
+ });
58
+
59
+ it('should accept $null for a date destination', () => {
60
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.state).toValue(null)));
61
+ setModelSchema(compiled, properties => { properties.state = { type: 'string', format: 'date-time' }; });
62
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
63
+ });
64
+
65
+ for (const expression of ['$value(2026-01-01T00:00:00.000Z)', '$increment', '$add(quantity)']) {
66
+ it(`should reject ${expression} against a date destination`, () => {
67
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.set(model => model.state).to(event => event.name)));
68
+ setModelSchema(compiled, properties => { properties.state = { type: 'string', format: 'date-time' }; });
69
+ definition.From[0].Value.Properties.state = expression;
70
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
71
+ .with.property('message').that.includes(`expression '${expression}'`);
72
+ });
73
+ }
74
+
75
+ it('should use a string key when neither id nor Id exists', () => {
76
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
77
+ setModelSchema(compiled, properties => { delete properties.id; });
78
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
79
+ });
80
+
81
+ it('should use Id when id is absent, but prefer id when both exist', () => {
82
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
83
+ setModelSchema(compiled, properties => { properties.Id = { type: 'string' }; delete properties.id; });
84
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
85
+ setModelSchema(compiled, properties => { properties.id = { type: 'boolean' }; });
86
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation, 'ReadModel.Schema.id');
87
+ });
88
+
89
+ it('should not treat ID as the kernel identifier', () => {
90
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
91
+ setModelSchema(compiled, properties => { delete properties.id; properties.ID = { type: 'boolean' }; });
92
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
93
+ });
94
+
95
+ it('should skip AutoMap validation for arithmetic-only events', () => {
96
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from.add(model => model.total).with(event => event.quantity)));
97
+ setModelSchema(compiled, properties => { properties.name.format = 'float'; });
98
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
99
+ });
100
+
101
+ for (const [expression, destination, reason] of [
102
+ ['$value(maybe)', 'state', '$value boolean literal'],
103
+ ['$value(not-a-guid)', 'state', '$value GUID literal'],
104
+ ['$value(text)', 'details', '$value object/array literals'],
105
+ ['$value(text)', 'labels', '$value object/array literals']
106
+ ]) {
107
+ it(`should reject invalid ${expression} for ${destination} (${reason})`, () => {
108
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed));
109
+ setModelSchema(compiled, properties => {
110
+ if (reason.includes('boolean')) properties.state = { type: 'boolean' };
111
+ if (reason.includes('GUID')) properties.state = { type: 'string', format: 'guid' };
112
+ });
113
+ definition.From[0].Value.Properties[destination] = expression;
114
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.throw(UnsupportedProjectionOperation)
115
+ .with.property('message').that.includes(reason).and.includes(`expression '${expression}'`);
116
+ });
117
+ }
118
+ });
@@ -0,0 +1,103 @@
1
+ // Copyright (c) Cratis. All rights reserved.
2
+ // Licensed under the MIT license. See LICENSE file in the project root for full license information.
3
+
4
+ import 'reflect-metadata';
5
+ import { chai, describe, it } from 'vitest';
6
+ import { addFrom } from '../../../projections/modelBound/addFrom.js';
7
+ import { clearWith } from '../../../projections/modelBound/clearWith.js';
8
+ import { count } from '../../../projections/modelBound/count.js';
9
+ import { decrement } from '../../../projections/modelBound/decrement.js';
10
+ import { increment } from '../../../projections/modelBound/increment.js';
11
+ import { noAutoMap } from '../../../projections/modelBound/noAutoMap.js';
12
+ import { removedWith } from '../../../projections/modelBound/removedWith.js';
13
+ import { setFrom } from '../../../projections/modelBound/setFrom.js';
14
+ import { setFromContext } from '../../../projections/modelBound/setFromContext.js';
15
+ import { setValue } from '../../../projections/modelBound/setValue.js';
16
+ import { subtractFrom } from '../../../projections/modelBound/subtractFrom.js';
17
+ import { ProjectionCapabilities } from '../ProjectionCapabilities.js';
18
+ import { Changed } from './given/Changed.js';
19
+ import { Removed } from './given/Removed.js';
20
+ import { compileDeclarative, compileModelBound } from './given/compile.js';
21
+
22
+ chai.should();
23
+
24
+ describe('when validating the phase-one projection subset', () => {
25
+ const modelBound = [
26
+ { name: 'root From and schema-inferred AutoMap', configure: () => {} },
27
+ { name: 'setFrom and event source identity', configure: (model: Function) => setFrom(Changed, 'name')(model.prototype, 'name') },
28
+ { name: 'setFromContext maps eventType without needing it in the event schema', configure: (model: Function) => setFromContext(Changed, 'eventType')(model.prototype, 'state') },
29
+ { name: 'setFromContext maps the event source identity', configure: (model: Function) => setFromContext(Changed, 'eventSourceId')(model.prototype, 'state') },
30
+ { name: 'setFromContext maps a nested identity member', configure: (model: Function) => setFromContext(Changed, 'causedBy.subject')(model.prototype, 'state') },
31
+ { name: 'constant text', configure: (model: Function) => setValue(Changed, 'ready')(model.prototype, 'state') },
32
+ { name: 'scalar clearing', configure: (model: Function) => clearWith(Removed)(model.prototype, 'state') },
33
+ { name: 'addition', configure: (model: Function) => addFrom(Changed, 'quantity')(model.prototype, 'total') },
34
+ { name: 'subtraction', configure: (model: Function) => subtractFrom(Changed, 'quantity')(model.prototype, 'total') },
35
+ { name: 'count', configure: (model: Function) => count(Changed)(model.prototype, 'total') },
36
+ { name: 'increment', configure: (model: Function) => increment(Changed)(model.prototype, 'total') },
37
+ { name: 'decrement', configure: (model: Function) => decrement(Changed)(model.prototype, 'total') },
38
+ { name: 'root removal', configure: (model: Function) => removedWith(Removed)(model) },
39
+ { name: 'AutoMap exclusions', configure: (model: Function) => noAutoMap(model.prototype, 'quantity') }
40
+ ];
41
+ for (const testCase of modelBound) {
42
+ it(`should accept model-bound ${testCase.name} before any events are seeded`, () => {
43
+ const { compiled, definition } = compileModelBound(testCase.configure);
44
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
45
+ });
46
+ }
47
+
48
+ it('should accept declarative multiple root sources, explicit mappings, constants, arithmetic, and removal', () => {
49
+ const { compiled, definition } = compileDeclarative(builder => builder
50
+ .from(Changed, from => from.set(model => model.name).to(event => event.name)
51
+ .set(model => model.id).toEventSourceId()
52
+ .set(model => model.state).toValue('ready')
53
+ .add(model => model.total).with(event => event.quantity)
54
+ .subtract(model => model.total).with(event => event.quantity)
55
+ .increment(model => model.total).decrement(model => model.total).count(model => model.total))
56
+ .from(Removed).removedWith(Removed).withInitialValues(() => ({ id: '', name: '', quantity: 0, total: 0, state: 'new', labels: [], details: {} })));
57
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
58
+ });
59
+
60
+ for (const [property, expression] of [
61
+ ['sequenceNumber', '$eventContext(SequenceNumber)'],
62
+ ['eventSourceId', '$eventContext(EventSourceId)'],
63
+ ['eventStore', '$eventContext(EventStore)'],
64
+ ['namespace', '$eventContext(Namespace)'],
65
+ ['eventSourceType', '$eventContext(EventSourceType)'],
66
+ ['eventStreamType', '$eventContext(EventStreamType)'],
67
+ ['eventStreamId', '$eventContext(EventStreamId)'],
68
+ ['subject', '$eventContext(Subject)'],
69
+ ['hash', '$eventContext(Hash)'],
70
+ ['causedBy', '$eventContext(CausedBy)'],
71
+ ['observationState', '$eventContext(ObservationState)'],
72
+ ['eventType', '$eventContext(EventType)'],
73
+ ['occurred', '$eventContext(Occurred)'],
74
+ ['correlationId', '$eventContext(CorrelationId)'],
75
+ ['causation', '$eventContext(Causation)'],
76
+ ['tags', '$eventContext(Tags)'],
77
+ ['causedBy.subject', '$eventContext(CausedBy.Subject)'],
78
+ ['causedBy.name', '$eventContext(CausedBy.Name)'],
79
+ ['causedBy.userName', '$eventContext(CausedBy.UserName)'],
80
+ ['causedBy.onBehalfOf', '$eventContext(CausedBy.OnBehalfOf)']
81
+ ]) {
82
+ it(`should accept the client's emitted context expression ${expression}`, () => {
83
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from =>
84
+ from.set(model => model.state).toEventContextProperty(property)));
85
+ definition.From[0].Value.Properties.state.should.equal(expression);
86
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
87
+ });
88
+ }
89
+
90
+ it('should accept declarative null clearing and an explicit null initial accumulator', () => {
91
+ const { compiled, definition } = compileDeclarative(builder => builder
92
+ .from(Changed, from => from.set(model => model.state).toValue(null).count(model => model.total))
93
+ .withInitialValues(() => ({ id: '', name: '', quantity: 0, total: null as unknown as number, state: '', labels: [], details: {} })));
94
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
95
+ });
96
+
97
+ it('should accept an ordinary object or array value without mistaking it for a child projection', () => {
98
+ const { compiled, definition } = compileDeclarative(builder => builder.from(Changed, from => from
99
+ .set(model => model.labels).to(event => event.labels)
100
+ .set(model => model.details).to(event => event.details)));
101
+ (() => ProjectionCapabilities.validate(compiled, definition)).should.not.throw();
102
+ });
103
+ });