@cratis/chronicle 6.7.1 → 6.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/EventStore.ts +10 -4
- package/README.md +14 -5
- package/dist/EventStore.d.ts +1 -0
- package/dist/EventStore.d.ts.map +1 -1
- package/dist/EventStore.js +7 -4
- package/dist/EventStore.js.map +1 -1
- package/dist/eventSequences/EventLog.d.ts +2 -1
- package/dist/eventSequences/EventLog.d.ts.map +1 -1
- package/dist/eventSequences/EventLog.js +2 -2
- package/dist/eventSequences/EventLog.js.map +1 -1
- package/dist/eventSequences/EventSequence.d.ts +3 -1
- package/dist/eventSequences/EventSequence.d.ts.map +1 -1
- package/dist/eventSequences/EventSequence.js +11 -6
- package/dist/eventSequences/EventSequence.js.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.d.ts +2 -0
- package/dist/events/constraints/ConstraintBuilder.d.ts.map +1 -1
- package/dist/events/constraints/ConstraintBuilder.js.map +1 -1
- package/dist/events/constraints/Constraints.d.ts +8 -3
- package/dist/events/constraints/Constraints.d.ts.map +1 -1
- package/dist/events/constraints/Constraints.js +147 -6
- package/dist/events/constraints/Constraints.js.map +1 -1
- package/dist/events/constraints/index.d.ts +2 -0
- package/dist/events/constraints/index.d.ts.map +1 -1
- package/dist/events/constraints/index.js +2 -0
- package/dist/events/constraints/index.js.map +1 -1
- package/dist/events/constraints/removeConstraint.d.ts +11 -0
- package/dist/events/constraints/removeConstraint.d.ts.map +1 -0
- package/dist/events/constraints/removeConstraint.js +24 -0
- package/dist/events/constraints/removeConstraint.js.map +1 -0
- package/dist/events/constraints/unique.d.ts +21 -0
- package/dist/events/constraints/unique.d.ts.map +1 -0
- package/dist/events/constraints/unique.js +37 -0
- package/dist/events/constraints/unique.js.map +1 -0
- package/dist/reactors/Reactors.d.ts.map +1 -1
- package/dist/reactors/Reactors.js +64 -13
- package/dist/reactors/Reactors.js.map +1 -1
- package/dist/reactors/index.d.ts +2 -0
- package/dist/reactors/index.d.ts.map +1 -1
- package/dist/reactors/index.js +2 -0
- package/dist/reactors/index.js.map +1 -1
- package/dist/reactors/onceOnly.d.ts +11 -0
- package/dist/reactors/onceOnly.d.ts.map +1 -0
- package/dist/reactors/onceOnly.js +34 -0
- package/dist/reactors/onceOnly.js.map +1 -0
- package/dist/reactors/replay.d.ts +14 -0
- package/dist/reactors/replay.d.ts.map +1 -0
- package/dist/reactors/replay.js +33 -0
- package/dist/reactors/replay.js.map +1 -0
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/eventSequences/EventLog.ts +4 -2
- package/eventSequences/EventSequence.ts +10 -6
- package/events/classDecorators.standard.spec.ts +45 -1
- package/events/constraints/ConstraintBuilder.ts +2 -0
- package/events/constraints/Constraints.ts +140 -7
- package/events/constraints/for_Constraints/when_combining_decorators_with_fluent_constraints.spec.ts +142 -0
- package/events/constraints/for_Constraints/when_inheriting_removal_decorators.legacy.spec.ts +36 -0
- package/events/constraints/for_Constraints/when_registering_decorated_constraints.spec.ts +63 -0
- package/events/constraints/for_Constraints/when_registering_decorated_event_types.spec.ts +90 -0
- package/events/constraints/for_Constraints/when_rejecting_incompatible_unique_merges.spec.ts +108 -0
- package/events/constraints/for_Constraints/when_resolving_violation_messages.spec.ts +92 -0
- package/events/constraints/index.ts +2 -0
- package/events/constraints/removeConstraint.ts +28 -0
- package/events/constraints/unique.ts +45 -0
- package/package.json +2 -2
- package/reactors/Reactors.replay.legacy.spec.ts +241 -0
- package/reactors/Reactors.ts +65 -14
- package/reactors/decorators.standard.spec.ts +39 -0
- package/reactors/index.ts +2 -0
- package/reactors/onceOnly.ts +34 -0
- package/reactors/replay.ts +36 -0
package/events/constraints/for_Constraints/when_combining_decorators_with_fluent_constraints.spec.ts
ADDED
|
@@ -0,0 +1,142 @@
|
|
|
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 '../../eventTypeDecorator.js';
|
|
8
|
+
import { constraint } from '../constraint.js';
|
|
9
|
+
import { Constraints } from '../Constraints.js';
|
|
10
|
+
import type { IConstraint } from '../IConstraint.js';
|
|
11
|
+
import type { IConstraintBuilder } from '../IConstraintBuilder.js';
|
|
12
|
+
import { unique } from '../unique.js';
|
|
13
|
+
import { removeConstraint } from '../removeConstraint.js';
|
|
14
|
+
|
|
15
|
+
const should = chai.should();
|
|
16
|
+
|
|
17
|
+
class Once {}
|
|
18
|
+
class Again {}
|
|
19
|
+
class Released {}
|
|
20
|
+
class ValueClaimed { address = ''; second = ''; }
|
|
21
|
+
for (const [type, id] of [[Once, 'once'], [Again, 'again'], [Released, 'released'], [ValueClaimed, 'value-claimed']] as const) eventType(id)(type);
|
|
22
|
+
unique('OneRegistration')(Once);
|
|
23
|
+
unique('OneRegistration')(Again);
|
|
24
|
+
unique('Shared')(ValueClaimed.prototype, 'address');
|
|
25
|
+
removeConstraint('OneRegistration')(Released);
|
|
26
|
+
|
|
27
|
+
class FluentOnce implements IConstraint {
|
|
28
|
+
define(builder: IConstraintBuilder): void {
|
|
29
|
+
builder.uniqueFor(Once, 'Registered {email}', 'OneRegistration');
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
constraint('FluentOnce')(FluentOnce);
|
|
33
|
+
|
|
34
|
+
class FluentWithoutMessage implements IConstraint {
|
|
35
|
+
define(builder: IConstraintBuilder): void {
|
|
36
|
+
builder.uniqueFor(Once, undefined, 'OneRegistration');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
constraint('FluentWithoutMessage')(FluentWithoutMessage);
|
|
40
|
+
|
|
41
|
+
class FluentValue implements IConstraint {
|
|
42
|
+
define(builder: IConstraintBuilder): void {
|
|
43
|
+
builder.unique(uniqueBuilder => uniqueBuilder.on(ValueClaimed, event => event.address));
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
constraint('Shared')(FluentValue);
|
|
47
|
+
|
|
48
|
+
async function discover(types: Function[], constraintTypes: (new () => IConstraint)[]): Promise<{ definitions: { Name: string; Definition: { Value0?: { EventDefinitions: { Properties: string[] }[] }; Value1?: { EventTypeIds: string[] } }; RemovedWith: string[] }[]; constraints: Constraints }> {
|
|
49
|
+
const register = vi.fn().mockResolvedValue({});
|
|
50
|
+
const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
|
|
51
|
+
const constraints = new Constraints('store', { constraints: { register } } as unknown as ChronicleConnection, artifacts);
|
|
52
|
+
await constraints.register();
|
|
53
|
+
return { definitions: register.mock.calls[0][0].Constraints, constraints };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
describe('when combining fluent and decorated unique event type constraints', () => {
|
|
57
|
+
it('should register one definition per wire name and merge event types and removals', async () => {
|
|
58
|
+
const { definitions, constraints } = await discover([Once, Again, Released], [FluentOnce]);
|
|
59
|
+
definitions.length.should.equal(1);
|
|
60
|
+
definitions[0].Name.should.equal('OneRegistration');
|
|
61
|
+
definitions[0].Definition.Value1!.EventTypeIds.should.deep.equal(['once', 'again']);
|
|
62
|
+
definitions[0].RemovedWith.should.deep.equal(['released']);
|
|
63
|
+
constraints.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: { email: 'a$b' } }).message.should.equal('Registered a$b');
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe('when merging fluent unique event types with the same name', () => {
|
|
68
|
+
it('should use the first supplied message', async () => {
|
|
69
|
+
class Additional {}
|
|
70
|
+
eventType('additional')(Additional);
|
|
71
|
+
class FluentWithMessage implements IConstraint {
|
|
72
|
+
define(builder: IConstraintBuilder): void {
|
|
73
|
+
builder.uniqueFor(Additional, 'Fluent message', 'OneRegistration');
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
constraint('FluentWithMessage')(FluentWithMessage);
|
|
77
|
+
class Another {}
|
|
78
|
+
eventType('another')(Another);
|
|
79
|
+
class FluentWithAnotherMessage implements IConstraint {
|
|
80
|
+
define(builder: IConstraintBuilder): void {
|
|
81
|
+
builder.uniqueFor(Another, 'Another message', 'OneRegistration');
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
constraint('FluentWithAnotherMessage')(FluentWithAnotherMessage);
|
|
85
|
+
const { definitions, constraints } = await discover([], [FluentWithoutMessage, FluentWithMessage, FluentWithAnotherMessage]);
|
|
86
|
+
definitions.length.should.equal(1);
|
|
87
|
+
definitions[0].Definition.Value1!.EventTypeIds.should.deep.equal(['once', 'additional', 'another']);
|
|
88
|
+
constraints.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: {} }).message
|
|
89
|
+
.should.equal('Fluent message');
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('when merging a fluent unique event type without a message and a decorated event type with a message', () => {
|
|
94
|
+
it('should use the decorated message', async () => {
|
|
95
|
+
class DecoratedOnce {}
|
|
96
|
+
eventType('decorated-once')(DecoratedOnce);
|
|
97
|
+
unique('OneRegistration', 'Decorated message')(DecoratedOnce);
|
|
98
|
+
const { constraints } = await discover([DecoratedOnce], [FluentWithoutMessage]);
|
|
99
|
+
constraints.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: {} }).message
|
|
100
|
+
.should.equal('Decorated message');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
describe('when combining fluent and decorated unique properties', () => {
|
|
105
|
+
it('should not register the same property twice', async () => {
|
|
106
|
+
const { definitions } = await discover([ValueClaimed], [FluentValue]);
|
|
107
|
+
definitions.length.should.equal(1);
|
|
108
|
+
definitions[0].Definition.Value0!.EventDefinitions[0].Properties.should.deep.equal(['address']);
|
|
109
|
+
});
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
describe('when a fluent property constraint and decorated event type share a name', () => {
|
|
113
|
+
it('should reject conflicting constraint definitions', async () => {
|
|
114
|
+
class Conflicting implements IConstraint {
|
|
115
|
+
define(builder: IConstraintBuilder): void {
|
|
116
|
+
builder.unique(uniqueBuilder => uniqueBuilder.on(ValueClaimed, event => event.address));
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
constraint('OneRegistration')(Conflicting);
|
|
120
|
+
const artifacts = { eventTypes: [Once], constraints: [Conflicting] } as unknown as IClientArtifactsProvider;
|
|
121
|
+
const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
|
|
122
|
+
await constraints.discover().then(
|
|
123
|
+
() => should.fail('Expected conflicting constraint types'),
|
|
124
|
+
(error: Error) => error.message.should.match(/not a unique event type constraint/)
|
|
125
|
+
);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
describe('when decorating two properties on the same event with one unique name', () => {
|
|
130
|
+
it('should reject the second property instead of silently making a compound key', async () => {
|
|
131
|
+
class PairedValues { address = ''; second = ''; }
|
|
132
|
+
eventType('paired-values')(PairedValues);
|
|
133
|
+
unique('Pair')(PairedValues.prototype, 'address');
|
|
134
|
+
unique('Pair')(PairedValues.prototype, 'second');
|
|
135
|
+
const artifacts = { eventTypes: [PairedValues], constraints: [] } as unknown as IClientArtifactsProvider;
|
|
136
|
+
const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
|
|
137
|
+
await constraints.discover().then(
|
|
138
|
+
() => should.fail('Expected a duplicate event type error'),
|
|
139
|
+
(error: Error) => error.message.should.match(/already added to unique constraint 'Pair'/)
|
|
140
|
+
);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
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 '../../eventTypeDecorator.js';
|
|
8
|
+
import { Constraints } from '../Constraints.js';
|
|
9
|
+
import { removeConstraint } from '../removeConstraint.js';
|
|
10
|
+
import { unique } from '../unique.js';
|
|
11
|
+
|
|
12
|
+
const should = chai.should();
|
|
13
|
+
|
|
14
|
+
@eventType('legacy-reserved')
|
|
15
|
+
class Reserved {
|
|
16
|
+
@unique('LegacyValue', 'Already reserved') value = '';
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
@removeConstraint('LegacyValue')
|
|
20
|
+
class BaseRelease {}
|
|
21
|
+
|
|
22
|
+
@eventType('legacy-released')
|
|
23
|
+
class Released extends BaseRelease {}
|
|
24
|
+
|
|
25
|
+
describe('when registering legacy decorators with an inherited removal', () => {
|
|
26
|
+
it('should register the inherited removal and the decorated property', async () => {
|
|
27
|
+
const register = vi.fn().mockResolvedValue({});
|
|
28
|
+
const artifacts = { eventTypes: [Reserved, Released], constraints: [] } as unknown as IClientArtifactsProvider;
|
|
29
|
+
const constraints = new Constraints('store', { constraints: { register } } as unknown as ChronicleConnection, artifacts);
|
|
30
|
+
await constraints.register();
|
|
31
|
+
const definition = register.mock.calls[0][0].Constraints[0];
|
|
32
|
+
definition.Name.should.equal('LegacyValue');
|
|
33
|
+
definition.Definition.Value0.EventDefinitions.should.deep.equal([{ EventTypeId: 'legacy-reserved', Properties: ['value'] }]);
|
|
34
|
+
definition.RemovedWith.should.deep.equal(['legacy-released']);
|
|
35
|
+
});
|
|
36
|
+
});
|
|
@@ -0,0 +1,63 @@
|
|
|
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 { ConstraintType } from '@cratis/chronicle.contracts';
|
|
6
|
+
import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
8
|
+
import { eventType } from '../../eventTypeDecorator.js';
|
|
9
|
+
import { constraint } from '../constraint.js';
|
|
10
|
+
import { Constraints } from '../Constraints.js';
|
|
11
|
+
import type { IConstraint } from '../IConstraint.js';
|
|
12
|
+
import type { IConstraintBuilder } from '../IConstraintBuilder.js';
|
|
13
|
+
import { unique, getUniquePropertyMetadata } from '../unique.js';
|
|
14
|
+
import { removeConstraint } from '../removeConstraint.js';
|
|
15
|
+
|
|
16
|
+
const should = chai.should();
|
|
17
|
+
|
|
18
|
+
class Registered { address = ''; }
|
|
19
|
+
class AddressChanged { contact = ''; }
|
|
20
|
+
class Abandoned {}
|
|
21
|
+
class Deleted {}
|
|
22
|
+
class Reset {}
|
|
23
|
+
for (const [type, id] of [
|
|
24
|
+
[Registered, 'decorator-registered'], [AddressChanged, 'decorator-changed'],
|
|
25
|
+
[Abandoned, 'decorator-abandoned'], [Deleted, 'decorator-deleted'], [Reset, 'decorator-reset']
|
|
26
|
+
] as const) eventType(id)(type);
|
|
27
|
+
unique('SharedAddress', 'Address {email} already registered')(Registered.prototype, 'address');
|
|
28
|
+
unique('SharedAddress')(AddressChanged.prototype, 'contact');
|
|
29
|
+
removeConstraint('SharedAddress')(Abandoned);
|
|
30
|
+
removeConstraint('SharedAddress')(Deleted);
|
|
31
|
+
removeConstraint('SharedAddress')(Reset);
|
|
32
|
+
|
|
33
|
+
class FluentAddress implements IConstraint {
|
|
34
|
+
define(builder: IConstraintBuilder): void {
|
|
35
|
+
builder.unique(uniqueBuilder => {
|
|
36
|
+
uniqueBuilder.on(Registered, event => event.address);
|
|
37
|
+
uniqueBuilder.on(AddressChanged, event => event.contact);
|
|
38
|
+
uniqueBuilder.withMessage('Address {email} already registered');
|
|
39
|
+
uniqueBuilder.removedWith(Abandoned).removedWith(Deleted).removedWith(Reset);
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
constraint('SharedAddress')(FluentAddress);
|
|
44
|
+
|
|
45
|
+
async function register(types: Function[], constraintTypes: (new () => IConstraint)[] = []) {
|
|
46
|
+
const send = vi.fn().mockResolvedValue({});
|
|
47
|
+
const connection = { constraints: { register: send } } as unknown as ChronicleConnection;
|
|
48
|
+
const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
|
|
49
|
+
await new Constraints('store', connection, artifacts).register();
|
|
50
|
+
return send.mock.calls[0][0].Constraints;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
describe('when registering decorated unique properties', () => {
|
|
54
|
+
it('should group properties by name with all releasing events and match the fluent definition', async () => {
|
|
55
|
+
const decorated = (await register([Registered, AddressChanged, Abandoned, Deleted, Reset]))[0];
|
|
56
|
+
const fluent = (await register([], [FluentAddress]))[0];
|
|
57
|
+
decorated.should.deep.equal(fluent);
|
|
58
|
+
decorated.Type.should.equal(ConstraintType.Unique);
|
|
59
|
+
decorated.RemovedWith.should.deep.equal(['decorator-abandoned', 'decorator-deleted', 'decorator-reset']);
|
|
60
|
+
should.equal(decorated.Definition.Value0.IgnoreCasing, false);
|
|
61
|
+
getUniquePropertyMetadata(Registered, 'address')!.message!.should.equal('Address {email} already registered');
|
|
62
|
+
});
|
|
63
|
+
});
|
|
@@ -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 { chai, describe, it, vi } from 'vitest';
|
|
5
|
+
import { ConstraintType } from '@cratis/chronicle.contracts';
|
|
6
|
+
import type { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
|
|
7
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
8
|
+
import { eventType } from '../../eventTypeDecorator.js';
|
|
9
|
+
import { constraint } from '../constraint.js';
|
|
10
|
+
import { Constraints } from '../Constraints.js';
|
|
11
|
+
import type { IConstraint } from '../IConstraint.js';
|
|
12
|
+
import type { IConstraintBuilder } from '../IConstraintBuilder.js';
|
|
13
|
+
import { unique } from '../unique.js';
|
|
14
|
+
import { removeConstraint } from '../removeConstraint.js';
|
|
15
|
+
|
|
16
|
+
const should = chai.should();
|
|
17
|
+
class OnlyOnce {}
|
|
18
|
+
class Alternate {}
|
|
19
|
+
class Reset {}
|
|
20
|
+
class DefaultName { value = ''; }
|
|
21
|
+
class DefaultEventType {}
|
|
22
|
+
for (const [type, id] of [
|
|
23
|
+
[OnlyOnce, 'decorator-once'], [Alternate, 'decorator-alternate'], [Reset, 'decorator-reset'],
|
|
24
|
+
[DefaultName, 'decorator-default'], [DefaultEventType, 'decorator-default-event']
|
|
25
|
+
] as const) eventType(id)(type);
|
|
26
|
+
unique()(DefaultName.prototype, 'value');
|
|
27
|
+
unique()(DefaultEventType);
|
|
28
|
+
unique('OneRegistration', 'Already registered')(OnlyOnce);
|
|
29
|
+
unique('OneRegistration')(Alternate);
|
|
30
|
+
removeConstraint('OneRegistration')(Reset);
|
|
31
|
+
|
|
32
|
+
class FluentRegistration implements IConstraint {
|
|
33
|
+
define(builder: IConstraintBuilder): void {
|
|
34
|
+
builder.uniqueFor(OnlyOnce, 'Already registered', 'OneRegistration');
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
constraint('OneRegistration')(FluentRegistration);
|
|
38
|
+
|
|
39
|
+
async function register(types: Function[], constraintTypes: (new () => IConstraint)[] = []) {
|
|
40
|
+
const send = vi.fn().mockResolvedValue({});
|
|
41
|
+
const connection = { constraints: { register: send } } as unknown as ChronicleConnection;
|
|
42
|
+
const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
|
|
43
|
+
await new Constraints('store', connection, artifacts).register();
|
|
44
|
+
return send.mock.calls[0][0].Constraints;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
describe('when registering decorated event types', () => {
|
|
48
|
+
it('should use the property name as the default property constraint name', async () => {
|
|
49
|
+
const definitions = await register([DefaultName]);
|
|
50
|
+
definitions[0].Name.should.equal('value');
|
|
51
|
+
definitions[0].Definition.Value0.EventDefinitions.should.deep.equal([{ EventTypeId: 'decorator-default', Properties: ['value'] }]);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should use the class name rather than the event type id as the default event constraint name', async () => {
|
|
55
|
+
const definitions = await register([DefaultEventType]);
|
|
56
|
+
definitions[0].Name.should.equal('DefaultEventType');
|
|
57
|
+
definitions[0].Definition.Value1.EventTypeIds.should.deep.equal(['decorator-default-event']);
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it('should match the fluent unique event type definition', async () => {
|
|
61
|
+
const decorated = (await register([OnlyOnce]))[0];
|
|
62
|
+
const fluent = (await register([], [FluentRegistration]))[0];
|
|
63
|
+
decorated.should.deep.equal(fluent);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should group class-level unique declarations sharing a name', async () => {
|
|
67
|
+
const definitions = await register([OnlyOnce, Alternate, Reset]);
|
|
68
|
+
definitions.length.should.equal(1);
|
|
69
|
+
definitions[0].Type.should.equal(ConstraintType.UniqueEventType);
|
|
70
|
+
definitions[0].Definition.Value1.EventTypeIds.should.deep.equal(['decorator-once', 'decorator-alternate']);
|
|
71
|
+
definitions[0].RemovedWith.should.deep.equal(['decorator-reset']);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('should retain the first supplied message when the first declaration has none', async () => {
|
|
75
|
+
class First {}
|
|
76
|
+
class Second {}
|
|
77
|
+
class Third {}
|
|
78
|
+
eventType('first-registration')(First);
|
|
79
|
+
eventType('second-registration')(Second);
|
|
80
|
+
eventType('third-registration')(Third);
|
|
81
|
+
unique('SharedRegistration')(First);
|
|
82
|
+
unique('SharedRegistration', 'Second message')(Second);
|
|
83
|
+
unique('SharedRegistration', 'Third message')(Third);
|
|
84
|
+
const artifacts = { eventTypes: [First, Second, Third], constraints: [] } as IClientArtifactsProvider;
|
|
85
|
+
const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
|
|
86
|
+
await constraints.discover();
|
|
87
|
+
constraints.resolveMessageFor({ constraintId: 'SharedRegistration', message: 'Kernel', details: {} }).message
|
|
88
|
+
.should.equal('Second message');
|
|
89
|
+
});
|
|
90
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
// Copyright (c) Cratis. All rights reserved.
|
|
2
|
+
// Licensed under the MIT license. See LICENSE file in the project root for full license information.
|
|
3
|
+
|
|
4
|
+
import { chai, describe, it } from 'vitest';
|
|
5
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/index.js';
|
|
6
|
+
import type { ChronicleConnection } from '../../../connection/index.js';
|
|
7
|
+
import { eventType } from '../../eventTypeDecorator.js';
|
|
8
|
+
import { constraint } from '../constraint.js';
|
|
9
|
+
import { Constraints } from '../Constraints.js';
|
|
10
|
+
import type { IConstraint } from '../IConstraint.js';
|
|
11
|
+
import type { IConstraintBuilder } from '../IConstraintBuilder.js';
|
|
12
|
+
import { unique } from '../unique.js';
|
|
13
|
+
|
|
14
|
+
const should = chai.should();
|
|
15
|
+
|
|
16
|
+
async function assertDiscoveryFails(eventTypes: Function[], constraintTypes: (new () => IConstraint)[], message: RegExp): Promise<void> {
|
|
17
|
+
const artifacts = { eventTypes, constraints: constraintTypes } as IClientArtifactsProvider;
|
|
18
|
+
const constraints = new Constraints('store', {} as ChronicleConnection, artifacts);
|
|
19
|
+
await constraints.discover().then(
|
|
20
|
+
() => should.fail('Expected incompatible constraint definitions'),
|
|
21
|
+
(error: Error) => error.message.should.match(message)
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe('when two fluent unique event type constraints use the same class id', () => {
|
|
26
|
+
it('should reject the duplicate id even if their wire names differ', async () => {
|
|
27
|
+
class First {}
|
|
28
|
+
class Second {}
|
|
29
|
+
eventType('merge-first')(First);
|
|
30
|
+
eventType('merge-second')(Second);
|
|
31
|
+
class FirstConstraint implements IConstraint {
|
|
32
|
+
define(builder: IConstraintBuilder): void { builder.uniqueFor(First, undefined, 'FirstName'); }
|
|
33
|
+
}
|
|
34
|
+
class SecondConstraint implements IConstraint {
|
|
35
|
+
define(builder: IConstraintBuilder): void { builder.uniqueFor(Second, undefined, 'SecondName'); }
|
|
36
|
+
}
|
|
37
|
+
constraint('SharedId')(FirstConstraint);
|
|
38
|
+
constraint('SharedId')(SecondConstraint);
|
|
39
|
+
await assertDiscoveryFails([], [FirstConstraint, SecondConstraint], /Duplicate constraint id 'SharedId'/);
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('when merging fluent unique event types with different scopes', () => {
|
|
44
|
+
it('should reject the conflicting scope', async () => {
|
|
45
|
+
class First {}
|
|
46
|
+
class Second {}
|
|
47
|
+
eventType('merge-first')(First);
|
|
48
|
+
eventType('merge-second')(Second);
|
|
49
|
+
class Global implements IConstraint {
|
|
50
|
+
define(builder: IConstraintBuilder): void { builder.uniqueFor(First, undefined, 'Shared'); }
|
|
51
|
+
}
|
|
52
|
+
class Scoped implements IConstraint {
|
|
53
|
+
define(builder: IConstraintBuilder): void { builder.perEventStreamId().uniqueFor(Second, undefined, 'Shared'); }
|
|
54
|
+
}
|
|
55
|
+
constraint('Global')(Global);
|
|
56
|
+
constraint('Scoped')(Scoped);
|
|
57
|
+
await assertDiscoveryFails([], [Global, Scoped], /Conflicting scopes for constraint 'Shared'/);
|
|
58
|
+
});
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
describe('when merging a scoped fluent unique event type with a decorator', () => {
|
|
62
|
+
it('should reject the conflicting scope', async () => {
|
|
63
|
+
class First {}
|
|
64
|
+
class Second {}
|
|
65
|
+
eventType('merge-first')(First);
|
|
66
|
+
eventType('merge-second')(Second);
|
|
67
|
+
class Scoped implements IConstraint {
|
|
68
|
+
define(builder: IConstraintBuilder): void { builder.perEventSourceType().uniqueFor(First, undefined, 'Shared'); }
|
|
69
|
+
}
|
|
70
|
+
constraint('ScopedEvent')(Scoped);
|
|
71
|
+
unique('Shared')(Second);
|
|
72
|
+
await assertDiscoveryFails([Second], [Scoped], /Conflicting scopes for constraint 'Shared'/);
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('when merging a scoped fluent unique property with a decorator', () => {
|
|
77
|
+
it('should reject the conflicting scope', async () => {
|
|
78
|
+
class First { address = ''; }
|
|
79
|
+
class Second { address = ''; }
|
|
80
|
+
eventType('merge-first')(First);
|
|
81
|
+
eventType('merge-second')(Second);
|
|
82
|
+
class Scoped implements IConstraint {
|
|
83
|
+
define(builder: IConstraintBuilder): void {
|
|
84
|
+
builder.perEventStreamType().unique(uniqueBuilder => uniqueBuilder.on(First, event => event.address));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
constraint('SharedProperty')(Scoped);
|
|
88
|
+
unique('SharedProperty')(Second.prototype, 'address');
|
|
89
|
+
await assertDiscoveryFails([Second], [Scoped], /Conflicting scopes for constraint 'SharedProperty'/);
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe('when merging a case-insensitive fluent unique property with a decorator', () => {
|
|
94
|
+
it('should reject the conflicting casing setting', async () => {
|
|
95
|
+
class First { address = ''; }
|
|
96
|
+
class Second { address = ''; }
|
|
97
|
+
eventType('merge-first')(First);
|
|
98
|
+
eventType('merge-second')(Second);
|
|
99
|
+
class CaseInsensitive implements IConstraint {
|
|
100
|
+
define(builder: IConstraintBuilder): void {
|
|
101
|
+
builder.unique(uniqueBuilder => uniqueBuilder.on(First, event => event.address).ignoreCasing());
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
constraint('SharedProperty')(CaseInsensitive);
|
|
105
|
+
unique('SharedProperty')(Second.prototype, 'address');
|
|
106
|
+
await assertDiscoveryFails([Second], [CaseInsensitive], /Conflicting ignoreCasing for unique property constraint 'SharedProperty'/);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,92 @@
|
|
|
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 { ChronicleConnection } from '../../../connection/ChronicleConnection.js';
|
|
6
|
+
import type { IClientArtifactsProvider } from '../../../artifacts/IClientArtifactsProvider.js';
|
|
7
|
+
import { EventSequence } from '../../../eventSequences/EventSequence.js';
|
|
8
|
+
import { EventSequenceId } from '../../../eventSequences/EventSequenceId.js';
|
|
9
|
+
import type { IUnitOfWorkManager } from '../../../transactions/IUnitOfWorkManager.js';
|
|
10
|
+
import { eventType } from '../../eventTypeDecorator.js';
|
|
11
|
+
import { constraint } from '../constraint.js';
|
|
12
|
+
import { Constraints } from '../Constraints.js';
|
|
13
|
+
import type { IConstraint } from '../IConstraint.js';
|
|
14
|
+
import type { IConstraintBuilder } from '../IConstraintBuilder.js';
|
|
15
|
+
import { unique } from '../unique.js';
|
|
16
|
+
|
|
17
|
+
const should = chai.should();
|
|
18
|
+
class Registered { email = ''; }
|
|
19
|
+
class OnlyOnce {}
|
|
20
|
+
eventType('message-registered')(Registered);
|
|
21
|
+
eventType('message-once')(OnlyOnce);
|
|
22
|
+
unique('SharedAddress', 'Address {email} already registered')(Registered.prototype, 'email');
|
|
23
|
+
unique('OneRegistration', 'Already registered')(OnlyOnce);
|
|
24
|
+
|
|
25
|
+
class FluentRegistration implements IConstraint {
|
|
26
|
+
define(builder: IConstraintBuilder): void {
|
|
27
|
+
builder.uniqueFor(OnlyOnce, 'Already registered', 'OneRegistration');
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
constraint('FluentRegistration')(FluentRegistration);
|
|
31
|
+
|
|
32
|
+
class FluentAddress implements IConstraint {
|
|
33
|
+
define(builder: IConstraintBuilder): void {
|
|
34
|
+
builder.unique(uniqueBuilder => uniqueBuilder.on(Registered, event => event.email).withMessage('Address {email} already registered'));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
constraint('SharedAddress')(FluentAddress);
|
|
38
|
+
|
|
39
|
+
function constraints(types: Function[], constraintTypes: (new () => IConstraint)[] = []): Constraints {
|
|
40
|
+
const artifacts = { eventTypes: types, constraints: constraintTypes } as IClientArtifactsProvider;
|
|
41
|
+
return new Constraints('store', { constraints: { register: vi.fn() } } as unknown as ChronicleConnection, artifacts);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
describe('when resolving violation messages', () => {
|
|
45
|
+
it('should substitute detail values literally and preserve unknown violations', async () => {
|
|
46
|
+
const discovered = constraints([Registered]);
|
|
47
|
+
await discovered.discover();
|
|
48
|
+
discovered.resolveMessageFor({ constraintId: 'SharedAddress', message: 'Kernel', details: { email: '$& $$ $` $\'' } }).message
|
|
49
|
+
.should.equal('Address $& $$ $` $\' already registered');
|
|
50
|
+
const violation = { constraintId: 'unknown', message: 'Kernel default', details: {} };
|
|
51
|
+
discovered.resolveMessageFor(violation).should.equal(violation);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should resolve fluent withMessage values on append results', async () => {
|
|
55
|
+
const discovered = constraints([Registered], [FluentAddress]);
|
|
56
|
+
await discovered.discover();
|
|
57
|
+
const connection = {
|
|
58
|
+
eventSequences: { append: vi.fn().mockResolvedValue({ Response: {
|
|
59
|
+
SequenceNumber: 0n, Errors: [], ConstraintViolations: [{
|
|
60
|
+
ConstraintId: 'SharedAddress', Message: 'Kernel default', Details: { email: 'a$b' }
|
|
61
|
+
}]
|
|
62
|
+
} }) }
|
|
63
|
+
} as unknown as ChronicleConnection;
|
|
64
|
+
const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
|
|
65
|
+
{} as IUnitOfWorkManager, discovered.resolveMessageFor.bind(discovered));
|
|
66
|
+
const result = await sequence.append('source', new Registered());
|
|
67
|
+
result.constraintViolations[0].message.should.equal('Address a$b already registered');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
it('should resolve a named fluent event type with a different class id', async () => {
|
|
71
|
+
const discovered = constraints([], [FluentRegistration]);
|
|
72
|
+
await discovered.discover();
|
|
73
|
+
discovered.resolveMessageFor({ constraintId: 'OneRegistration', message: 'Kernel', details: {} }).message
|
|
74
|
+
.should.equal('Already registered');
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
it('should resolve decorated event type messages on append results', async () => {
|
|
78
|
+
const discovered = constraints([OnlyOnce]);
|
|
79
|
+
await discovered.discover();
|
|
80
|
+
const connection = {
|
|
81
|
+
eventSequences: { append: vi.fn().mockResolvedValue({ Response: {
|
|
82
|
+
SequenceNumber: 0n, Errors: [], ConstraintViolations: [{
|
|
83
|
+
ConstraintId: 'OneRegistration', Message: 'Kernel default', Details: {}
|
|
84
|
+
}]
|
|
85
|
+
} }) }
|
|
86
|
+
} as unknown as ChronicleConnection;
|
|
87
|
+
const sequence = new EventSequence(EventSequenceId.eventLog, 'store', 'namespace', connection,
|
|
88
|
+
{} as IUnitOfWorkManager, discovered.resolveMessageFor.bind(discovered));
|
|
89
|
+
const result = await sequence.append('source', new OnlyOnce());
|
|
90
|
+
result.constraintViolations[0].message.should.equal('Already registered');
|
|
91
|
+
});
|
|
92
|
+
});
|
|
@@ -6,6 +6,8 @@ export type { IConstraint } from './IConstraint.js';
|
|
|
6
6
|
export type { IConstraintBuilder } from './IConstraintBuilder.js';
|
|
7
7
|
export type { IUniqueConstraintBuilder } from './IUniqueConstraintBuilder.js';
|
|
8
8
|
export { constraint, getConstraintMetadata, isConstraint } from './constraint.js';
|
|
9
|
+
export { unique } from './unique.js';
|
|
10
|
+
export { removeConstraint } from './removeConstraint.js';
|
|
9
11
|
export type { ConstraintMetadata } from './constraint.js';
|
|
10
12
|
export type { IConstraints } from './IConstraints.js';
|
|
11
13
|
export { Constraints } from './Constraints.js';
|
|
@@ -0,0 +1,28 @@
|
|
|
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 { ChronicleClassDecorator } from '../../types/standardDecoratorMetadata.js';
|
|
6
|
+
|
|
7
|
+
const KEY = 'chronicle:constraint:remove';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Marks an event class as releasing a named constraint when appended. May be applied repeatedly.
|
|
11
|
+
* @param name - Exact name of the constraint to release.
|
|
12
|
+
* @returns A class decorator.
|
|
13
|
+
*/
|
|
14
|
+
export function removeConstraint(name: string): ChronicleClassDecorator {
|
|
15
|
+
return (target: object) => {
|
|
16
|
+
const existing = Reflect.getOwnMetadata(KEY, target) as string[] | undefined ?? [];
|
|
17
|
+
Reflect.defineMetadata(KEY, [...existing, name], target);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/** Gets the constraint names released by this event class. */
|
|
22
|
+
export function getRemovedConstraintNames(type: Function): string[] {
|
|
23
|
+
const names: string[] = [];
|
|
24
|
+
for (let current: object | null = type; current && current !== Function.prototype; current = Object.getPrototypeOf(current)) {
|
|
25
|
+
names.push(...(Reflect.getOwnMetadata(KEY, current) as string[] | undefined ?? []));
|
|
26
|
+
}
|
|
27
|
+
return [...new Set(names)];
|
|
28
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
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 { ChronicleClassOrPropertyDecorator, decorateClassOrProperty, getPropertyMetadata, getTypeOrFieldMetadata } from '../../types/propertyDecoratorMetadata.js';
|
|
6
|
+
import { TypeIntrospector } from '../../types/TypeIntrospector.js';
|
|
7
|
+
|
|
8
|
+
const CLASS_KEY = 'chronicle:constraint:unique:class';
|
|
9
|
+
const PROPERTY_KEY = 'chronicle:constraint:unique:property';
|
|
10
|
+
|
|
11
|
+
interface UniqueMetadata {
|
|
12
|
+
name?: string;
|
|
13
|
+
message?: string;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Marks an event class as unique per event source, or an event property as unique across event sources.
|
|
18
|
+
* The optional name defaults to the class or property name. Like the .NET attribute, this decorator
|
|
19
|
+
* does not offer ignore-casing; use a fluent constraint for case-insensitive uniqueness.
|
|
20
|
+
* @param name - Optional shared constraint name.
|
|
21
|
+
* @param message - Optional fixed violation message.
|
|
22
|
+
* @returns A class or public instance field decorator.
|
|
23
|
+
*/
|
|
24
|
+
export function unique(name?: string, message?: string): ChronicleClassOrPropertyDecorator {
|
|
25
|
+
return decorateClassOrProperty((target, property) => {
|
|
26
|
+
const metadata: UniqueMetadata = { name, message };
|
|
27
|
+
if (property !== undefined) {
|
|
28
|
+
const key = property.toString();
|
|
29
|
+
TypeIntrospector.trackProperty((target as { constructor: Function }).constructor, key);
|
|
30
|
+
Reflect.defineMetadata(PROPERTY_KEY, metadata, target, key);
|
|
31
|
+
} else {
|
|
32
|
+
Reflect.defineMetadata(CLASS_KEY, metadata, target);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Gets the unique constraint declared on an event class, if present. */
|
|
38
|
+
export function getUniqueEventMetadata(type: Function): UniqueMetadata | undefined {
|
|
39
|
+
return getTypeOrFieldMetadata<UniqueMetadata>(CLASS_KEY, type);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Gets the unique constraint declared on an event property, if present. */
|
|
43
|
+
export function getUniquePropertyMetadata(type: Function, property: string): UniqueMetadata | undefined {
|
|
44
|
+
return getPropertyMetadata<UniqueMetadata>(PROPERTY_KEY, type.prototype, property);
|
|
45
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cratis/chronicle",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.9.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",
|
|
@@ -162,7 +162,7 @@
|
|
|
162
162
|
"build": "tsc -b",
|
|
163
163
|
"clean": "rm -rf dist",
|
|
164
164
|
"test": "vitest run",
|
|
165
|
-
"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/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts",
|
|
165
|
+
"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/modelBound/decorators.standard.spec.ts events/classDecorators.standard.spec.ts reactors/decorators.standard.spec.ts readModels/standardFallbacks.standard.spec.ts artifacts/validateArtifactSchemas.spec.ts",
|
|
166
166
|
"test:native": "node ./scripts/check-native-esm.mjs"
|
|
167
167
|
},
|
|
168
168
|
"dependencies": {
|