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 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 }}
@@ -0,0 +1,2 @@
1
+ dist/
2
+ node_modules/
@@ -0,0 +1,3 @@
1
+ tabWidth: 2
2
+ semi: true
3
+ singleQuote: false
@@ -0,0 +1,11 @@
1
+ {
2
+ "[ts]": {
3
+ "editor.codeActionsOnSave": {
4
+ "source.organizeImports": true,
5
+ "source.fixAll.eslint": true
6
+ },
7
+ "editor.formatOnSave": true,
8
+ "typescript.tsdk": "node_modules/typescript/lib",
9
+ "eslint.validate": ["javascript", "json"]
10
+ }
11
+ }
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
@@ -0,0 +1,5 @@
1
+ /** @type {import("ts-jest/dist/types").InitialOptionsTsJest} */
2
+ module.exports = {
3
+ preset: "ts-jest",
4
+ testEnvironment: "node",
5
+ };
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,4 @@
1
+ {
2
+ "$schema": "https://docs.renovatebot.com/renovate-schema.json",
3
+ "extends": ["config:base"]
4
+ }
@@ -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,2 @@
1
+ export * from "./types";
2
+ export * from "./event-store";
@@ -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 };