event-store-adapter-js 1.0.9 → 1.0.10-snapshot.2

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.
@@ -1,7 +1,7 @@
1
1
  import { Aggregate, AggregateId, Event, EventSerializer, KeyResolver, Logger, SnapshotSerializer } from "../types";
2
2
  import { EventStore } from "../event-store";
3
3
  import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
4
- import * as moment from "moment/moment";
4
+ import moment from "moment/moment";
5
5
  declare class EventStoreForDynamoDB<AID extends AggregateId, A extends Aggregate<A, AID>, E extends Event<AID>> implements EventStore<AID, A, E> {
6
6
  private dynamodbClient;
7
7
  private journalTableName;
@@ -30,5 +30,10 @@ declare class EventStoreForDynamoDB<AID extends AggregateId, A extends Aggregate
30
30
  private putJournal;
31
31
  private putSnapshot;
32
32
  private updateSnapshot;
33
+ private tryPurgeExcessSnapshots;
34
+ private getSnapshotCount;
35
+ private getLastSnapshotKeys;
36
+ private updateTtlOfExcessSnapshots;
37
+ private deleteExcessSnapshots;
33
38
  }
34
39
  export { EventStoreForDynamoDB };
@@ -8,9 +8,13 @@ var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, ge
8
8
  step((generator = generator.apply(thisArg, _arguments || [])).next());
9
9
  });
10
10
  };
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
11
14
  Object.defineProperty(exports, "__esModule", { value: true });
12
15
  exports.EventStoreForDynamoDB = void 0;
13
16
  const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
17
+ const moment_1 = __importDefault(require("moment/moment"));
14
18
  const default_key_resolver_1 = require("./default-key-resolver");
15
19
  const default_serializer_1 = require("./default-serializer");
16
20
  class EventStoreForDynamoDB {
@@ -109,21 +113,22 @@ class EventStoreForDynamoDB {
109
113
  throw new Error("Cannot persist created event");
110
114
  }
111
115
  yield this.updateEventAndSnapshotOpt(event, version, undefined);
116
+ yield this.tryPurgeExcessSnapshots(event);
112
117
  (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`persistEvent(${JSON.stringify(event)}, ${version}): finished`);
113
118
  });
114
119
  }
115
120
  persistEventAndSnapshot(event, aggregate) {
116
- var _a, _b, _c;
121
+ var _a, _b;
117
122
  return __awaiter(this, void 0, void 0, function* () {
118
123
  (_a = this.logger) === null || _a === void 0 ? void 0 : _a.debug(`persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(aggregate)}): start`);
119
124
  if (event.isCreated) {
120
125
  yield this.createEventAndSnapshot(event, aggregate);
121
126
  }
122
127
  else {
123
- (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug("update!!!");
124
128
  yield this.updateEventAndSnapshotOpt(event, aggregate.sequenceNumber, aggregate);
129
+ yield this.tryPurgeExcessSnapshots(event);
125
130
  }
126
- (_c = this.logger) === null || _c === void 0 ? void 0 : _c.debug(`persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(aggregate)}): finished`);
131
+ (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(aggregate)}): finished`);
127
132
  });
128
133
  }
129
134
  withDeleteTtl(deleteTtl) {
@@ -274,5 +279,148 @@ class EventStoreForDynamoDB {
274
279
  (_b = this.logger) === null || _b === void 0 ? void 0 : _b.debug(`updateSnapshot(${JSON.stringify(event)}, ${sequenceNumber}, ${version}, ${JSON.stringify(aggregate)}): finished`);
275
280
  return result;
276
281
  }
282
+ tryPurgeExcessSnapshots(event) {
283
+ return __awaiter(this, void 0, void 0, function* () {
284
+ if (this.keepSnapshotCount !== undefined) {
285
+ if (this.deleteTtl !== undefined) {
286
+ yield this.updateTtlOfExcessSnapshots(event.aggregateId);
287
+ }
288
+ else {
289
+ yield this.deleteExcessSnapshots(event.aggregateId);
290
+ }
291
+ }
292
+ });
293
+ }
294
+ getSnapshotCount(aggregateId) {
295
+ return __awaiter(this, void 0, void 0, function* () {
296
+ const request = {
297
+ TableName: this.snapshotTableName,
298
+ IndexName: this.snapshotAidIndexName,
299
+ KeyConditionExpression: "#aid = :aid",
300
+ ExpressionAttributeNames: {
301
+ "#aid": "aid",
302
+ },
303
+ ExpressionAttributeValues: {
304
+ ":aid": { S: aggregateId.asString },
305
+ },
306
+ Select: "COUNT",
307
+ };
308
+ const queryResult = yield this.dynamodbClient.send(new client_dynamodb_1.QueryCommand(request));
309
+ return queryResult.Count;
310
+ });
311
+ }
312
+ getLastSnapshotKeys(aggregateId, limit) {
313
+ return __awaiter(this, void 0, void 0, function* () {
314
+ const names = {
315
+ "#aid": "aid",
316
+ "#seq_nr": "seq_nr",
317
+ };
318
+ const values = {
319
+ ":aid": { S: aggregateId.asString },
320
+ ":seq_nr": { N: "0" },
321
+ };
322
+ const request = {
323
+ TableName: this.snapshotTableName,
324
+ IndexName: this.snapshotAidIndexName,
325
+ KeyConditionExpression: "#aid = :aid AND #seq_nr > :seq_nr",
326
+ ExpressionAttributeNames: Object.assign({}, names),
327
+ ExpressionAttributeValues: Object.assign({}, values),
328
+ ScanIndexForward: false,
329
+ Limit: limit,
330
+ };
331
+ if (this.deleteTtl !== undefined) {
332
+ request.FilterExpression = "#ttl = :ttl";
333
+ request.ExpressionAttributeNames = Object.assign(Object.assign({}, names), { "#ttl": "ttl" });
334
+ request.ExpressionAttributeValues = Object.assign(Object.assign({}, values), { ":ttl": { N: "0" } });
335
+ }
336
+ const queryResult = yield this.dynamodbClient.send(new client_dynamodb_1.QueryCommand(request));
337
+ if (queryResult.Items === undefined || queryResult.Items.length === 0) {
338
+ return undefined;
339
+ }
340
+ else {
341
+ return queryResult.Items.map((item) => {
342
+ const pkey = item.pkey.S;
343
+ const skey = item.skey.S;
344
+ if (pkey === undefined || skey === undefined) {
345
+ throw new Error("pkey or skey is undefined");
346
+ }
347
+ return { pkey, skey };
348
+ });
349
+ }
350
+ });
351
+ }
352
+ updateTtlOfExcessSnapshots(aggregateId) {
353
+ return __awaiter(this, void 0, void 0, function* () {
354
+ if (this.keepSnapshotCount !== undefined && this.deleteTtl !== undefined) {
355
+ let snapshotCount = yield this.getSnapshotCount(aggregateId);
356
+ if (snapshotCount === undefined) {
357
+ return undefined;
358
+ }
359
+ snapshotCount -= 1;
360
+ const excessCount = snapshotCount - this.keepSnapshotCount;
361
+ if (excessCount > 0) {
362
+ const keys = yield this.getLastSnapshotKeys(aggregateId, excessCount);
363
+ if (keys === undefined) {
364
+ return undefined;
365
+ }
366
+ const ttl = (0, moment_1.default)().add(this.deleteTtl);
367
+ const result = keys.map((key) => {
368
+ const request = {
369
+ TableName: this.snapshotTableName,
370
+ Key: {
371
+ pkey: { S: key.pkey },
372
+ skey: { S: key.skey },
373
+ },
374
+ UpdateExpression: "SET #ttl = :ttl",
375
+ ExpressionAttributeNames: {
376
+ "#ttl": "ttl",
377
+ },
378
+ ExpressionAttributeValues: {
379
+ ":ttl": { N: ttl.seconds().toString() },
380
+ },
381
+ };
382
+ return this.dynamodbClient.send(new client_dynamodb_1.UpdateItemCommand(request));
383
+ });
384
+ return yield Promise.all(result);
385
+ }
386
+ }
387
+ return undefined;
388
+ });
389
+ }
390
+ deleteExcessSnapshots(aggregateId) {
391
+ return __awaiter(this, void 0, void 0, function* () {
392
+ if (this.keepSnapshotCount !== undefined && this.deleteTtl !== undefined) {
393
+ let snapshotCount = yield this.getSnapshotCount(aggregateId);
394
+ if (snapshotCount === undefined) {
395
+ return undefined;
396
+ }
397
+ snapshotCount -= 1;
398
+ const excessCount = snapshotCount - this.keepSnapshotCount;
399
+ if (excessCount > 0) {
400
+ const keys = yield this.getLastSnapshotKeys(aggregateId, excessCount);
401
+ if (keys === undefined) {
402
+ return undefined;
403
+ }
404
+ const result = keys.map((key) => {
405
+ const request = {
406
+ DeleteRequest: {
407
+ Key: {
408
+ pkey: { S: key.pkey },
409
+ skey: { S: key.skey },
410
+ },
411
+ },
412
+ };
413
+ return this.dynamodbClient.send(new client_dynamodb_1.BatchWriteItemCommand({
414
+ RequestItems: {
415
+ [this.snapshotTableName]: [request],
416
+ },
417
+ }));
418
+ });
419
+ return yield Promise.all(result);
420
+ }
421
+ }
422
+ return undefined;
423
+ });
424
+ }
277
425
  }
278
426
  exports.EventStoreForDynamoDB = EventStoreForDynamoDB;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "event-store-adapter-js",
3
- "version": "1.0.9",
3
+ "version": "1.0.10-snapshot.2",
4
4
  "description": "This library is designed to turn DynamoDB into an Event Store for Event Sourcing.",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -9,6 +9,7 @@ import {
9
9
  } from "../types";
10
10
  import { EventStore } from "../event-store";
11
11
  import {
12
+ BatchWriteItemCommand,
12
13
  DynamoDBClient,
13
14
  Put,
14
15
  QueryCommand,
@@ -16,8 +17,11 @@ import {
16
17
  TransactWriteItemsCommand,
17
18
  TransactWriteItemsInput,
18
19
  Update,
20
+ UpdateItemCommand,
21
+ UpdateItemInput,
22
+ WriteRequest,
19
23
  } from "@aws-sdk/client-dynamodb";
20
- import * as moment from "moment/moment";
24
+ import moment from "moment/moment";
21
25
  import { DefaultKeyResolver } from "./default-key-resolver";
22
26
  import {
23
27
  JsonEventSerializer,
@@ -149,6 +153,7 @@ class EventStoreForDynamoDB<
149
153
  throw new Error("Cannot persist created event");
150
154
  }
151
155
  await this.updateEventAndSnapshotOpt(event, version, undefined);
156
+ await this.tryPurgeExcessSnapshots(event);
152
157
  this.logger?.debug(
153
158
  `persistEvent(${JSON.stringify(event)}, ${version}): finished`,
154
159
  );
@@ -163,12 +168,12 @@ class EventStoreForDynamoDB<
163
168
  if (event.isCreated) {
164
169
  await this.createEventAndSnapshot(event, aggregate);
165
170
  } else {
166
- this.logger?.debug("update!!!");
167
171
  await this.updateEventAndSnapshotOpt(
168
172
  event,
169
173
  aggregate.sequenceNumber,
170
174
  aggregate,
171
175
  );
176
+ await this.tryPurgeExcessSnapshots(event);
172
177
  }
173
178
  this.logger?.debug(
174
179
  `persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(
@@ -461,6 +466,154 @@ class EventStoreForDynamoDB<
461
466
  );
462
467
  return result;
463
468
  }
469
+
470
+ private async tryPurgeExcessSnapshots(event: E) {
471
+ if (this.keepSnapshotCount !== undefined) {
472
+ if (this.deleteTtl !== undefined) {
473
+ await this.updateTtlOfExcessSnapshots(event.aggregateId);
474
+ } else {
475
+ await this.deleteExcessSnapshots(event.aggregateId);
476
+ }
477
+ }
478
+ }
479
+
480
+ private async getSnapshotCount(aggregateId: AID) {
481
+ const request: QueryCommandInput = {
482
+ TableName: this.snapshotTableName,
483
+ IndexName: this.snapshotAidIndexName,
484
+ KeyConditionExpression: "#aid = :aid",
485
+ ExpressionAttributeNames: {
486
+ "#aid": "aid",
487
+ },
488
+ ExpressionAttributeValues: {
489
+ ":aid": { S: aggregateId.asString },
490
+ },
491
+ Select: "COUNT",
492
+ };
493
+ const queryResult = await this.dynamodbClient.send(
494
+ new QueryCommand(request),
495
+ );
496
+ return queryResult.Count;
497
+ }
498
+
499
+ private async getLastSnapshotKeys(aggregateId: AID, limit: number) {
500
+ const names = {
501
+ "#aid": "aid",
502
+ "#seq_nr": "seq_nr",
503
+ };
504
+ const values = {
505
+ ":aid": { S: aggregateId.asString },
506
+ ":seq_nr": { N: "0" },
507
+ };
508
+ const request: QueryCommandInput = {
509
+ TableName: this.snapshotTableName,
510
+ IndexName: this.snapshotAidIndexName,
511
+ KeyConditionExpression: "#aid = :aid AND #seq_nr > :seq_nr",
512
+ ExpressionAttributeNames: { ...names },
513
+ ExpressionAttributeValues: { ...values },
514
+ ScanIndexForward: false,
515
+ Limit: limit,
516
+ };
517
+ if (this.deleteTtl !== undefined) {
518
+ request.FilterExpression = "#ttl = :ttl";
519
+ request.ExpressionAttributeNames = {
520
+ ...names,
521
+ "#ttl": "ttl",
522
+ };
523
+ request.ExpressionAttributeValues = {
524
+ ...values,
525
+ ":ttl": { N: "0" },
526
+ };
527
+ }
528
+ const queryResult = await this.dynamodbClient.send(
529
+ new QueryCommand(request),
530
+ );
531
+ if (queryResult.Items === undefined || queryResult.Items.length === 0) {
532
+ return undefined;
533
+ } else {
534
+ return queryResult.Items.map((item) => {
535
+ const pkey = item.pkey.S;
536
+ const skey = item.skey.S;
537
+ if (pkey === undefined || skey === undefined) {
538
+ throw new Error("pkey or skey is undefined");
539
+ }
540
+ return { pkey, skey };
541
+ });
542
+ }
543
+ }
544
+
545
+ private async updateTtlOfExcessSnapshots(aggregateId: AID) {
546
+ if (this.keepSnapshotCount !== undefined && this.deleteTtl !== undefined) {
547
+ let snapshotCount = await this.getSnapshotCount(aggregateId);
548
+ if (snapshotCount === undefined) {
549
+ return undefined;
550
+ }
551
+ snapshotCount -= 1;
552
+ const excessCount = snapshotCount - this.keepSnapshotCount;
553
+ if (excessCount > 0) {
554
+ const keys = await this.getLastSnapshotKeys(aggregateId, excessCount);
555
+ if (keys === undefined) {
556
+ return undefined;
557
+ }
558
+ const ttl = moment().add(this.deleteTtl);
559
+ const result = keys.map((key) => {
560
+ const request: UpdateItemInput = {
561
+ TableName: this.snapshotTableName,
562
+ Key: {
563
+ pkey: { S: key.pkey },
564
+ skey: { S: key.skey },
565
+ },
566
+ UpdateExpression: "SET #ttl = :ttl",
567
+ ExpressionAttributeNames: {
568
+ "#ttl": "ttl",
569
+ },
570
+ ExpressionAttributeValues: {
571
+ ":ttl": { N: ttl.seconds().toString() },
572
+ },
573
+ };
574
+ return this.dynamodbClient.send(new UpdateItemCommand(request));
575
+ });
576
+ return await Promise.all(result);
577
+ }
578
+ }
579
+ return undefined;
580
+ }
581
+
582
+ private async deleteExcessSnapshots(aggregateId: AID) {
583
+ if (this.keepSnapshotCount !== undefined && this.deleteTtl !== undefined) {
584
+ let snapshotCount = await this.getSnapshotCount(aggregateId);
585
+ if (snapshotCount === undefined) {
586
+ return undefined;
587
+ }
588
+ snapshotCount -= 1;
589
+ const excessCount = snapshotCount - this.keepSnapshotCount;
590
+ if (excessCount > 0) {
591
+ const keys = await this.getLastSnapshotKeys(aggregateId, excessCount);
592
+ if (keys === undefined) {
593
+ return undefined;
594
+ }
595
+ const result = keys.map((key) => {
596
+ const request: WriteRequest = {
597
+ DeleteRequest: {
598
+ Key: {
599
+ pkey: { S: key.pkey },
600
+ skey: { S: key.skey },
601
+ },
602
+ },
603
+ };
604
+ return this.dynamodbClient.send(
605
+ new BatchWriteItemCommand({
606
+ RequestItems: {
607
+ [this.snapshotTableName]: [request],
608
+ },
609
+ }),
610
+ );
611
+ });
612
+ return await Promise.all(result);
613
+ }
614
+ }
615
+ return undefined;
616
+ }
464
617
  }
465
618
 
466
619
  export { EventStoreForDynamoDB };