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
package/.eslintrc.cjs
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
module.exports = {
|
|
2
|
+
rules: {
|
|
3
|
+
},
|
|
4
|
+
reportUnusedDisableDirectives: true,
|
|
5
|
+
ignorePatterns: ["dist/*"],
|
|
6
|
+
env: { browser: true, es2020: true, node: true },
|
|
7
|
+
parserOptions: { ecmaVersion: "latest", sourceType: "module" },
|
|
8
|
+
settings: { react: { version: "detect" } },
|
|
9
|
+
plugins: [],
|
|
10
|
+
extends: [
|
|
11
|
+
"eslint:recommended",
|
|
12
|
+
],
|
|
13
|
+
overrides: [
|
|
14
|
+
{
|
|
15
|
+
files: ["*.ts", "*.tsx"],
|
|
16
|
+
parser: "@typescript-eslint/parser",
|
|
17
|
+
plugins: ["@typescript-eslint"],
|
|
18
|
+
extends: ["plugin:@typescript-eslint/recommended"],
|
|
19
|
+
rules: {
|
|
20
|
+
// TypeScriptに関するカスタムルールをここに追加できます
|
|
21
|
+
"@typescript-eslint/no-unused-vars": ["error", { "argsIgnorePattern": "^_" }]
|
|
22
|
+
},
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
* @j5ik2o
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
import csv
|
|
6
|
+
import re
|
|
7
|
+
|
|
8
|
+
commit_messages = {'BREAKING CHANGE': [], 'build': [], 'ci': [], 'feat': [], 'fix': [], 'docs': [], 'style': [], 'refactor': [], 'perf': [], 'test': [], 'revert': [], 'chore': []}
|
|
9
|
+
|
|
10
|
+
titles = {'BREAKING CHANGE': 'Breaking Changes',
|
|
11
|
+
'build': 'Build Systems',
|
|
12
|
+
'ci': 'Continuous Integration',
|
|
13
|
+
'feat': 'Features',
|
|
14
|
+
'fix': 'Bug Fixes',
|
|
15
|
+
'docs': 'Documentation',
|
|
16
|
+
'style': 'Styles',
|
|
17
|
+
'refactor': 'Code Refactoring',
|
|
18
|
+
'perf': 'Performance Improvements',
|
|
19
|
+
'test': 'Tests',
|
|
20
|
+
'revert': 'Reverts',
|
|
21
|
+
'chore': 'Chores'}
|
|
22
|
+
|
|
23
|
+
cin = csv.reader(sys.stdin, delimiter="\t") # 補足: 開く対象がファイルのときは newline='' をパラメータに追加
|
|
24
|
+
|
|
25
|
+
def match_append(key, row):
|
|
26
|
+
r = re.match(f"^{key}(.*)?\: (.*)", row[2])
|
|
27
|
+
if r:
|
|
28
|
+
commit_messages[key].append((r.group(2), row))
|
|
29
|
+
|
|
30
|
+
for row in cin:
|
|
31
|
+
for key in commit_messages.keys():
|
|
32
|
+
match_append(key, row)
|
|
33
|
+
|
|
34
|
+
for k,v in titles.items():
|
|
35
|
+
if commit_messages[k]:
|
|
36
|
+
print(f"### {v}\n")
|
|
37
|
+
for messages in commit_messages[k]:
|
|
38
|
+
print(f"* {messages[0]} ({messages[1][0]})")
|
|
39
|
+
if commit_messages[k]:
|
|
40
|
+
print()
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
|
|
4
|
+
import sys
|
|
5
|
+
from datetime import datetime, timezone
|
|
6
|
+
|
|
7
|
+
args = sys.argv
|
|
8
|
+
|
|
9
|
+
server_url = args[1]
|
|
10
|
+
repo_name = args[2]
|
|
11
|
+
previous_tag = args[3]
|
|
12
|
+
next_tag = args[4]
|
|
13
|
+
|
|
14
|
+
repo_url = f"{server_url}/{repo_name}"
|
|
15
|
+
compare_url = f"{repo_url}/compare/{previous_tag}...{next_tag}"
|
|
16
|
+
|
|
17
|
+
next_version = next_tag.replace("v", "")
|
|
18
|
+
|
|
19
|
+
utc_now = datetime.now(timezone.utc)
|
|
20
|
+
today = utc_now.strftime("%Y-%m-%d")
|
|
21
|
+
|
|
22
|
+
header=f"""
|
|
23
|
+
### [{next_version}]({compare_url}) ({today})
|
|
24
|
+
|
|
25
|
+
"""
|
|
26
|
+
|
|
27
|
+
print(header)
|
|
28
|
+
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import sys
|
|
4
|
+
import semver
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
args = sys.argv
|
|
8
|
+
|
|
9
|
+
for s in sys.stdin:
|
|
10
|
+
r = re.match('.*v?(\d+\.\d+\.\d+)', s)
|
|
11
|
+
if r:
|
|
12
|
+
cur_ver = semver.VersionInfo.parse(r.group(1))
|
|
13
|
+
next_ver = ""
|
|
14
|
+
if args[1] == "major":
|
|
15
|
+
next_ver = str(cur_ver.bump_major())
|
|
16
|
+
elif args[1] == "minor":
|
|
17
|
+
next_ver = str(cur_ver.bump_minor())
|
|
18
|
+
else:
|
|
19
|
+
next_ver = str(cur_ver.bump_patch())
|
|
20
|
+
print(next_ver)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
#! /usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
import sys
|
|
4
|
+
import csv
|
|
5
|
+
import re
|
|
6
|
+
|
|
7
|
+
commit_messages = {'BREAKING CHANGE': 0, 'build': 0, 'ci': 0, 'feat': 0, 'fix': 0, 'docs': 0, 'style': 0, 'refactor': 0, 'perf': 0, 'test': 0, 'revert': 0, 'chore': 0}
|
|
8
|
+
|
|
9
|
+
rules = {'major': ['perf', 'BREAKING CHANGE'], 'minor': ['feat', 'revert'], 'patch': ['build', 'ci', 'fix', 'docs', 'style', 'refactor', 'chore', 'test']}
|
|
10
|
+
|
|
11
|
+
cin = csv.reader(sys.stdin, delimiter="\t")
|
|
12
|
+
|
|
13
|
+
def match_append(key, row):
|
|
14
|
+
r = re.match(f"^{key}(.*)?\: (.*)", row[2])
|
|
15
|
+
if r:
|
|
16
|
+
commit_messages[key]+=1
|
|
17
|
+
|
|
18
|
+
for row in cin:
|
|
19
|
+
for key in commit_messages.keys():
|
|
20
|
+
match_append(key, row)
|
|
21
|
+
|
|
22
|
+
if sum(commit_messages.values()) > 0:
|
|
23
|
+
for k,v in rules.items():
|
|
24
|
+
sum = 0
|
|
25
|
+
for t in v:
|
|
26
|
+
sum += commit_messages[t]
|
|
27
|
+
if sum > 0:
|
|
28
|
+
print(k)
|
|
29
|
+
break
|
|
30
|
+
else:
|
|
31
|
+
sys.exit(-1)
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
name: Bump-Version
|
|
2
|
+
on:
|
|
3
|
+
workflow_dispatch:
|
|
4
|
+
schedule:
|
|
5
|
+
- cron: "0 0 * * *"
|
|
6
|
+
jobs:
|
|
7
|
+
bump-version:
|
|
8
|
+
runs-on: ubuntu-latest
|
|
9
|
+
steps:
|
|
10
|
+
- uses: actions/checkout@v4
|
|
11
|
+
with:
|
|
12
|
+
fetch-depth: 0
|
|
13
|
+
persist-credentials: true
|
|
14
|
+
token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
|
15
|
+
- uses: actions/setup-node@v3
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20.x
|
|
18
|
+
cache: 'npm'
|
|
19
|
+
- uses: actions/setup-python@v4
|
|
20
|
+
with:
|
|
21
|
+
python-version: "3.10"
|
|
22
|
+
- run: pip install semver
|
|
23
|
+
- id: defines
|
|
24
|
+
run: |
|
|
25
|
+
LATEST_TAG=$(git describe --abbrev=0 --tags)
|
|
26
|
+
echo "LATEST_TAG=$LATEST_TAG"
|
|
27
|
+
echo "prev_tag=$LATEST_TAG" >> $GITHUB_OUTPUT
|
|
28
|
+
- name: Calculate changes from the latest tag to HEAD
|
|
29
|
+
id: changes
|
|
30
|
+
run: |
|
|
31
|
+
COUNT=$(git log ${{ steps.defines.outputs.prev_tag }}..HEAD --pretty=format:"%s" \
|
|
32
|
+
--no-merges -P --grep='^(BREAKING CHANGE|build|ci|feat|fix|docs|style|refactor|perf|test|revert|chore)(\(.*\))?:' | awk 'END{print NR}')
|
|
33
|
+
echo "COUNT=$COUNT"
|
|
34
|
+
echo "count=$COUNT" >> $GITHUB_OUTPUT
|
|
35
|
+
- name: Calculate semver level
|
|
36
|
+
id: semver_level
|
|
37
|
+
run: |
|
|
38
|
+
SEMVER_LEVEL=$(git log ${{ steps.defines.outputs.prev_tag }}..HEAD --pretty=format:"%h%x09%H%x09%s" \
|
|
39
|
+
--no-merges -P --grep='^(BREAKING CHANGE|build|ci|feat|fix|docs|style|refactor|perf|test|revert|chore)(\(.*\))?:' \
|
|
40
|
+
| python3 "${GITHUB_WORKSPACE}"/.github/semver-level.py)
|
|
41
|
+
echo "SEMVER_LEVEL=$SEMVER_LEVEL"
|
|
42
|
+
echo "semver_level=$SEMVER_LEVEL" >> $GITHUB_OUTPUT
|
|
43
|
+
if: steps.changes.outputs.count > 0
|
|
44
|
+
- name: Get the next version
|
|
45
|
+
id: versions
|
|
46
|
+
run: |
|
|
47
|
+
NEXT_VERSION=$(echo ${{ steps.defines.outputs.prev_tag }} | python3 "${GITHUB_WORKSPACE}"/.github/next-semver.py ${{ steps.semver_level.outputs.semver_level }})
|
|
48
|
+
echo "NEXT_VERSION=$NEXT_VERSION"
|
|
49
|
+
echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT
|
|
50
|
+
echo "next_tag=v$NEXT_VERSION" >> $GITHUB_OUTPUT
|
|
51
|
+
if: steps.changes.outputs.count > 0
|
|
52
|
+
- name: set the next version
|
|
53
|
+
run: |
|
|
54
|
+
- name: git commit & push
|
|
55
|
+
id: git_commit_push
|
|
56
|
+
run: |
|
|
57
|
+
git config --global user.email "j5ik2o@gmail.com"
|
|
58
|
+
git config --global user.name "Junichi Kato"
|
|
59
|
+
npm version ${{ steps.versions.outputs.next_version }} --no-git-tag-version
|
|
60
|
+
git add .
|
|
61
|
+
git commit -m "version up to ${{ steps.versions.outputs.next_tag }}"
|
|
62
|
+
git push origin main
|
|
63
|
+
COMMIT_SHA=$(git rev-parse HEAD)
|
|
64
|
+
echo "commit_sha=$COMMIT_SHA" >> $GITHUB_OUTPUT
|
|
65
|
+
if: steps.changes.outputs.count > 0
|
|
66
|
+
- name: tagging and push tag
|
|
67
|
+
id: tag_version
|
|
68
|
+
run: |
|
|
69
|
+
git tag -a "${{ steps.versions.outputs.next_tag }}" ${{ steps.git_commit_push.outputs.commit_sha }} -m "${{ steps.versions.outputs.next_tag }}"
|
|
70
|
+
git push origin ${{ steps.versions.outputs.next_tag }}
|
|
71
|
+
python3 "${GITHUB_WORKSPACE}"/.github/create-release-note-header.py \
|
|
72
|
+
${{ github.server_url }} \
|
|
73
|
+
${{ github.repository }} \
|
|
74
|
+
${{ steps.defines.outputs.prev_tag }} \
|
|
75
|
+
${{ steps.versions.outputs.next_tag }} \
|
|
76
|
+
> changelog.txt
|
|
77
|
+
git log ${{ steps.defines.outputs.prev_tag }}..${{ steps.versions.outputs.next_tag }} --pretty=format:"%h%x09%H%x09%s" \
|
|
78
|
+
-P --grep='^(BREAKING CHANGE|build|ci|feat|fix|docs|style|refactor|perf|test|revert|chore)(.*)?:.*$' \
|
|
79
|
+
--no-merges --full-history | \
|
|
80
|
+
python3 "${GITHUB_WORKSPACE}"/.github/create-release-note-body.py >> changelog.txt
|
|
81
|
+
if: steps.changes.outputs.count > 0
|
|
82
|
+
- name: Create a GitHub release
|
|
83
|
+
uses: actions/create-release@v1
|
|
84
|
+
env:
|
|
85
|
+
GITHUB_TOKEN: ${{ secrets.PERSONAL_ACCESS_TOKEN }}
|
|
86
|
+
with:
|
|
87
|
+
tag_name: ${{ steps.versions.outputs.next_tag }}
|
|
88
|
+
release_name: Release ${{ steps.versions.outputs.next_tag }}
|
|
89
|
+
body_path: changelog.txt
|
|
90
|
+
if: steps.changes.outputs.count > 0
|
|
91
|
+
- name: Switch to Snapshot version
|
|
92
|
+
run: |
|
|
93
|
+
rm changelog.txt
|
|
94
|
+
npm version prerelease --preid=snapshot --no-git-tag-version
|
|
95
|
+
SNAPSHOT_VERSION=$(npm version --json | jq -r '.["event-store-adapter-js"]')
|
|
96
|
+
git add .
|
|
97
|
+
git commit -m "version up to ${SNAPSHOT_VERSION}"
|
|
98
|
+
git push origin main
|
|
99
|
+
if: steps.changes.outputs.count > 0
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
on:
|
|
3
|
+
push:
|
|
4
|
+
branches: [ "main" ]
|
|
5
|
+
pull_request:
|
|
6
|
+
branches: [ "main" ]
|
|
7
|
+
schedule:
|
|
8
|
+
- cron: '0 * * * *'
|
|
9
|
+
jobs:
|
|
10
|
+
lint:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
name: "Run lint"
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v3
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20.x
|
|
18
|
+
cache: 'npm'
|
|
19
|
+
- run: npm install
|
|
20
|
+
- run: npm run lint
|
|
21
|
+
test:
|
|
22
|
+
runs-on: ubuntu-latest
|
|
23
|
+
needs: lint
|
|
24
|
+
env:
|
|
25
|
+
TEST_TIME_FACTOR: 10.0
|
|
26
|
+
strategy:
|
|
27
|
+
matrix:
|
|
28
|
+
node-version: [20.x]
|
|
29
|
+
steps:
|
|
30
|
+
- uses: actions/checkout@v4
|
|
31
|
+
- name: Use Node.js ${{ matrix.node-version }}
|
|
32
|
+
uses: actions/setup-node@v3
|
|
33
|
+
with:
|
|
34
|
+
node-version: ${{ matrix.node-version }}
|
|
35
|
+
cache: 'npm'
|
|
36
|
+
- run: npm install
|
|
37
|
+
- run: npm run test
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
name: Snapshot
|
|
2
|
+
on:
|
|
3
|
+
workflow_run:
|
|
4
|
+
workflows:
|
|
5
|
+
- CI
|
|
6
|
+
types:
|
|
7
|
+
- completed
|
|
8
|
+
jobs:
|
|
9
|
+
snapshot:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
|
12
|
+
name: "Publish Snapshot"
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
with:
|
|
16
|
+
ref: ${{ github.event.workflow_run.head_branch }}
|
|
17
|
+
fetch-depth: 0
|
|
18
|
+
- uses: actions/setup-node@v3
|
|
19
|
+
with:
|
|
20
|
+
node-version: 20.x
|
|
21
|
+
registry-url: 'https://registry.npmjs.org'
|
|
22
|
+
cache: 'npm'
|
|
23
|
+
- run: npm install
|
|
24
|
+
- run: npm publish --access public
|
|
25
|
+
env:
|
|
26
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
package/.prettierignore
ADDED
package/.prettierrc.yml
ADDED
package/README.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# event-store-adapter-js
|
|
2
|
+
|
|
3
|
+
This library is designed to turn DynamoDB into an Event Store for Event Sourcing.
|
|
4
|
+
|
|
5
|
+
Status: WIP
|
|
6
|
+
|
|
7
|
+
## License.
|
|
8
|
+
|
|
9
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
10
|
+
|
|
11
|
+
## Other language implementations
|
|
12
|
+
|
|
13
|
+
- [for Java](https://github.com/j5ik2o/event-store-adapter-java)
|
|
14
|
+
- [for Scala](https://github.com/j5ik2o/event-store-adapter-scala)
|
|
15
|
+
- [for Kotlin](https://github.com/j5ik2o/event-store-adapter-kotlin)
|
|
16
|
+
- [for Rust](https://github.com/j5ik2o/event-store-adapter-rs)
|
|
17
|
+
- [for Go](https://github.com/j5ik2o/event-store-adapter-go)
|
|
18
|
+
- [for JavaScript/TypeScript](https://github.com/j5ik2o/event-store-adapter-js)
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## EventStoreが利用するDynamoDBのテーブル構成
|
|
2
|
+
|
|
3
|
+
- Journal
|
|
4
|
+
- Snapshot
|
|
5
|
+
|
|
6
|
+
いずれのテーブルも、キー設計の前提としては、論理シャード内で最大限に書き込みが分散させることを想定しています。
|
|
7
|
+
|
|
8
|
+
### Journalテーブル
|
|
9
|
+
|
|
10
|
+
集約で起きたイベントを保存するためのテーブル。原則的に、このイベントを使って集約状態を再生(リプレイ)します。
|
|
11
|
+
|
|
12
|
+
| キー名 | 説明 | 具体的な値 | 備考 |
|
|
13
|
+
|:------------|:--------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---|
|
|
14
|
+
| pkey | パーションキー(集約種別名-hash(集約ID) % 論理シャードサイズ) | user-account-1 | |
|
|
15
|
+
| skey | ソートキー(集約種別名-集約IDの値部分-シーケンス番号) | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z-12345 | |
|
|
16
|
+
| aid | 集約ID | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z | |
|
|
17
|
+
| ser_nr | シーケンス番号(開始番号は1) | 12345 | |
|
|
18
|
+
| payload | イベント内容 | {"type":"Created","id":"01H42KBHCW1BZG504J4ZXKA2F2","aggregate_id":{"value":"01890535-c59c-72d5-08a8-dcea316374c8"},"seq_nr":1,"name":"test","members":{"members_ids_by_user_account_id":{"01H42KBHCWBDTZYQ7P78T8BTWX":"01H42KBHCWA8NE32M49YH544H1"},"members":{"01H42KBHCWA8NE32M49YH544H1":{"id":"01H42KBHCWA8NE32M49YH544H1","user_account_id":{"value":"01890535-c59c-5b75-ff5c-f63a3485eb9d"},"role":"Admin"}}},"occurred_at":"2023-06-29T03:32:37.404481Z"} | |
|
|
19
|
+
| occurred_at | 発生日時 | 2023-06-29T03:32:37.404481Z | |
|
|
20
|
+
|
|
21
|
+
aidとseq_nrはGSIが適用されており、リプレイ時はこのインデックスを利用されます。
|
|
22
|
+
|
|
23
|
+
### Snapshotテーブル
|
|
24
|
+
|
|
25
|
+
集約の状態を保存するためのテーブルであり、集約のリプレイを高速化するためのテーブルです。スナップショット保存後にもイベントは保存されるため、最新の集約状態を表さない場合あります。
|
|
26
|
+
|
|
27
|
+
| キー名 | 説明 | 具体的な値 | 備考 |
|
|
28
|
+
|:--------|:-----------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:---|
|
|
29
|
+
| pkey | パーションキー(集約種別名-hash(集約ID) % 論理シャードサイズ) | user-account-1 | |
|
|
30
|
+
| skey | ソートキー(集約種別名-集約IDの値部分-シーケンス番号), 最新のスナップショットはシーケンス番号=0として読み書きされます。 | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z-12345 | |
|
|
31
|
+
| payload | 集約の状態 | {"id":{"value":"0189053a-d0b4-8f9b-4fb6-db91f72ccf16"},"name":"test","members":{"members_ids_by_user_account_id":{"01H42KNM5MBVRBZTADAZ9ETSPZ":"01H42KNM5M2QEW700VCW4J2KYE"},"members":{"01H42KNM5M2QEW700VCW4J2KYE":{"id":"01H42KNM5M2QEW700VCW4J2KYE","user_account_id":{"value":"0189053a-d0b4-5ef0-bfe9-4d57d2ed66df"},"role":"Admin"}}},"messages":[],"seq_nr_counter":1,"version":1} | |
|
|
32
|
+
| aid | 集約ID | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z | |
|
|
33
|
+
| ser_nr | シーケンス番号(開始番号は1) | 12345 | |
|
|
34
|
+
| ttl | 削除のためのTTL(秒) | 1624980000 | |
|
|
35
|
+
| version | バージョン(楽観的ロック用)(開始番号は1) | 1 | |
|
|
36
|
+
|
|
37
|
+
- スナップショット冗長化機能が無効な場合はskey=0にスナップショットが保存されるだけですが、有効にした場合はスナップショットはskey=0以外にskey=aggregate.seq_nr()の2件保存されます。スナップショットを保存するたびにskey=aggregate.seq_nr()のスナップショットが増えますが、あなたはスナップショットは上限を指定できます(デフォルトは1)。上限を超えた場合は古いスナップショットから削除されます。デフォルトでは、クライアント主導で削除されます。TTLを使ってDynamoDB自身に削除させることもできます。
|
|
38
|
+
- aidとseq_nrはGSIが適用されており、リプレイ時はこのインデックスを利用されます。
|
|
39
|
+
|
|
40
|
+
### イベント及びスナップショットの書き込み
|
|
41
|
+
|
|
42
|
+
1. 集約にてコマンドが受理されると、最新のseq_nrが付与されたイベントが生成されます。
|
|
43
|
+
2. 生成されたイベントはjournalテーブルに書き込まれます。ただし、この書き込みは必ずsnapshotテーブルと同じトランザクションで行われ、versionが一致する条件下で実施されます。初回のイベント以外は、snapshotのpayloadを更新することはオプションです。
|
|
44
|
+
|
|
45
|
+
### イベント及びスナップショットを使って集約をリプレイする
|
|
46
|
+
|
|
47
|
+
1. 集約のIDを指定して、スナップショットを取得します。
|
|
48
|
+
2. 取得した集約のIDとスナップショットのシーケンス番号以降のイベントをjournalテーブルから読み込みます。
|
|
49
|
+
3. 読み込んだイベントをスナップショットに適用することで、最新の集約状態を取得します。
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
## DynamoDB table schema used by EventStore
|
|
2
|
+
|
|
3
|
+
- Journal
|
|
4
|
+
- Snapshot
|
|
5
|
+
|
|
6
|
+
The key design assumption for both tables is that writes are distributed to the greatest extent possible within the logical shard.
|
|
7
|
+
|
|
8
|
+
### Journal table
|
|
9
|
+
|
|
10
|
+
The table used to store events that have occurred in an aggregate. In principle, this event is used to replay (replay) the aggregate state.
|
|
11
|
+
|
|
12
|
+
| key name | description | example | remarks |
|
|
13
|
+
|:------------|:---------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|
|
|
14
|
+
| pkey | Partition key(${aggregate-type-name}-hash($aid) % logical-shard-size) | user-account-1 | |
|
|
15
|
+
| skey | Sort Key(${aggregate type name}-${aid.value}-${seq_nr}) | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z-12345 | |
|
|
16
|
+
| aid | Aggregate ID | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z | |
|
|
17
|
+
| ser_nr | Sequence Number(origin=1) | 12345 | |
|
|
18
|
+
| payload | Event Payload | {"type":"Created","id":"01H42KBHCW1BZG504J4ZXKA2F2","aggregate_id":{"value":"01890535-c59c-72d5-08a8-dcea316374c8"},"seq_nr":1,"name":"test","members":{"members_ids_by_user_account_id":{"01H42KBHCWBDTZYQ7P78T8BTWX":"01H42KBHCWA8NE32M49YH544H1"},"members":{"01H42KBHCWA8NE32M49YH544H1":{"id":"01H42KBHCWA8NE32M49YH544H1","user_account_id":{"value":"01890535-c59c-5b75-ff5c-f63a3485eb9d"},"role":"Admin"}}},"occurred_at":"2023-06-29T03:32:37.404481Z"} | |
|
|
19
|
+
| occurred_at | Occurred DateTime of the Event | 2023-06-29T03:32:37.404481Z | |
|
|
20
|
+
|
|
21
|
+
GSI is applied to aid and seq_nr, and this index is used during replay.
|
|
22
|
+
|
|
23
|
+
### Snapshot table
|
|
24
|
+
|
|
25
|
+
This table is used to store aggregate state and to speed up replay of aggregates. It may not represent the latest aggregation state because events are saved even after the snapshot is saved.
|
|
26
|
+
|
|
27
|
+
| column name | description | example | remarks |
|
|
28
|
+
|:------------|:----------------------------------------------------------------------------------------------------------|:-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------|
|
|
29
|
+
| pkey | Partition key(${aggregate-type-name}hash($aid) % logical-shard-size) | user-account-1 | |
|
|
30
|
+
| skey | Sort Key(${aggregate type name}-${aid.value}-${seq_nr}), The latest snapshot is read/written as seq_nr=0. | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z-12345 | |
|
|
31
|
+
| payload | State of Aggregate | {"id":{"value":"0189053a-d0b4-8f9b-4fb6-db91f72ccf16"},"name":"test","members":{"members_ids_by_user_account_id":{"01H42KNM5MBVRBZTADAZ9ETSPZ":"01H42KNM5M2QEW700VCW4J2KYE"},"members":{"01H42KNM5M2QEW700VCW4J2KYE":{"id":"01H42KNM5M2QEW700VCW4J2KYE","user_account_id":{"value":"0189053a-d0b4-5ef0-bfe9-4d57d2ed66df"},"role":"Admin"}}},"messages":[],"seq_nr_counter":1,"version":1} | |
|
|
32
|
+
| aid | Aggregate ID | user-account-01H42K4ABWQ5V2XQEP3A48VE0Z | |
|
|
33
|
+
| ser_nr | Sequence Number(origin=1) | 12345 | |
|
|
34
|
+
| ttl | TTL for deletion(seconds) | 1624980000 | |
|
|
35
|
+
| version | Version for optimistic lock(origin=1) | 1 | |
|
|
36
|
+
|
|
37
|
+
- When the snapshot redundancy feature is disabled, only a snapshot is stored at skey=0. When enabled, two snapshots are stored at skey=aggregate.seq_nr() in addition to skey=0. Each time a snapshot is saved, skey=aggregate.seq_nr() snapshot will be increased, but you can specify an upper limit for the snapshot (default is 1). If the upper limit is exceeded, the older snapshots will be deleted first. By default, the deletion is client-initiated; you can also use TTL to let DynamoDB itself do the deletion.
|
|
38
|
+
- GSI is applied to aid and seq_nr, and this index is used during replay.
|
|
39
|
+
|
|
40
|
+
### Writing events and snapshots
|
|
41
|
+
|
|
42
|
+
1. When the command is accepted by aggregate, an event with the latest seq_nr is generated.
|
|
43
|
+
2. The generated events are written to the journal table. However, this write is always done in the same transaction as the snapshot table and under version matching conditions. Except for the first event, updating the payload of snapshot is optional.
|
|
44
|
+
|
|
45
|
+
### Replaying an aggregate with events and snapshots
|
|
46
|
+
|
|
47
|
+
1. Specify the ID of the aggregate and take a snapshot.
|
|
48
|
+
2. Read the events from the journal table after the ID of the retrieved aggregate and the sequence number of the snapshot.
|
|
49
|
+
3. Apply the read events to the snapshot to obtain the latest aggregate state.
|
package/jest.config.ts
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "event-store-adapter-js",
|
|
3
|
+
"version": "1.0.2-snapshot.0",
|
|
4
|
+
"description": "This library is designed to turn DynamoDB into an Event Store for Event Sourcing.",
|
|
5
|
+
"main": "dist/index.js",
|
|
6
|
+
"types": "dist/index.d.ts",
|
|
7
|
+
"scripts": {
|
|
8
|
+
"build": "tsc",
|
|
9
|
+
"test": "jest --no-cache",
|
|
10
|
+
"fix": "npm run fix:prettier && npm run fix:eslint",
|
|
11
|
+
"fix:eslint": "eslint --fix .",
|
|
12
|
+
"fix:prettier": "prettier --write \"**/*.{js,ts,jsx,tsx,json}\"",
|
|
13
|
+
"lint": "npm run lint:prettier && npm run lint:eslint",
|
|
14
|
+
"lint:eslint": "eslint .",
|
|
15
|
+
"lint:prettier": "prettier --check \"**/*.{js,ts,jsx,tsx,json}\"",
|
|
16
|
+
"clean": "rimraf ./dist"
|
|
17
|
+
},
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/j5ik2o/event-store-adapter-js.git"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"ddd",
|
|
24
|
+
"cqrs",
|
|
25
|
+
"evnet-sourcing",
|
|
26
|
+
"dynamodb"
|
|
27
|
+
],
|
|
28
|
+
"author": "Junichi Kato <j5ik2o@gmail.com>",
|
|
29
|
+
"license": "MIT",
|
|
30
|
+
"devDependencies": {
|
|
31
|
+
"@types/jest": "^29.5.5",
|
|
32
|
+
"@types/node": "^20.6.0",
|
|
33
|
+
"@types/winston": "^2.4.4",
|
|
34
|
+
"@typescript-eslint/eslint-plugin": "^6.3.0",
|
|
35
|
+
"@typescript-eslint/parser": "^6.3.0",
|
|
36
|
+
"eslint": "^8.47.0",
|
|
37
|
+
"jest": "^29.7.0",
|
|
38
|
+
"prettier": "3.0.3",
|
|
39
|
+
"rimraf": "^5.0.1",
|
|
40
|
+
"testcontainers": "^10.2.1",
|
|
41
|
+
"ts-jest": "^29.1.1",
|
|
42
|
+
"ts-node": "^10.9.1",
|
|
43
|
+
"typescript": "^5.2.2"
|
|
44
|
+
},
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@aws-sdk/client-dynamodb": "^3.413.0",
|
|
47
|
+
"aws-sdk": "^2.1459.0",
|
|
48
|
+
"moment": "^2.29.4",
|
|
49
|
+
"ulid": "^2.3.0",
|
|
50
|
+
"winston": "^3.10.0"
|
|
51
|
+
}
|
|
52
|
+
}
|
package/renovate.json
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Aggregate,
|
|
3
|
+
AggregateId,
|
|
4
|
+
Event,
|
|
5
|
+
EventSerializer,
|
|
6
|
+
KeyResolver,
|
|
7
|
+
SnapshotSerializer,
|
|
8
|
+
} from "./types";
|
|
9
|
+
import * as moment from "moment";
|
|
10
|
+
|
|
11
|
+
interface EventStoreOptions<
|
|
12
|
+
This extends EventStoreOptions<This, AID, A, E>,
|
|
13
|
+
AID extends AggregateId,
|
|
14
|
+
A extends Aggregate<A, AID>,
|
|
15
|
+
E extends Event<AID>,
|
|
16
|
+
> {
|
|
17
|
+
withKeepSnapshotCount(keepSnapshotCount: number): This;
|
|
18
|
+
|
|
19
|
+
withDeleteTtl(deleteTtl: moment.Duration): This;
|
|
20
|
+
|
|
21
|
+
withKeyResolver(keyResolver: KeyResolver<AID>): This;
|
|
22
|
+
|
|
23
|
+
withEventSerializer(eventSerializer: EventSerializer<AID, E>): This;
|
|
24
|
+
|
|
25
|
+
withSnapshotSerializer(snapshotSerializer: SnapshotSerializer<AID, A>): This;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { EventStoreOptions };
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { Aggregate, AggregateId, Event } from "./types";
|
|
2
|
+
import { EventStoreOptions } from "./event-store-options";
|
|
3
|
+
|
|
4
|
+
interface EventStore<
|
|
5
|
+
AID extends AggregateId,
|
|
6
|
+
A extends Aggregate<A, AID>,
|
|
7
|
+
E extends Event<AID>,
|
|
8
|
+
> extends EventStoreOptions<EventStore<AID, A, E>, AID, A, E> {
|
|
9
|
+
persistEvent(event: E, version: number): Promise<void>;
|
|
10
|
+
persistEventAndSnapshot(event: E, aggregate: A): Promise<void>;
|
|
11
|
+
getEventsByIdSinceSequenceNumber(
|
|
12
|
+
id: AID,
|
|
13
|
+
sequenceNumber: number,
|
|
14
|
+
converter: (json: string) => E,
|
|
15
|
+
): Promise<E[]>;
|
|
16
|
+
getLatestSnapshotById(
|
|
17
|
+
id: AID,
|
|
18
|
+
converter: (json: string) => A,
|
|
19
|
+
): Promise<A | undefined>;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { EventStore };
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { AggregateId, KeyResolver } from "../types";
|
|
2
|
+
import { LoggerFactory } from "./logger-factory";
|
|
3
|
+
|
|
4
|
+
class DefaultKeyResolver<AID extends AggregateId> implements KeyResolver<AID> {
|
|
5
|
+
private logger = LoggerFactory.createLogger();
|
|
6
|
+
private hashString(str: string): number {
|
|
7
|
+
if (str === undefined || str === null) {
|
|
8
|
+
throw new Error(`str is undefined or null: ${str}`);
|
|
9
|
+
}
|
|
10
|
+
// this.logger.debug("hashString = ", str);
|
|
11
|
+
let hash = 0;
|
|
12
|
+
for (let i = 0; i < str.length; i++) {
|
|
13
|
+
const char = str.charCodeAt(i);
|
|
14
|
+
hash = (hash << 5) - hash + char;
|
|
15
|
+
hash = hash & hash; // Convert to 32bit integer
|
|
16
|
+
}
|
|
17
|
+
return hash >>> 0; // Convert to unsigned 32bit integer
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
resolvePartitionKey(aggregateId: AID, shardCount: number): string {
|
|
21
|
+
if (aggregateId === undefined || aggregateId === null) {
|
|
22
|
+
throw new Error(`aggregateId is undefined or null: ${aggregateId}`);
|
|
23
|
+
}
|
|
24
|
+
const hash = this.hashString(aggregateId.asString);
|
|
25
|
+
const remainder = hash % shardCount;
|
|
26
|
+
return `${aggregateId.typeName}-${remainder}`;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
resolveSortKey(aggregateId: AID, sequenceNumber: number): string {
|
|
30
|
+
return `${aggregateId.typeName}-${aggregateId.value}-${sequenceNumber}`;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export { DefaultKeyResolver };
|