• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

snatalenko / node-cqrs / 10223129684

02 Aug 2024 11:08PM UTC coverage: 94.639%. First build
10223129684

Pull #21

github

snatalenko
Separate github workflows for tests and coveralls
Pull Request #21: Migrate to TypeScript

552 of 854 branches covered (64.64%)

2231 of 2360 new or added lines in 28 files covered. (94.53%)

2348 of 2481 relevant lines covered (94.64%)

21.9 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

99.1
/src/AbstractAggregate.ts
1
import {
4✔
2
        IAggregate,
4✔
3
        IAggregateState,
4✔
4
        ICommand,
4✔
5
        Identifier,
4✔
6
        IEvent,
4✔
7
        IEventSet,
4✔
8
        IAggregateConstructorParams
4✔
9
} from "./interfaces";
4✔
10

4✔
11
import { getClassName, validateHandlers, getHandler } from './utils';
4✔
12

4✔
13
/**
4✔
14
 * Deep-clone simple JS object
4✔
15
 */
4✔
16
function clone<T>(obj: T): T {
18✔
17
        return JSON.parse(JSON.stringify(obj));
18✔
18
}
18✔
19

4✔
20
const SNAPSHOT_EVENT_TYPE = 'snapshot';
4✔
21

4✔
22
/**
4✔
23
 * Base class for Aggregate definition
4✔
24
 */
4✔
25
export abstract class AbstractAggregate<TState extends IAggregateState|void> implements IAggregate {
78✔
26

78✔
27
        /**
78✔
28
         * Optional list of commands handled by Aggregate.
78✔
29
         * 
78✔
30
         * If not overridden in Aggregate implementation,
78✔
31
         * `AggregateCommandHandler` will treat all public methods as command handlers
78✔
32
         *
78✔
33
         * @example
78✔
34
         *         return ['createUser', 'changePassword'];
78✔
35
         */
78✔
36
        static get handles(): string[] | undefined {
78✔
37
                return undefined;
8✔
38
        }
8✔
39

78✔
40
        #id: Identifier;
78✔
41
        #changes: IEvent[] = [];
78✔
42
        #version: number = 0;
78✔
43
        #snapshotVersion: number | undefined;
78✔
44

78✔
45
        /** Internal aggregate state */
78✔
46
        protected state?: TState;
78✔
47

78✔
48
        /** Command being handled by aggregate */
78✔
49
        protected command?: ICommand;
78✔
50

78✔
51
        /** Unique aggregate instance identifier */
78✔
52
        get id(): Identifier {
78✔
53
                return this.#id;
38✔
54
        }
38✔
55

78✔
56
        /** Aggregate instance version */
78✔
57
        get version(): number {
78✔
58
                return this.#version;
64✔
59
        }
64✔
60

78✔
61
        /** Restored snapshot version */
78✔
62
        get snapshotVersion(): number | undefined {
78✔
63
                return this.#snapshotVersion;
4✔
64
        }
4✔
65

78✔
66
        /** Events emitted by Aggregate */
78✔
67
        get changes(): IEventSet {
78✔
68
                return [...this.#changes];
44✔
69
        }
44✔
70

78✔
71
        /**
78✔
72
         * Override to define whether an aggregate state snapshot should be taken
78✔
73
         *
78✔
74
         * @example
78✔
75
         *         // create snapshot every 50 events
78✔
76
         *         return this.version % 50 === 0;
78✔
77
         */
78✔
78
        get shouldTakeSnapshot(): boolean {
78✔
79
                return false;
12✔
80
        }
12✔
81

78✔
82
        constructor(options: IAggregateConstructorParams<TState>) {
78✔
83
                const { id, state, events } = options;
78✔
84
                if (!id)
78✔
85
                        throw new TypeError('id argument required');
78!
86
                if (state && typeof state !== 'object')
78✔
87
                        throw new TypeError('state argument, when provided, must be an Object');
78!
88
                if (events && !Array.isArray(events))
78✔
89
                        throw new TypeError('events argument, when provided, must be an Array');
78!
90

78✔
91
                this.#id = id;
78✔
92

78✔
93
                validateHandlers(this);
78✔
94

78✔
95
                if (state)
78✔
96
                        this.state = state;
78✔
97

76✔
98
                if (events)
76✔
99
                        events.forEach(event => this.mutate(event));
78✔
100
        }
78✔
101

78✔
102
        /** Pass command to command handler */
78✔
103
        handle(command: ICommand) {
78✔
104
                if (!command)
24✔
105
                        throw new TypeError('command argument required');
24!
106
                if (!command.type)
24✔
107
                        throw new TypeError('command.type argument required');
24!
108

24✔
109
                const handler = getHandler(this, command.type);
24✔
110
                if (!handler)
24✔
111
                        throw new Error(`'${command.type}' handler is not defined or not a function`);
24✔
112

22✔
113
                this.command = command;
22✔
114

22✔
115
                return handler.call(this, command.payload, command.context);
22✔
116
        }
22✔
117

78✔
118
        /** Mutate aggregate state and increment aggregate version */
78✔
119
        mutate(event) {
78✔
120
                if (event.aggregateVersion !== undefined)
58✔
121
                        this.#version = event.aggregateVersion;
58✔
122

58✔
123
                if (event.type === SNAPSHOT_EVENT_TYPE) {
58✔
124
                        this.#snapshotVersion = event.aggregateVersion;
10✔
125
                        this.restoreSnapshot(event);
10✔
126
                }
10✔
127
                else if (this.state) {
48✔
128
                        const handler = this.state.mutate || getHandler(this.state, event.type);
48✔
129
                        if (handler)
48✔
130
                                handler.call(this.state, event);
48✔
131
                }
48✔
132

58✔
133
                this.#version += 1;
58✔
134
        }
58✔
135

78✔
136
        /** Format and register aggregate event and mutate aggregate state */
78✔
137
        protected emit<TPayload>(type: string, payload?: TPayload) {
78✔
138
                if (typeof type !== 'string' || !type.length)
36✔
139
                        throw new TypeError('type argument must be a non-empty string');
36!
140

36✔
141
                const event = this.makeEvent<TPayload>(type, payload, this.command);
36✔
142

36✔
143
                this.emitRaw(event);
36✔
144
        }
36✔
145

78✔
146
        /** Format event based on a current aggregate state and a command being executed */
78✔
147
        protected makeEvent<TPayload>(type: string, payload?: TPayload, sourceCommand?: ICommand): IEvent<TPayload> {
78✔
148
                const event: IEvent<TPayload> = {
36✔
149
                        aggregateId: this.id,
36✔
150
                        aggregateVersion: this.version,
36✔
151
                        type,
36✔
152
                        payload
36✔
153
                };
36✔
154

36✔
155
                if (sourceCommand) {
36✔
156
                        // augment event with command context
26✔
157
                        const { context, sagaId, sagaVersion } = sourceCommand;
26✔
158
                        if (context !== undefined)
26✔
159
                                event.context = context;
26✔
160
                        if (sagaId !== undefined)
26✔
161
                                event.sagaId = sagaId;
26✔
162
                        if (sagaVersion !== undefined)
26✔
163
                                event.sagaVersion = sagaVersion;
26✔
164
                }
26✔
165

36✔
166
                return event;
36✔
167
        }
36✔
168

78✔
169
        /** Register aggregate event and mutate aggregate state */
78✔
170
        protected emitRaw<TPayload>(event: IEvent<TPayload>): void {
78✔
171
                if (!event)
36✔
172
                        throw new TypeError('event argument required');
36!
173
                if (!event.aggregateId)
36✔
174
                        throw new TypeError('event.aggregateId argument required');
36!
175
                if (typeof event.aggregateVersion !== 'number')
36✔
176
                        throw new TypeError('event.aggregateVersion argument must be a Number');
36!
177
                if (typeof event.type !== 'string' || !event.type.length)
36✔
178
                        throw new TypeError('event.type argument must be a non-empty String');
36!
179

36✔
180
                this.mutate(event);
36✔
181

36✔
182
                this.#changes.push(event);
36✔
183
        }
36✔
184

78✔
185
        /**
78✔
186
         * Take an aggregate state snapshot and add it to the changes queue
78✔
187
         */
78✔
188
        takeSnapshot() {
78✔
189
                this.emit(SNAPSHOT_EVENT_TYPE, this.makeSnapshot());
4✔
190
        }
4✔
191

78✔
192
        /** Create an aggregate state snapshot */
78✔
193
        makeSnapshot(): TState {
78✔
194
                if (!this.state)
4✔
195
                        throw new Error('state property is empty, either define state or override makeSnapshot method');
4!
196

4✔
197
                return clone(this.state);
4✔
198
        }
4✔
199

78✔
200
        /** Restore aggregate state from a snapshot */
78✔
201
        protected restoreSnapshot(snapshotEvent: IEvent<TState>) {
78✔
202
                if (!snapshotEvent)
22✔
203
                        throw new TypeError('snapshotEvent argument required');
22✔
204
                if (!snapshotEvent.type)
20✔
205
                        throw new TypeError('snapshotEvent.type argument required');
22✔
206
                if (!snapshotEvent.payload)
18✔
207
                        throw new TypeError('snapshotEvent.payload argument required');
22✔
208

16✔
209
                if (snapshotEvent.type !== SNAPSHOT_EVENT_TYPE)
16✔
210
                        throw new Error(`${SNAPSHOT_EVENT_TYPE} event type expected`);
22✔
211
                if (!this.state)
14✔
212
                        throw new Error('state property is empty, either defined state or override restoreSnapshot method');
22!
213

14✔
214
                Object.assign(this.state, clone(snapshotEvent.payload));
14✔
215
        }
14✔
216

78✔
217
        /** Get human-readable aggregate identifier */
78✔
218
        toString(): string {
78✔
NEW
219
                return `${getClassName(this)} ${this.id} (v${this.version})`;
×
NEW
220
        }
×
221
}
78✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc