@cratis/chronicle 6.7.1 → 6.8.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/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 +1 -1
|
@@ -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