event-store-adapter-js 1.0.2-snapshot.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/.eslintrc.cjs +25 -0
- package/.github/CODEONWERS +1 -0
- package/.github/create-release-note-body.py +40 -0
- package/.github/create-release-note-header.py +28 -0
- package/.github/next-semver.py +20 -0
- package/.github/semver-level.py +31 -0
- package/.github/workflows/bump-version.yml +99 -0
- package/.github/workflows/ci.yml +37 -0
- package/.github/workflows/snapshot.yml +26 -0
- package/.prettierignore +2 -0
- package/.prettierrc.yml +3 -0
- package/.vscode/settings.json +11 -0
- package/README.md +18 -0
- package/docs/DATABASE_SCHEMA.ja.md +49 -0
- package/docs/DATABASE_SCHEMA.md +49 -0
- package/jest.config.ts +5 -0
- package/package.json +52 -0
- package/renovate.json +4 -0
- package/src/event-store-options.ts +28 -0
- package/src/event-store.ts +22 -0
- package/src/index.ts +2 -0
- package/src/internal/default-key-resolver.ts +34 -0
- package/src/internal/default-serializer.ts +50 -0
- package/src/internal/event-store-for-dynamodb.test.ts +142 -0
- package/src/internal/event-store-for-dynamodb.ts +416 -0
- package/src/internal/logger-factory.ts +31 -0
- package/src/internal/test/dynamodb-utils.ts +156 -0
- package/src/internal/test/user-account-event.ts +62 -0
- package/src/internal/test/user-account-id.ts +17 -0
- package/src/internal/test/user-account-repository.test.ts +119 -0
- package/src/internal/test/user-account-repository.ts +44 -0
- package/src/internal/test/user-account.ts +107 -0
- package/src/types.ts +54 -0
- package/tsconfig.json +16 -0
- package/version +1 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Aggregate,
|
|
3
|
+
AggregateId,
|
|
4
|
+
Event,
|
|
5
|
+
EventSerializer,
|
|
6
|
+
SnapshotSerializer,
|
|
7
|
+
} from "../types";
|
|
8
|
+
|
|
9
|
+
class JsonEventSerializer<AID extends AggregateId, E extends Event<AID>>
|
|
10
|
+
implements EventSerializer<AID, E>
|
|
11
|
+
{
|
|
12
|
+
private encoder = new TextEncoder();
|
|
13
|
+
private decoder = new TextDecoder();
|
|
14
|
+
|
|
15
|
+
deserialize(bytes: Uint8Array, converter: (json: string) => E): E {
|
|
16
|
+
const jsonString = this.decoder.decode(bytes);
|
|
17
|
+
return converter(jsonString);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
serialize(event: E): Uint8Array {
|
|
21
|
+
const jsonString = JSON.stringify({
|
|
22
|
+
type: event.constructor.name,
|
|
23
|
+
data: event,
|
|
24
|
+
});
|
|
25
|
+
return this.encoder.encode(jsonString);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
class JsonSnapshotSerializer<
|
|
30
|
+
AID extends AggregateId,
|
|
31
|
+
A extends Aggregate<A, AID>,
|
|
32
|
+
> implements SnapshotSerializer<AID, A>
|
|
33
|
+
{
|
|
34
|
+
private encoder = new TextEncoder();
|
|
35
|
+
private decoder = new TextDecoder();
|
|
36
|
+
deserialize(bytes: Uint8Array, converter: (json: string) => A): A {
|
|
37
|
+
const jsonString = this.decoder.decode(bytes);
|
|
38
|
+
return converter(jsonString);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
serialize(aggregate: A): Uint8Array {
|
|
42
|
+
const jsonString = JSON.stringify({
|
|
43
|
+
type: aggregate.constructor.name,
|
|
44
|
+
data: aggregate,
|
|
45
|
+
});
|
|
46
|
+
return this.encoder.encode(jsonString);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { JsonEventSerializer, JsonSnapshotSerializer };
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
import {
|
|
2
|
+
GenericContainer,
|
|
3
|
+
StartedTestContainer,
|
|
4
|
+
TestContainer,
|
|
5
|
+
Wait,
|
|
6
|
+
} from "testcontainers";
|
|
7
|
+
import { describe } from "node:test";
|
|
8
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
9
|
+
import { EventStoreForDynamoDB } from "./event-store-for-dynamodb";
|
|
10
|
+
import { ulid } from "ulid";
|
|
11
|
+
import { UserAccountId } from "./test/user-account-id";
|
|
12
|
+
import { convertJSONToUserAccount, UserAccount } from "./test/user-account";
|
|
13
|
+
import { UserAccountEvent } from "./test/user-account-event";
|
|
14
|
+
import {
|
|
15
|
+
createDynamoDBClient,
|
|
16
|
+
createJournalTable,
|
|
17
|
+
createSnapshotTable,
|
|
18
|
+
} from "./test/dynamodb-utils";
|
|
19
|
+
|
|
20
|
+
afterEach(() => {
|
|
21
|
+
jest.useRealTimers();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("EventStoreForDynamoDB", () => {
|
|
25
|
+
const TEST_TIME_FACTOR = parseFloat(process.env.TEST_TIME_FACTOR ?? "1.0");
|
|
26
|
+
const TIMEOUT: number = 10 * 1000 * TEST_TIME_FACTOR;
|
|
27
|
+
console.log("TIMEOUT = ", TIMEOUT);
|
|
28
|
+
|
|
29
|
+
let container: TestContainer;
|
|
30
|
+
let startedContainer: StartedTestContainer;
|
|
31
|
+
let eventStore: EventStoreForDynamoDB<
|
|
32
|
+
UserAccountId,
|
|
33
|
+
UserAccount,
|
|
34
|
+
UserAccountEvent
|
|
35
|
+
>;
|
|
36
|
+
|
|
37
|
+
const JOURNAL_TABLE_NAME = "journal";
|
|
38
|
+
const SNAPSHOT_TABLE_NAME = "snapshot";
|
|
39
|
+
const JOURNAL_AID_INDEX_NAME = "journal-aid-index";
|
|
40
|
+
const SNAPSHOTS_AID_INDEX_NAME = "snapshots-aid-index";
|
|
41
|
+
|
|
42
|
+
function createEventStore(
|
|
43
|
+
dynamodbClient: DynamoDBClient,
|
|
44
|
+
): EventStoreForDynamoDB<UserAccountId, UserAccount, UserAccountEvent> {
|
|
45
|
+
return new EventStoreForDynamoDB<
|
|
46
|
+
UserAccountId,
|
|
47
|
+
UserAccount,
|
|
48
|
+
UserAccountEvent
|
|
49
|
+
>(
|
|
50
|
+
dynamodbClient,
|
|
51
|
+
JOURNAL_TABLE_NAME,
|
|
52
|
+
SNAPSHOT_TABLE_NAME,
|
|
53
|
+
JOURNAL_AID_INDEX_NAME,
|
|
54
|
+
SNAPSHOTS_AID_INDEX_NAME,
|
|
55
|
+
32,
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
beforeAll(async () => {
|
|
60
|
+
container = new GenericContainer("localstack/localstack:2.1.0")
|
|
61
|
+
.withEnvironment({
|
|
62
|
+
SERVICES: "dynamodb",
|
|
63
|
+
DEFAULT_REGION: "us-west-1",
|
|
64
|
+
EAGER_SERVICE_LOADING: "1",
|
|
65
|
+
DYNAMODB_SHARED_DB: "1",
|
|
66
|
+
DYNAMODB_IN_MEMORY: "1",
|
|
67
|
+
})
|
|
68
|
+
.withWaitStrategy(Wait.forLogMessage("Ready."))
|
|
69
|
+
.withExposedPorts(4566);
|
|
70
|
+
startedContainer = await container.start();
|
|
71
|
+
const dynamodbClient = createDynamoDBClient(startedContainer);
|
|
72
|
+
await createJournalTable(
|
|
73
|
+
dynamodbClient,
|
|
74
|
+
JOURNAL_TABLE_NAME,
|
|
75
|
+
JOURNAL_AID_INDEX_NAME,
|
|
76
|
+
);
|
|
77
|
+
await createSnapshotTable(
|
|
78
|
+
dynamodbClient,
|
|
79
|
+
SNAPSHOT_TABLE_NAME,
|
|
80
|
+
SNAPSHOTS_AID_INDEX_NAME,
|
|
81
|
+
);
|
|
82
|
+
eventStore = createEventStore(dynamodbClient);
|
|
83
|
+
}, TIMEOUT);
|
|
84
|
+
|
|
85
|
+
afterAll(async () => {
|
|
86
|
+
if (startedContainer !== undefined) {
|
|
87
|
+
await startedContainer.stop();
|
|
88
|
+
}
|
|
89
|
+
}, TIMEOUT);
|
|
90
|
+
|
|
91
|
+
test(
|
|
92
|
+
"persistAndSnapshot",
|
|
93
|
+
async () => {
|
|
94
|
+
const id = new UserAccountId(ulid());
|
|
95
|
+
const name = "Alice";
|
|
96
|
+
const [userAccount1, created] = UserAccount.create(id, name);
|
|
97
|
+
|
|
98
|
+
await eventStore.persistEventAndSnapshot(created, userAccount1);
|
|
99
|
+
|
|
100
|
+
const userAccount2 = await eventStore.getLatestSnapshotById(
|
|
101
|
+
id,
|
|
102
|
+
convertJSONToUserAccount,
|
|
103
|
+
);
|
|
104
|
+
if (userAccount2 === undefined) {
|
|
105
|
+
throw new Error("userAccount2 is undefined");
|
|
106
|
+
}
|
|
107
|
+
expect(userAccount2.id).toEqual(id);
|
|
108
|
+
expect(userAccount2.name).toEqual(name);
|
|
109
|
+
expect(userAccount2.version).toEqual(1);
|
|
110
|
+
},
|
|
111
|
+
TIMEOUT,
|
|
112
|
+
);
|
|
113
|
+
|
|
114
|
+
test(
|
|
115
|
+
"persistAndSnapshot2",
|
|
116
|
+
async () => {
|
|
117
|
+
const id = new UserAccountId(ulid());
|
|
118
|
+
const name = "Alice";
|
|
119
|
+
const [userAccount1, created] = UserAccount.create(id, name);
|
|
120
|
+
|
|
121
|
+
await eventStore.persistEventAndSnapshot(created, userAccount1);
|
|
122
|
+
|
|
123
|
+
const [userAccount2, renamed] = userAccount1.rename("Bob");
|
|
124
|
+
|
|
125
|
+
await eventStore.persistEvent(renamed, userAccount2.version);
|
|
126
|
+
|
|
127
|
+
const userAccount3 = await eventStore.getLatestSnapshotById(
|
|
128
|
+
id,
|
|
129
|
+
convertJSONToUserAccount,
|
|
130
|
+
);
|
|
131
|
+
if (userAccount3 === undefined) {
|
|
132
|
+
throw new Error("userAccount2 is undefined");
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
expect(userAccount3.id).toEqual(id);
|
|
136
|
+
expect(userAccount3.name).toEqual(name);
|
|
137
|
+
expect(userAccount3.sequenceNumber).toEqual(1);
|
|
138
|
+
expect(userAccount3.version).toEqual(2);
|
|
139
|
+
},
|
|
140
|
+
TIMEOUT,
|
|
141
|
+
);
|
|
142
|
+
});
|
|
@@ -0,0 +1,416 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Aggregate,
|
|
3
|
+
AggregateId,
|
|
4
|
+
Event,
|
|
5
|
+
EventSerializer,
|
|
6
|
+
KeyResolver,
|
|
7
|
+
SnapshotSerializer,
|
|
8
|
+
} from "../types";
|
|
9
|
+
import { EventStore } from "../event-store";
|
|
10
|
+
import {
|
|
11
|
+
DynamoDBClient,
|
|
12
|
+
Put,
|
|
13
|
+
QueryCommand,
|
|
14
|
+
QueryCommandInput,
|
|
15
|
+
TransactWriteItemsCommand,
|
|
16
|
+
TransactWriteItemsInput,
|
|
17
|
+
Update,
|
|
18
|
+
} from "@aws-sdk/client-dynamodb";
|
|
19
|
+
import * as moment from "moment/moment";
|
|
20
|
+
import { DefaultKeyResolver } from "./default-key-resolver";
|
|
21
|
+
import {
|
|
22
|
+
JsonEventSerializer,
|
|
23
|
+
JsonSnapshotSerializer,
|
|
24
|
+
} from "./default-serializer";
|
|
25
|
+
import { LoggerFactory } from "./logger-factory";
|
|
26
|
+
import * as winston from "winston";
|
|
27
|
+
|
|
28
|
+
class EventStoreForDynamoDB<
|
|
29
|
+
AID extends AggregateId,
|
|
30
|
+
A extends Aggregate<A, AID>,
|
|
31
|
+
E extends Event<AID>,
|
|
32
|
+
> implements EventStore<AID, A, E>
|
|
33
|
+
{
|
|
34
|
+
static logger: winston.Logger;
|
|
35
|
+
constructor(
|
|
36
|
+
private dynamodbClient: DynamoDBClient,
|
|
37
|
+
private journalTableName: string,
|
|
38
|
+
private snapshotTableName: string,
|
|
39
|
+
private journalAidIndexName: string,
|
|
40
|
+
private snapshotAidIndexName: string,
|
|
41
|
+
private shardCount: number,
|
|
42
|
+
private keepSnapshotCount: number | undefined = undefined,
|
|
43
|
+
private deleteTtl: moment.Duration | undefined = undefined,
|
|
44
|
+
private keyResolver: KeyResolver<AID> = new DefaultKeyResolver(),
|
|
45
|
+
private eventSerializer: EventSerializer<AID, E> = new JsonEventSerializer<
|
|
46
|
+
AID,
|
|
47
|
+
E
|
|
48
|
+
>(),
|
|
49
|
+
private snapshotSerializer: SnapshotSerializer<
|
|
50
|
+
AID,
|
|
51
|
+
A
|
|
52
|
+
> = new JsonSnapshotSerializer<AID, A>(),
|
|
53
|
+
) {
|
|
54
|
+
EventStoreForDynamoDB.logger = LoggerFactory.createLogger(
|
|
55
|
+
process.env.STAGE ?? "dev",
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async getEventsByIdSinceSequenceNumber(
|
|
60
|
+
id: AID,
|
|
61
|
+
sequenceNumber: number,
|
|
62
|
+
converter: (json: string) => E,
|
|
63
|
+
): Promise<E[]> {
|
|
64
|
+
const request: QueryCommandInput = {
|
|
65
|
+
TableName: this.journalTableName,
|
|
66
|
+
IndexName: this.journalAidIndexName,
|
|
67
|
+
KeyConditionExpression: "#aid = :aid AND #seq_nr >= :seq_nr",
|
|
68
|
+
ExpressionAttributeNames: {
|
|
69
|
+
"#aid": "aid",
|
|
70
|
+
"#seq_nr": "seq_nr",
|
|
71
|
+
},
|
|
72
|
+
ExpressionAttributeValues: {
|
|
73
|
+
":aid": { S: id.asString },
|
|
74
|
+
":seq_nr": { N: sequenceNumber.toString() },
|
|
75
|
+
},
|
|
76
|
+
};
|
|
77
|
+
const result = await this.dynamodbClient.send(new QueryCommand(request));
|
|
78
|
+
if (result.Items === undefined) {
|
|
79
|
+
return Promise.resolve([]);
|
|
80
|
+
} else {
|
|
81
|
+
return Promise.resolve(
|
|
82
|
+
result.Items.map((item) => {
|
|
83
|
+
const payload = item.payload.B;
|
|
84
|
+
if (payload === undefined) {
|
|
85
|
+
throw new Error("Payload is undefined");
|
|
86
|
+
}
|
|
87
|
+
return this.eventSerializer.deserialize(payload, converter);
|
|
88
|
+
}),
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
async getLatestSnapshotById(
|
|
94
|
+
id: AID,
|
|
95
|
+
converter: (json: string) => A,
|
|
96
|
+
): Promise<A | undefined> {
|
|
97
|
+
const request: QueryCommandInput = {
|
|
98
|
+
TableName: this.snapshotTableName,
|
|
99
|
+
IndexName: this.snapshotAidIndexName,
|
|
100
|
+
KeyConditionExpression: "#aid = :aid AND #seq_nr = :seq_nr",
|
|
101
|
+
ExpressionAttributeNames: {
|
|
102
|
+
"#aid": "aid",
|
|
103
|
+
"#seq_nr": "seq_nr",
|
|
104
|
+
},
|
|
105
|
+
ExpressionAttributeValues: {
|
|
106
|
+
":aid": { S: id.asString },
|
|
107
|
+
":seq_nr": { N: "0" },
|
|
108
|
+
},
|
|
109
|
+
Limit: 1,
|
|
110
|
+
};
|
|
111
|
+
const queryResult = await this.dynamodbClient.send(
|
|
112
|
+
new QueryCommand(request),
|
|
113
|
+
);
|
|
114
|
+
if (queryResult.Items === undefined || queryResult.Items.length === 0) {
|
|
115
|
+
return undefined;
|
|
116
|
+
} else {
|
|
117
|
+
const item = queryResult.Items[0];
|
|
118
|
+
const version = item.version.N;
|
|
119
|
+
if (version === undefined) {
|
|
120
|
+
throw new Error("Version is undefined");
|
|
121
|
+
}
|
|
122
|
+
const payload = item.payload.B;
|
|
123
|
+
if (payload === undefined) {
|
|
124
|
+
throw new Error("Payload is undefined");
|
|
125
|
+
}
|
|
126
|
+
const result = this.snapshotSerializer.deserialize(payload, converter);
|
|
127
|
+
EventStoreForDynamoDB.logger.info("result: " + JSON.stringify(result));
|
|
128
|
+
return result.withVersion(Number(version));
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async persistEvent(event: E, version: number): Promise<void> {
|
|
133
|
+
EventStoreForDynamoDB.logger.info(
|
|
134
|
+
`persistEvent(${JSON.stringify(event)}, ${version}): start`,
|
|
135
|
+
);
|
|
136
|
+
if (event.isCreated) {
|
|
137
|
+
throw new Error("Cannot persist created event");
|
|
138
|
+
}
|
|
139
|
+
await this.updateEventAndSnapshotOpt(event, version, undefined);
|
|
140
|
+
EventStoreForDynamoDB.logger.info(
|
|
141
|
+
`persistEvent(${JSON.stringify(event)}, ${version}): finished`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async persistEventAndSnapshot(event: E, aggregate: A): Promise<void> {
|
|
146
|
+
EventStoreForDynamoDB.logger.info(
|
|
147
|
+
`persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(
|
|
148
|
+
aggregate,
|
|
149
|
+
)}): start`,
|
|
150
|
+
);
|
|
151
|
+
if (event.isCreated) {
|
|
152
|
+
await this.createEventAndSnapshot(event, aggregate);
|
|
153
|
+
EventStoreForDynamoDB.logger.info(
|
|
154
|
+
`persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(
|
|
155
|
+
aggregate,
|
|
156
|
+
)}): finished`,
|
|
157
|
+
);
|
|
158
|
+
} else {
|
|
159
|
+
EventStoreForDynamoDB.logger.info("update!!!");
|
|
160
|
+
await this.updateEventAndSnapshotOpt(
|
|
161
|
+
event,
|
|
162
|
+
aggregate.sequenceNumber,
|
|
163
|
+
aggregate,
|
|
164
|
+
);
|
|
165
|
+
EventStoreForDynamoDB.logger.info(
|
|
166
|
+
`persistEventAndSnapshot(${JSON.stringify(event)}, ${JSON.stringify(
|
|
167
|
+
aggregate,
|
|
168
|
+
)}): finished`,
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
withDeleteTtl(deleteTtl: moment.Duration): EventStore<AID, A, E> {
|
|
174
|
+
return new EventStoreForDynamoDB(
|
|
175
|
+
this.dynamodbClient,
|
|
176
|
+
this.journalTableName,
|
|
177
|
+
this.snapshotTableName,
|
|
178
|
+
this.journalAidIndexName,
|
|
179
|
+
this.snapshotAidIndexName,
|
|
180
|
+
this.shardCount,
|
|
181
|
+
this.keepSnapshotCount,
|
|
182
|
+
deleteTtl,
|
|
183
|
+
this.keyResolver,
|
|
184
|
+
this.eventSerializer,
|
|
185
|
+
this.snapshotSerializer,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
withEventSerializer(
|
|
190
|
+
eventSerializer: EventSerializer<AID, E>,
|
|
191
|
+
): EventStore<AID, A, E> {
|
|
192
|
+
return new EventStoreForDynamoDB(
|
|
193
|
+
this.dynamodbClient,
|
|
194
|
+
this.journalTableName,
|
|
195
|
+
this.snapshotTableName,
|
|
196
|
+
this.journalAidIndexName,
|
|
197
|
+
this.snapshotAidIndexName,
|
|
198
|
+
this.shardCount,
|
|
199
|
+
this.keepSnapshotCount,
|
|
200
|
+
this.deleteTtl,
|
|
201
|
+
this.keyResolver,
|
|
202
|
+
eventSerializer,
|
|
203
|
+
this.snapshotSerializer,
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
withKeepSnapshotCount(keepSnapshotCount: number): EventStore<AID, A, E> {
|
|
208
|
+
return new EventStoreForDynamoDB(
|
|
209
|
+
this.dynamodbClient,
|
|
210
|
+
this.journalTableName,
|
|
211
|
+
this.snapshotTableName,
|
|
212
|
+
this.journalAidIndexName,
|
|
213
|
+
this.snapshotAidIndexName,
|
|
214
|
+
this.shardCount,
|
|
215
|
+
keepSnapshotCount,
|
|
216
|
+
this.deleteTtl,
|
|
217
|
+
this.keyResolver,
|
|
218
|
+
this.eventSerializer,
|
|
219
|
+
this.snapshotSerializer,
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
withKeyResolver(keyResolver: KeyResolver<AID>): EventStore<AID, A, E> {
|
|
224
|
+
return new EventStoreForDynamoDB(
|
|
225
|
+
this.dynamodbClient,
|
|
226
|
+
this.journalTableName,
|
|
227
|
+
this.snapshotTableName,
|
|
228
|
+
this.journalAidIndexName,
|
|
229
|
+
this.snapshotAidIndexName,
|
|
230
|
+
this.shardCount,
|
|
231
|
+
this.keepSnapshotCount,
|
|
232
|
+
this.deleteTtl,
|
|
233
|
+
keyResolver,
|
|
234
|
+
this.eventSerializer,
|
|
235
|
+
this.snapshotSerializer,
|
|
236
|
+
);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
withSnapshotSerializer(
|
|
240
|
+
snapshotSerializer: SnapshotSerializer<AID, A>,
|
|
241
|
+
): EventStore<AID, A, E> {
|
|
242
|
+
return new EventStoreForDynamoDB(
|
|
243
|
+
this.dynamodbClient,
|
|
244
|
+
this.journalTableName,
|
|
245
|
+
this.snapshotTableName,
|
|
246
|
+
this.journalAidIndexName,
|
|
247
|
+
this.snapshotAidIndexName,
|
|
248
|
+
this.shardCount,
|
|
249
|
+
this.keepSnapshotCount,
|
|
250
|
+
this.deleteTtl,
|
|
251
|
+
this.keyResolver,
|
|
252
|
+
this.eventSerializer,
|
|
253
|
+
snapshotSerializer,
|
|
254
|
+
);
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
private async createEventAndSnapshot(event: E, aggregate: A): Promise<void> {
|
|
258
|
+
const putSnapshot = this.putSnapshot(event, 0, aggregate);
|
|
259
|
+
const putJournal = this.putJournal(event);
|
|
260
|
+
const transactWriteItems = [
|
|
261
|
+
{
|
|
262
|
+
Put: putSnapshot,
|
|
263
|
+
},
|
|
264
|
+
{
|
|
265
|
+
Put: putJournal,
|
|
266
|
+
},
|
|
267
|
+
];
|
|
268
|
+
const input: TransactWriteItemsInput = {
|
|
269
|
+
TransactItems: transactWriteItems,
|
|
270
|
+
};
|
|
271
|
+
await this.dynamodbClient.send(new TransactWriteItemsCommand(input));
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
private async updateEventAndSnapshotOpt(
|
|
275
|
+
event: E,
|
|
276
|
+
version: number,
|
|
277
|
+
aggregate: A | undefined,
|
|
278
|
+
): Promise<void> {
|
|
279
|
+
const update = this.updateSnapshot(event, 0, version, aggregate);
|
|
280
|
+
const put = this.putJournal(event);
|
|
281
|
+
const transactWriteItems = [
|
|
282
|
+
{
|
|
283
|
+
Update: update,
|
|
284
|
+
},
|
|
285
|
+
{
|
|
286
|
+
Put: put,
|
|
287
|
+
},
|
|
288
|
+
];
|
|
289
|
+
const input: TransactWriteItemsInput = {
|
|
290
|
+
TransactItems: transactWriteItems,
|
|
291
|
+
};
|
|
292
|
+
await this.dynamodbClient.send(new TransactWriteItemsCommand(input));
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
private putJournal(event: E): Put {
|
|
296
|
+
const pkey = this.keyResolver.resolvePartitionKey(
|
|
297
|
+
event.aggregateId,
|
|
298
|
+
this.shardCount,
|
|
299
|
+
);
|
|
300
|
+
const skey = this.keyResolver.resolveSortKey(
|
|
301
|
+
event.aggregateId,
|
|
302
|
+
event.sequenceNumber,
|
|
303
|
+
);
|
|
304
|
+
const payload = this.eventSerializer.serialize(event);
|
|
305
|
+
return {
|
|
306
|
+
TableName: this.journalTableName,
|
|
307
|
+
Item: {
|
|
308
|
+
pkey: { S: pkey },
|
|
309
|
+
skey: { S: skey },
|
|
310
|
+
aid: { S: event.aggregateId.asString },
|
|
311
|
+
seq_nr: { N: event.sequenceNumber.toString() },
|
|
312
|
+
payload: { B: payload },
|
|
313
|
+
occurred_at: { N: event.occurredAt.getUTCMilliseconds().toString() },
|
|
314
|
+
},
|
|
315
|
+
ConditionExpression:
|
|
316
|
+
"attribute_not_exists(pkey) AND attribute_not_exists(skey)",
|
|
317
|
+
};
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
private putSnapshot(event: E, sequenceNumber: number, aggregate: A): Put {
|
|
321
|
+
const pkey = this.keyResolver.resolvePartitionKey(
|
|
322
|
+
event.aggregateId,
|
|
323
|
+
this.shardCount,
|
|
324
|
+
);
|
|
325
|
+
const skey = this.keyResolver.resolveSortKey(
|
|
326
|
+
event.aggregateId,
|
|
327
|
+
sequenceNumber,
|
|
328
|
+
);
|
|
329
|
+
const payload = this.snapshotSerializer.serialize(aggregate);
|
|
330
|
+
return {
|
|
331
|
+
TableName: this.snapshotTableName,
|
|
332
|
+
Item: {
|
|
333
|
+
pkey: { S: pkey },
|
|
334
|
+
skey: { S: skey },
|
|
335
|
+
payload: { B: payload },
|
|
336
|
+
aid: { S: event.aggregateId.asString },
|
|
337
|
+
seq_nr: { N: sequenceNumber.toString() },
|
|
338
|
+
version: { N: "1" },
|
|
339
|
+
ttl: { N: "0" },
|
|
340
|
+
last_updated_at: {
|
|
341
|
+
N: event.occurredAt.getUTCMilliseconds().toString(),
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
ConditionExpression:
|
|
345
|
+
"attribute_not_exists(pkey) AND attribute_not_exists(skey)",
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
private updateSnapshot(
|
|
350
|
+
event: E,
|
|
351
|
+
sequenceNumber: number,
|
|
352
|
+
version: number,
|
|
353
|
+
aggregate: A | undefined,
|
|
354
|
+
): Update {
|
|
355
|
+
const pkey = this.keyResolver.resolvePartitionKey(
|
|
356
|
+
event.aggregateId,
|
|
357
|
+
this.shardCount,
|
|
358
|
+
);
|
|
359
|
+
const skey = this.keyResolver.resolveSortKey(
|
|
360
|
+
event.aggregateId,
|
|
361
|
+
sequenceNumber,
|
|
362
|
+
);
|
|
363
|
+
const keys = {
|
|
364
|
+
pkey: { S: pkey },
|
|
365
|
+
skey: { S: skey },
|
|
366
|
+
};
|
|
367
|
+
const names = {
|
|
368
|
+
"#version": "version",
|
|
369
|
+
"#last_updated_at": "last_updated_at",
|
|
370
|
+
};
|
|
371
|
+
const values = {
|
|
372
|
+
":before_version": { N: version.toString() },
|
|
373
|
+
":after_version": { N: (version + 1).toString() },
|
|
374
|
+
":last_updated_at": {
|
|
375
|
+
N: event.occurredAt.getUTCMilliseconds().toString(),
|
|
376
|
+
},
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
if (aggregate === undefined) {
|
|
380
|
+
const update: Update = {
|
|
381
|
+
TableName: this.snapshotTableName,
|
|
382
|
+
UpdateExpression:
|
|
383
|
+
"SET #version=:after_version, #last_updated_at=:last_updated_at",
|
|
384
|
+
Key: { ...keys },
|
|
385
|
+
ExpressionAttributeNames: { ...names },
|
|
386
|
+
ExpressionAttributeValues: { ...values },
|
|
387
|
+
ConditionExpression: "#version=:before_version",
|
|
388
|
+
};
|
|
389
|
+
EventStoreForDynamoDB.logger.info("update1: " + JSON.stringify(update));
|
|
390
|
+
return update;
|
|
391
|
+
} else {
|
|
392
|
+
const payload = this.snapshotSerializer.serialize(aggregate);
|
|
393
|
+
const update: Update = {
|
|
394
|
+
TableName: this.snapshotTableName,
|
|
395
|
+
UpdateExpression:
|
|
396
|
+
"SET #payload=:payload, #seq_nr=:seq_nr, #version=:after_version, #last_updated_at=:last_updated_at",
|
|
397
|
+
Key: { ...keys },
|
|
398
|
+
ExpressionAttributeNames: {
|
|
399
|
+
...names,
|
|
400
|
+
"#seq_nr": "seq_nr",
|
|
401
|
+
"#payload": "payload",
|
|
402
|
+
},
|
|
403
|
+
ExpressionAttributeValues: {
|
|
404
|
+
...values,
|
|
405
|
+
":seq_nr": { N: sequenceNumber.toString() },
|
|
406
|
+
":payload": { B: payload },
|
|
407
|
+
},
|
|
408
|
+
ConditionExpression: "#version=:before_version",
|
|
409
|
+
};
|
|
410
|
+
EventStoreForDynamoDB.logger.info("update2: " + JSON.stringify(update));
|
|
411
|
+
return update;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export { EventStoreForDynamoDB };
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import * as winston from "winston";
|
|
2
|
+
|
|
3
|
+
export class LoggerFactory {
|
|
4
|
+
public static createLogger(stage: string = "dev"): winston.Logger {
|
|
5
|
+
const logger = winston.createLogger({
|
|
6
|
+
level: "info",
|
|
7
|
+
format: winston.format.simple(),
|
|
8
|
+
// format: winston.format.combine(
|
|
9
|
+
// winston.format.timestamp({
|
|
10
|
+
// format: "YYYY-MM-DD HH:mm:ss"
|
|
11
|
+
// }),
|
|
12
|
+
// winston.format.errors({ stack: true }),
|
|
13
|
+
// winston.format.splat(),
|
|
14
|
+
// winston.format.json()
|
|
15
|
+
// ),
|
|
16
|
+
defaultMeta: { service: "winston-lambda" },
|
|
17
|
+
transports: new winston.transports.Console(),
|
|
18
|
+
});
|
|
19
|
+
// 検証環境の場合、loggingをdebugレベルまで上げる
|
|
20
|
+
if (stage !== "prd") {
|
|
21
|
+
// clear()をする事によって、createLoggerの際に指定したtransportsの設定を消せる
|
|
22
|
+
logger.clear();
|
|
23
|
+
logger.add(
|
|
24
|
+
new winston.transports.Console({
|
|
25
|
+
level: "debug",
|
|
26
|
+
}),
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
return logger;
|
|
30
|
+
}
|
|
31
|
+
}
|