@cratis/chronicle 6.41.1 → 6.42.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/events/tagDecorator.d.ts +2 -2
- package/dist/events/tagDecorator.js +2 -2
- package/dist/projections/ProjectionDefinitionCompiler.d.ts.map +1 -1
- package/dist/projections/ProjectionDefinitionCompiler.js +3 -1
- package/dist/projections/ProjectionDefinitionCompiler.js.map +1 -1
- package/dist/projections/captureProjectionProvenance.d.ts.map +1 -1
- package/dist/projections/captureProjectionProvenance.js +2 -0
- package/dist/projections/captureProjectionProvenance.js.map +1 -1
- package/dist/projections/declarative/FromAllBuilder.d.ts +18 -0
- package/dist/projections/declarative/FromAllBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/FromAllBuilder.js +36 -0
- package/dist/projections/declarative/FromAllBuilder.js.map +1 -0
- package/dist/projections/declarative/IFromAllBuilder.d.ts +33 -0
- package/dist/projections/declarative/IFromAllBuilder.d.ts.map +1 -0
- package/dist/projections/declarative/IFromAllBuilder.js +4 -0
- package/dist/projections/declarative/IFromAllBuilder.js.map +1 -0
- package/dist/projections/declarative/IProjectionBuilderFor.d.ts +8 -0
- package/dist/projections/declarative/IProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts +4 -0
- package/dist/projections/declarative/ProjectionBuilderFor.d.ts.map +1 -1
- package/dist/projections/declarative/ProjectionBuilderFor.js +15 -0
- package/dist/projections/declarative/ProjectionBuilderFor.js.map +1 -1
- package/dist/projections/declarative/index.d.ts +2 -0
- package/dist/projections/declarative/index.d.ts.map +1 -1
- package/dist/projections/declarative/index.js +1 -0
- package/dist/projections/declarative/index.js.map +1 -1
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts +6 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.d.ts.map +1 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js +12 -0
- package/dist/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.js.map +1 -0
- package/dist/projections/modelBound/fromAll.d.ts +2 -1
- package/dist/projections/modelBound/fromAll.d.ts.map +1 -1
- package/dist/projections/modelBound/fromAll.js +2 -1
- package/dist/projections/modelBound/fromAll.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/events/tagDecorator.ts +2 -2
- package/package.json +2 -2
- package/projections/ProjectionDefinitionCompiler.ts +3 -1
- package/projections/captureProjectionProvenance.ts +1 -0
- package/projections/declarative/FromAllBuilder.ts +49 -0
- package/projections/declarative/IFromAllBuilder.ts +39 -0
- package/projections/declarative/IProjectionBuilderFor.ts +9 -0
- package/projections/declarative/ProjectionBuilderFor.ts +17 -0
- package/projections/declarative/for_ProjectionBuilderFor/when_subscribing_to_all_events.spec.ts +111 -0
- package/projections/declarative/index.ts +2 -0
- package/projections/for_ProjectionDefinitionCompiler/given/compile_model_bound.ts +15 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.spec.ts +90 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_from_all.standard.spec.ts +69 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_legacy_decorators.legacy.spec.ts +87 -0
- package/projections/for_ProjectionDefinitionCompiler/when_compiling_tags/with_standard_decorators.standard.spec.ts +77 -0
- package/projections/modelBound/fromAll.ts +2 -1
- package/reactors/for_Reactors/against_a_kernel/when_replaying_once_only_reactors.integration.spec.ts +22 -3
- package/testing/projections/ProjectionReadModelProcessor.fixtures.spec.ts +5 -0
- package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts +2 -0
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
6
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
7
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
8
|
+
import { getTagsFor, tag, tags } from '../../../events/tagDecorator.js';
|
|
9
|
+
import { ReadModelScenario } from '../../../testing/ReadModelScenario.js';
|
|
10
|
+
import type { IProjectionBuilderFor } from '../../declarative/IProjectionBuilderFor.js';
|
|
11
|
+
import { projection } from '../../declarative/projection.js';
|
|
12
|
+
import { fromEvent } from '../../modelBound/fromEvent.js';
|
|
13
|
+
import { ProjectionDefinitionCompiler } from '../../ProjectionDefinitionCompiler.js';
|
|
14
|
+
import { Projections } from '../../Projections.js';
|
|
15
|
+
|
|
16
|
+
chai.should();
|
|
17
|
+
|
|
18
|
+
@eventType('legacy-projection-tags-renamed')
|
|
19
|
+
class Renamed {
|
|
20
|
+
name = 'updated';
|
|
21
|
+
}
|
|
22
|
+
function createArtifacts(modelBound: boolean, values: string[] = []) {
|
|
23
|
+
@tag(...(modelBound ? values : ['read-model-only']))
|
|
24
|
+
class Model {
|
|
25
|
+
static readonly readModelId = 'legacy-projection-tags-model';
|
|
26
|
+
id = '';
|
|
27
|
+
name = '';
|
|
28
|
+
}
|
|
29
|
+
@projection('legacy-projection-tags-projection', Model)
|
|
30
|
+
@tag(...(modelBound ? [] : values))
|
|
31
|
+
class ModelProjection {
|
|
32
|
+
define(builder: IProjectionBuilderFor<Model>): void {
|
|
33
|
+
builder.from(Renamed);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (modelBound) fromEvent(Renamed)(Model);
|
|
37
|
+
const observer = modelBound ? Model : ModelProjection;
|
|
38
|
+
const artifacts: IClientArtifactsProvider = {
|
|
39
|
+
eventTypes: [Renamed], readModels: [Model], projections: modelBound ? [] : [ModelProjection],
|
|
40
|
+
reducers: [], reactors: [], globalForHandlers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
41
|
+
};
|
|
42
|
+
const compile = () => new ProjectionDefinitionCompiler(artifacts, 'test-sink')
|
|
43
|
+
.compile(artifacts.projections, modelBound ? [Model] : []).definitions[0];
|
|
44
|
+
return { artifacts, observer, Model, compile };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const modelBound of [false, true]) {
|
|
48
|
+
describe(`when compiling tags on a legacy ${modelBound ? 'model-bound read model' : 'declarative projection class'}`, () => {
|
|
49
|
+
it('should send accumulated distinct observer labels to the kernel in stable order', async () => {
|
|
50
|
+
const { artifacts, observer } = createArtifacts(modelBound, ['reporting', 'analytics']);
|
|
51
|
+
tags('reporting', 'audit')(observer);
|
|
52
|
+
tag('analytics')(observer);
|
|
53
|
+
const register = vi.fn().mockResolvedValue({});
|
|
54
|
+
const connection = { projections: { register }, readModels: { registerMany: vi.fn().mockResolvedValue({}) } } as unknown as ChronicleConnection;
|
|
55
|
+
await new Projections('store', 'namespace', connection, artifacts, 'test-sink').register();
|
|
56
|
+
const request = register.mock.calls[0][0] as { Projections: { Tags: string[] }[] };
|
|
57
|
+
request.Projections[0].Tags.should.deep.equal(['analytics', 'audit', 'reporting']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should keep an untagged observer empty without borrowing declarative read-model labels', () => {
|
|
61
|
+
createArtifacts(modelBound).compile().Tags.should.deep.equal([]);
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('should hash the same tag set identically regardless of declaration order and duplicates', () => {
|
|
65
|
+
const first = createArtifacts(modelBound, ['reporting', 'analytics']);
|
|
66
|
+
tags('reporting')(first.observer);
|
|
67
|
+
const second = createArtifacts(modelBound, ['analytics', 'reporting', 'analytics']);
|
|
68
|
+
first.compile().LastUpdated!.Value.should.equal(second.compile().LastUpdated!.Value);
|
|
69
|
+
first.compile().LastUpdated!.Value.should.equal(first.compile().LastUpdated!.Value);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should include tag changes in the definition hash without reordering metadata', () => {
|
|
73
|
+
const { compile, observer } = createArtifacts(modelBound, ['reporting', 'analytics']);
|
|
74
|
+
const previous = compile().LastUpdated!.Value;
|
|
75
|
+
getTagsFor(observer).map(value => value.value).should.deep.equal(['reporting', 'analytics']);
|
|
76
|
+
tag('audit')(observer);
|
|
77
|
+
compile().LastUpdated!.Value.should.not.equal(previous);
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
it('should not filter untagged events in the in-process read-model scenario', async () => {
|
|
81
|
+
const { Model, artifacts } = createArtifacts(modelBound, ['analytics']);
|
|
82
|
+
const scenario = new ReadModelScenario(Model, artifacts);
|
|
83
|
+
scenario.given.forEventSource('source').events(new Renamed());
|
|
84
|
+
(await scenario.instance)!.name.should.equal('updated');
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
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, vi } from 'vitest';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
7
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
8
|
+
import { eventType } from '../../../events/eventTypeDecorator.js';
|
|
9
|
+
import { getTagsFor, tag, tags } from '../../../events/tagDecorator.js';
|
|
10
|
+
import { ReadModelScenario } from '../../../testing/ReadModelScenario.js';
|
|
11
|
+
import type { IProjectionBuilderFor } from '../../declarative/IProjectionBuilderFor.js';
|
|
12
|
+
import { projection } from '../../declarative/projection.js';
|
|
13
|
+
import { fromEvent } from '../../modelBound/fromEvent.js';
|
|
14
|
+
import { ProjectionDefinitionCompiler } from '../../ProjectionDefinitionCompiler.js';
|
|
15
|
+
import { Projections } from '../../Projections.js';
|
|
16
|
+
|
|
17
|
+
chai.should();
|
|
18
|
+
|
|
19
|
+
@eventType('standard-projection-tags-renamed')
|
|
20
|
+
class Renamed {
|
|
21
|
+
@field(String) name = 'updated';
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
@tag('read-model-only')
|
|
25
|
+
class DeclarativeModel {
|
|
26
|
+
@field(String) id = '';
|
|
27
|
+
@field(String) name = '';
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
@projection('standard-projection-tags-projection', DeclarativeModel)
|
|
31
|
+
@tag('analytics', 'reporting')
|
|
32
|
+
@tags('reporting', 'audit')
|
|
33
|
+
class ModelProjection {
|
|
34
|
+
define(builder: IProjectionBuilderFor<DeclarativeModel>): void {
|
|
35
|
+
builder.from(Renamed);
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
@fromEvent(Renamed)
|
|
40
|
+
@tags('analytics', 'reporting')
|
|
41
|
+
@tag('reporting', 'audit')
|
|
42
|
+
class ModelBound {
|
|
43
|
+
@field(String) id = '';
|
|
44
|
+
@field(String) name = '';
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const modelBound of [false, true]) {
|
|
48
|
+
describe(`when compiling tags on a standard ${modelBound ? 'model-bound read model' : 'declarative projection class'}`, () => {
|
|
49
|
+
const model = modelBound ? ModelBound : DeclarativeModel;
|
|
50
|
+
const observer = modelBound ? ModelBound : ModelProjection;
|
|
51
|
+
const artifacts: IClientArtifactsProvider = {
|
|
52
|
+
eventTypes: [Renamed], readModels: [model], projections: modelBound ? [] : [ModelProjection],
|
|
53
|
+
reactors: [], reducers: [], globalForHandlers: [], seeders: [], constraints: [], webhooks: [], eventTypeMigrations: []
|
|
54
|
+
};
|
|
55
|
+
const compile = () => new ProjectionDefinitionCompiler(artifacts, 'test-sink')
|
|
56
|
+
.compile(artifacts.projections, modelBound ? [ModelBound] : []).definitions[0];
|
|
57
|
+
|
|
58
|
+
it('should register sorted distinct tags using both class decorator spellings', async () => {
|
|
59
|
+
const register = vi.fn().mockResolvedValue({});
|
|
60
|
+
const connection = { projections: { register }, readModels: { registerMany: vi.fn().mockResolvedValue({}) } } as unknown as ChronicleConnection;
|
|
61
|
+
await new Projections('store', 'namespace', connection, artifacts, 'test-sink').register();
|
|
62
|
+
const request = register.mock.calls[0][0] as { Projections: { Tags: string[] }[] };
|
|
63
|
+
request.Projections[0].Tags.should.deep.equal(['analytics', 'audit', 'reporting']);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should retain a deterministic hash and leave decorator metadata untouched', () => {
|
|
67
|
+
compile().LastUpdated!.Value.should.equal(compile().LastUpdated!.Value);
|
|
68
|
+
getTagsFor(observer).map(value => value.value).should.deep.equal(['reporting', 'audit', 'analytics']);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should still evaluate untagged events through the shared compiler', async () => {
|
|
72
|
+
const scenario = new ReadModelScenario(model, artifacts);
|
|
73
|
+
scenario.given.forEventSource('source').events(new Renamed());
|
|
74
|
+
(await scenario.instance)!.name.should.equal('updated');
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -20,7 +20,8 @@ const METADATA_KEY = 'chronicle:projection:fromAll';
|
|
|
20
20
|
* Property decorator that sets the decorated read model property from a property present on every
|
|
21
21
|
* event type the projection is built from - the convention-based counterpart to declaring the same
|
|
22
22
|
* mapping on every individual `fromEvent`. Equivalent to {@link fromEvery} for model-bound
|
|
23
|
-
* projections
|
|
23
|
+
* projections.
|
|
24
|
+
* @deprecated In the next major, @fromAll subscribes to every event type like fluent fromAll() and .NET [FromAll]; use @fromEvery to keep the current behavior.
|
|
24
25
|
* @param property - Optional event property name. If not specified, uses the model property name.
|
|
25
26
|
* @param contextProperty - Optional event context property name.
|
|
26
27
|
* @returns A property decorator.
|
package/reactors/for_Reactors/against_a_kernel/when_replaying_once_only_reactors.integration.spec.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { randomUUID } from 'node:crypto';
|
|
5
5
|
import { afterAll, beforeAll, chai, describe, it } from 'vitest';
|
|
6
|
+
import { ObserverRunningState } from '@cratis/chronicle.contracts';
|
|
6
7
|
import { field } from '@cratis/fundamentals';
|
|
7
8
|
import { ChronicleClient } from '../../../ChronicleClient.js';
|
|
8
9
|
import { ChronicleOptions } from '../../../ChronicleOptions.js';
|
|
@@ -52,9 +53,9 @@ const artifacts: IClientArtifactsProvider = {
|
|
|
52
53
|
globalForHandlers: []
|
|
53
54
|
};
|
|
54
55
|
|
|
55
|
-
async function eventually(accept: () => boolean
|
|
56
|
+
async function eventually(accept: () => boolean | Promise<boolean>, timeoutMs = 30_000): Promise<void> {
|
|
56
57
|
const deadline = Date.now() + timeoutMs;
|
|
57
|
-
while (!accept()) {
|
|
58
|
+
while (!await accept()) {
|
|
58
59
|
if (Date.now() > deadline) throw new Error('Timed out waiting for the kernel');
|
|
59
60
|
await new Promise(resolve => setTimeout(resolve, 250));
|
|
60
61
|
}
|
|
@@ -77,6 +78,18 @@ describe.skipIf(!connectionString && !process.env.CI)('when replaying reactors a
|
|
|
77
78
|
EventSequenceId: 'event-log'
|
|
78
79
|
});
|
|
79
80
|
|
|
81
|
+
const waitForActiveObservers = (lastHandledSequenceNumber?: bigint) => eventually(async () => {
|
|
82
|
+
const observers = await Promise.all(['OnceOnlyReactor', 'ReplayableReactor'].map(observerId =>
|
|
83
|
+
connection.observers.getObserverInformation({
|
|
84
|
+
EventStore: storeName,
|
|
85
|
+
Namespace: 'Default',
|
|
86
|
+
ObserverId: observerId,
|
|
87
|
+
EventSequenceId: 'event-log'
|
|
88
|
+
})));
|
|
89
|
+
return observers.every(observer => observer.IsSubscribed && observer.RunningState === ObserverRunningState.Active &&
|
|
90
|
+
(lastHandledSequenceNumber === undefined || observer.LastHandledEventSequenceNumber === lastHandledSequenceNumber));
|
|
91
|
+
}, 15_000);
|
|
92
|
+
|
|
80
93
|
beforeAll(async () => {
|
|
81
94
|
client = new ChronicleClient(ChronicleOptions.fromConnectionString(connectionString!, {
|
|
82
95
|
discoveryPatterns: [],
|
|
@@ -85,8 +98,14 @@ describe.skipIf(!connectionString && !process.env.CI)('when replaying reactors a
|
|
|
85
98
|
store = await client.getEventStore(storeName);
|
|
86
99
|
connection = (store as unknown as { _connection: ChronicleConnection })._connection;
|
|
87
100
|
|
|
88
|
-
|
|
101
|
+
// Registration starts observation in the background. Append only after both subscriptions
|
|
102
|
+
// are active, so initial catch-up cannot race the explicit replay's state transition.
|
|
103
|
+
await waitForActiveObservers();
|
|
104
|
+
const appended = await store.eventLog.append(randomUUID(), Object.assign(new ThingHappened(), { name: 'first' }));
|
|
105
|
+
appended.isSuccess.should.be.true;
|
|
89
106
|
await eventually(() => onceOnlyInvocations.length === 1 && replayableInvocations.length === 1);
|
|
107
|
+
// The handlers update their sinks before the kernel acknowledges delivery and records progress.
|
|
108
|
+
await waitForActiveObservers(appended.sequenceNumber.value);
|
|
90
109
|
|
|
91
110
|
onceOnlyJobId = (await replay('OnceOnlyReactor')).JobId;
|
|
92
111
|
replayableJobId = (await replay('ReplayableReactor')).JobId;
|
|
@@ -38,6 +38,11 @@ const allFixtures = readdirSync(directory).filter(name => name.endsWith('.json')
|
|
|
38
38
|
.map(name => ({ name, fixture: JSON.parse(readFileSync(new URL(name, directory), 'utf8')) as Fixture }));
|
|
39
39
|
const fixtures = allFixtures.filter(({ fixture }) => fixture.kind === 'kernelSemantics');
|
|
40
40
|
const guardErrors: Record<string, { type: string; message: string }> = {
|
|
41
|
+
'from-all-only.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
42
|
+
'from-all-mixed.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
43
|
+
'from-all-exclude-children.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
44
|
+
'from-all-empty.json': { type: 'UnsupportedProjectionOperation', message: 'subscribe-to-all projections require a kernel-backed test' },
|
|
45
|
+
'from-every-restricted.json': { type: 'UnsupportedProjectionOperation', message: 'fromEvery/all subscriptions require a kernel-backed test' },
|
|
41
46
|
'joins-custom-key.json': { type: 'UnsupportedProjectionOperation', message: 'join keys other than $eventSourceId' },
|
|
42
47
|
'joins-initial-state.json': { type: 'UnsupportedProjectionOperation', message: 'joins with initial model state' },
|
|
43
48
|
'arithmetic-string-operand.json': { type: 'UnsupportedProjectionOperation', message: 'arithmetic operands must have number/double' },
|
package/testing/projections/for_ProjectionCapabilities/when_rejecting_unsupported_operations.spec.ts
CHANGED
|
@@ -79,6 +79,8 @@ describe('when rejecting unsupported operations before any event is seeded', ()
|
|
|
79
79
|
{ name: 'removedWithJoin', define: builder => { builder.from(Changed).removedWithJoin(Removed); }, path: 'RemovedWithJoin[capability-removed:1] (.removedWithJoin)' },
|
|
80
80
|
{ name: 'removal key', define: builder => { builder.from(Removed).removedWith(Changed, removal => removal.usingKey(event => event.name)); }, path: 'RemovedWith[capability-changed:1].Key (.removedWith)' },
|
|
81
81
|
{ name: 'fromEvery', define: builder => { builder.from(Changed).fromEvery(all => all.set(model => model.name).toEventSourceId()); }, path: 'All (.fromEvery)' },
|
|
82
|
+
{ name: 'fromAll', define: builder => { builder.fromAll(all => all.set(model => model.name).toEventSourceId()); }, path: 'SubscribesToAllEvents (.fromAll)' },
|
|
83
|
+
{ name: 'empty fromAll', define: builder => { builder.fromAll(() => {}); }, path: 'SubscribesToAllEvents (.fromAll)' },
|
|
82
84
|
{ name: 'empty fromEvery', define: builder => { builder.from(Changed).fromEvery(all => all.excludeChildProjections()); }, path: 'All (.fromEvery)' },
|
|
83
85
|
{ name: 'passive', define: builder => { builder.from(Changed).passive(); }, path: 'IsActive (.passive)' },
|
|
84
86
|
{ name: 'event sequence', define: builder => { builder.from(Changed).fromEventSequence('custom'); }, path: 'EventSequenceId (.fromEventSequence)' }
|