graphddb 0.4.1 → 0.4.3
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/README.md +166 -643
- package/docs/cdc-emulator.md +203 -0
- package/docs/class-hydration.md +409 -0
- package/docs/cqrs-contract.md +526 -0
- package/docs/design-patterns.md +521 -0
- package/docs/middleware.md +189 -0
- package/docs/mutation-command-derivation.md +356 -0
- package/docs/python-bridge.md +611 -0
- package/docs/spec.md +1626 -0
- package/docs/testing.md +265 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,62 +1,55 @@
|
|
|
1
1
|
# GraphDDB
|
|
2
2
|
|
|
3
|
-
**DynamoDB
|
|
3
|
+
**Build GraphQL-like queries directly on DynamoDB —— while keeping every access pattern explicit.**
|
|
4
4
|
|
|
5
|
-
GraphDDB
|
|
6
|
-
PK
|
|
5
|
+
GraphDDB is a type-safe Runtime that maps DynamoDB's native access patterns onto a GraphQL-like query
|
|
6
|
+
model. PK / GSI / projection / relation / cursor / operation limits are all kept visible rather than hidden.
|
|
7
|
+
It is not an ORM —— it is for **defining, validating, and executing** DynamoDB access patterns through
|
|
8
|
+
TypeScript types.
|
|
7
9
|
|
|
8
|
-
|
|
9
|
-
TypeScript の型を使って DynamoDB のアクセスパターンを定義・検証・実行するための Runtime である。
|
|
10
|
-
|
|
11
|
-
## 🤔 Why GraphDDB?
|
|
12
|
-
|
|
13
|
-
DynamoDB は高速でスケーラブルだが、誤った設計をしやすい。チームはしばしば DynamoDB を
|
|
14
|
-
RDBMS のように扱い —— エンティティごとに1テーブル、アプリケーションコード内での join、
|
|
15
|
-
ORM 抽象の裏に隠れたその場しのぎのアクセスパターン —— その結果、不安定なパフォーマンス、
|
|
16
|
-
不要な GSI、不明瞭なサービス境界に陥る。
|
|
17
|
-
|
|
18
|
-
GraphDDB は逆のアプローチを取る。DynamoDB のアクセスパターンを **コードに直接** 表現し、
|
|
19
|
-
PK/GSI、projection、relation、cursor、操作の上限を可視に保つ。アクセスパターンと relation が
|
|
20
|
-
明示的になり、クエリプランが検査可能になり、よく設計されたアクセスパターンが自然と
|
|
21
|
-
サービス境界に整合する。GraphDDB は DynamoDB の設計を教えるのではない —— 正しい設計を、
|
|
22
|
-
コードを書く上で最も簡単な道にする。
|
|
23
|
-
|
|
24
|
-
これは GraphQL のクエリモデルと DynamoDB のアクセスパターンの自然な対応関係を通じて実現される:
|
|
25
|
-
|
|
26
|
-
```text
|
|
27
|
-
GraphQL Query Model DynamoDB
|
|
28
|
-
arguments ----> PK / GSI lookup
|
|
29
|
-
selection ----> ProjectionExpression
|
|
30
|
-
relation ----> Query / Get / BatchGet
|
|
31
|
-
connection ----> Cursor pagination
|
|
32
|
-
complexity ----> Operation limits
|
|
33
|
-
```
|
|
10
|
+
## ✨ Features
|
|
34
11
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
12
|
+
- ✅ **Type-safe models** —— the TS class is the single source of truth. Keys / GSIs / field types are checked at compile time.
|
|
13
|
+
- ✅ **Graph-style traversal** —— nest `@hasMany` / `@belongsTo` / `@hasOne` inside `select`.
|
|
14
|
+
- ✅ **Automatic access-pattern resolution** —— the runtime picks the PK / GSI from the fields you give.
|
|
15
|
+
- ✅ **Projection** —— only `select`ed fields are read, and only those appear in the result type.
|
|
16
|
+
- ✅ **Execution plan (Explain)** —— inspect the execution plan before touching DynamoDB.
|
|
17
|
+
- ✅ **CQRS contracts** —— public Query/Command contracts (N+1-safe, context boundaries).
|
|
18
|
+
- ✅ **Python generation** —— generate a Python client + runtime from TS as the SSoT (conformance-verified).
|
|
19
|
+
- ✅ **Testing adapter** —— unit-test planner / traversal / transaction / CDC with an in-memory executor.
|
|
20
|
+
- ✅ **Middleware** —— host-side hooks for reads / writes (logging, tenant, authorization).
|
|
21
|
+
- ✅ **CDC emulator** —— drive and test change events equivalent to DynamoDB Streams locally.
|
|
22
|
+
- ✅ **Maintained access paths** —— keep embedded snapshots / aggregate counters synchronized atomically.
|
|
23
|
+
|
|
24
|
+
## 🤔 Philosophy
|
|
25
|
+
|
|
26
|
+
- **Native DynamoDB access patterns** —— express access patterns directly in code instead of hiding them
|
|
27
|
+
behind an ORM abstraction. Because PK / GSI, projection, and limits stay visible, the correct design
|
|
28
|
+
becomes the easiest path.
|
|
29
|
+
- **Graph-style traversal** —— GraphQL's query model (arguments → key lookup, selection → projection,
|
|
30
|
+
relation → Query/Get/BatchGet) corresponds naturally to DynamoDB's access patterns.
|
|
31
|
+
- **Compile-time safety** —— undefined relations, missing GSIs, limit-less lists, and traversals exceeding
|
|
32
|
+
the allowed depth are rejected before execution at compile time / by the linter. The same constraints
|
|
33
|
+
apply equally to human-written and AI-generated code.
|
|
34
|
+
- **Runtime transparency** —— the runtime is not a black box. `explain()` shows the execution plan, and the
|
|
35
|
+
plan reveals the shape of RCU usage.
|
|
38
36
|
|
|
39
37
|
## 📦 Install
|
|
40
38
|
|
|
41
39
|
```bash
|
|
42
40
|
npm install graphddb
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
AWS SDK v3 は peer dependency である —— 使用するクライアントをインストールすること:
|
|
46
|
-
|
|
47
|
-
```bash
|
|
41
|
+
# AWS SDK v3 is a peer dependency. Install the clients you use:
|
|
48
42
|
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
|
|
49
43
|
```
|
|
50
44
|
|
|
51
|
-
`tsx`
|
|
52
|
-
TypeScript
|
|
53
|
-
不要である(そのため esbuild がランタイムバンドルに混入することはない):
|
|
45
|
+
`tsx` is an optional peer dependency, needed only when running the code-generation CLI
|
|
46
|
+
(`graphddb generate …`) directly against TypeScript definitions (library consumers do not need it):
|
|
54
47
|
|
|
55
48
|
```bash
|
|
56
49
|
npm install -D tsx
|
|
57
50
|
```
|
|
58
51
|
|
|
59
|
-
Node.js ≥ 22
|
|
52
|
+
Node.js ≥ 22 is required.
|
|
60
53
|
|
|
61
54
|
## 🚀 Quick Start
|
|
62
55
|
|
|
@@ -76,403 +69,41 @@ class UserModel extends DDBModel {
|
|
|
76
69
|
}
|
|
77
70
|
const User = UserModel.asModel();
|
|
78
71
|
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
// 二重 retry(SDK × ライブラリ)を避けること。
|
|
72
|
+
// Connect the client once. GraphDDB owns throttle / transient retries, so set
|
|
73
|
+
// `maxAttempts: 1` to avoid double retries (SDK × library) — see "Retry & throttling" below.
|
|
82
74
|
DDBModel.setClient(new DynamoDBClient({ maxAttempts: 1 }));
|
|
83
75
|
|
|
84
|
-
//
|
|
76
|
+
// READ —— only the fields you select appear in the result type.
|
|
85
77
|
const user = await User.query({ userId: 'alice' }, { name: true });
|
|
86
78
|
|
|
87
|
-
//
|
|
88
|
-
await User.putItem({ userId: 'alice', name: 'Alice' });
|
|
89
|
-
|
|
90
|
-
// 複数アイテムの atomic な書き込み —— 統一 envelope(デフォルト `mode: 'transaction'`)。
|
|
79
|
+
// WRITE —— a unified envelope that writes multiple items atomically (default mode: 'transaction').
|
|
91
80
|
await DDBModel.mutate({
|
|
92
81
|
user: { update: User, key: { userId: 'alice' }, input: { name: 'Alice B.' } },
|
|
93
82
|
});
|
|
94
|
-
```
|
|
95
|
-
|
|
96
|
-
クロスサービス contract、Python bridge、複数 fragment の atomic mutation などについては、
|
|
97
|
-
以下の各セクションおよび [`docs/spec.md`](./docs/spec.md) を参照。
|
|
98
|
-
|
|
99
|
-
## 🔁 Retry & throttling
|
|
100
|
-
|
|
101
|
-
GraphDDB は **すべての** 単一アイテム操作(`GetItem` / `Query` / `PutItem` / `UpdateItem` /
|
|
102
|
-
`DeleteItem`)、relation read の fan-out、そして `TransactWriteItems` に対して、throttle /
|
|
103
|
-
transient エラーの retry を行う —— 設定不要で、すぐに使える。デフォルトのポリシーは
|
|
104
|
-
full jitter 付きの指数バックオフ(`min(1000, 50·2^(n-1))` ms)で、最大 10 回まで、
|
|
105
|
-
throttle / 5xx / transient エラーのみを retry する(`ValidationException` や
|
|
106
|
-
条件チェック失敗は決して retry しない)。キャンセルされた transaction は理由ごとに
|
|
107
|
-
検査される: `ConditionalCheckFailed` の理由は即座に throw し、純粋に throttle による
|
|
108
|
-
キャンセルは retry される。
|
|
109
|
-
|
|
110
|
-
> これはライブラリ自身の **error retry** であり、`batchGet` / `batchWrite` が既に行う
|
|
111
|
-
> 部分バッチの `UnprocessedKeys` / `UnprocessedItems` retry とは別物である。両者は同じ
|
|
112
|
-
> バックオフのランプを使うが、独立したメカニズムである。
|
|
113
|
-
|
|
114
|
-
**クライアントは `maxAttempts: 1` に設定すること。** ライブラリが retry を所有する以上、
|
|
115
|
-
AWS SDK をデフォルト(`maxAttempts: 3`)のままにすると、GraphDDB の下層で
|
|
116
|
-
*乗算的に* retry してしまう。ライブラリを単一の真実の源とすること:
|
|
117
|
-
|
|
118
|
-
```ts
|
|
119
|
-
DDBModel.setClient(new DynamoDBClient({ maxAttempts: 1 }));
|
|
120
|
-
```
|
|
121
|
-
|
|
122
|
-
GraphDDB は渡されたクライアントを決して変更しない —— この設定はあなたの責任である。
|
|
123
|
-
|
|
124
|
-
ポリシーはグローバルに、あるいは呼び出しごとに調整できる:
|
|
125
|
-
|
|
126
|
-
```ts
|
|
127
|
-
import { DDBModel } from 'graphddb';
|
|
128
|
-
|
|
129
|
-
// グローバルな上書き(setClient / setTableMapping と同様)。`null` でデフォルトに戻る。
|
|
130
|
-
DDBModel.setRetryPolicy({
|
|
131
|
-
maxAttempts: 5,
|
|
132
|
-
jitter: 'full', // または 'none'
|
|
133
|
-
onRetry: ({ attempt, error, delayMs, operation }) => {
|
|
134
|
-
console.warn(`retry #${attempt} of ${operation} after ${delayMs}ms`, error);
|
|
135
|
-
},
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
// 任意の操作の options での呼び出しごとの上書き(`RetryPolicy | false`)。`false` で retry 無効。
|
|
139
|
-
await User.putItem({ userId: 'alice', name: 'Alice' }, { retry: false });
|
|
140
|
-
await User.query({ userId: 'alice' }, { name: true }, { retry: { maxAttempts: 3 } });
|
|
141
|
-
```
|
|
142
|
-
|
|
143
|
-
## 👥 User Permissions Example
|
|
144
|
-
|
|
145
|
-
[user-permissions](./examples/user-permissions/) の例は、ユーザー・グループ・メンバーシップ・
|
|
146
|
-
パーミッションを単一の DynamoDB テーブルでモデル化している。
|
|
147
|
-
ここで GraphDDB の設計が最も分かりやすく見えてくる: テーブルは物理的だが、クエリの表面は
|
|
148
|
-
Graph の形をしている。
|
|
149
|
-
|
|
150
|
-
```text
|
|
151
|
-
UserPermissions table
|
|
152
|
-
|
|
153
|
-
Entity PK SK GSI1PK GSI1SK
|
|
154
|
-
Group GROUP#<groupId> META
|
|
155
|
-
User USER#<userId> PROFILE EMAIL#<email> PROFILE
|
|
156
|
-
GroupMembership GROUP#<groupId> USER#<userId> USER#<userId> GROUP#<groupId>
|
|
157
|
-
Permission GROUP#<groupId> PERM#<resource>#<action>
|
|
158
|
-
```
|
|
159
|
-
|
|
160
|
-
`GroupMembership` は隣接リスト(adjacency list)パターンを使う:
|
|
161
|
-
|
|
162
|
-
- `PK/SK` はグループのメンバーを読む
|
|
163
|
-
- `GSI1PK/GSI1SK` はユーザーが所属するグループを読む
|
|
164
|
-
|
|
165
|
-
同じ物理テーブルが、キーを隠すことなく両方向をサポートする。
|
|
166
|
-
|
|
167
|
-
```ts
|
|
168
|
-
const engGroup = await Group.query(
|
|
169
|
-
{ groupId: 'eng' },
|
|
170
|
-
{
|
|
171
|
-
groupId: true,
|
|
172
|
-
name: true,
|
|
173
|
-
members: {
|
|
174
|
-
select: { userId: true, role: true },
|
|
175
|
-
limit: 20,
|
|
176
|
-
},
|
|
177
|
-
permissions: {
|
|
178
|
-
select: { resource: true, action: true },
|
|
179
|
-
limit: 20,
|
|
180
|
-
},
|
|
181
|
-
},
|
|
182
|
-
);
|
|
183
|
-
|
|
184
|
-
// email(GSI)でユーザーを探し、所属グループを一覧し、各グループについてグループ本体と
|
|
185
|
-
// そのパーミッションを取得する。N 件の membership -> 親 Group の解決は単一の
|
|
186
|
-
// BatchGetItem に集約される(N+1 にならない)。
|
|
187
|
-
const alice = await User.query(
|
|
188
|
-
{ email: 'alice@example.com' },
|
|
189
|
-
{
|
|
190
|
-
userId: true,
|
|
191
|
-
name: true,
|
|
192
|
-
groups: {
|
|
193
|
-
select: {
|
|
194
|
-
groupId: true,
|
|
195
|
-
role: true,
|
|
196
|
-
group: { // membership -> Group (belongsTo, BatchGet)
|
|
197
|
-
select: {
|
|
198
|
-
name: true,
|
|
199
|
-
permissions: { // group -> permissions (hasMany)
|
|
200
|
-
select: { resource: true, action: true },
|
|
201
|
-
filter: { resource: { attributeExists: true } },
|
|
202
|
-
},
|
|
203
|
-
},
|
|
204
|
-
},
|
|
205
|
-
},
|
|
206
|
-
limit: 20,
|
|
207
|
-
},
|
|
208
|
-
},
|
|
209
|
-
{ maxDepth: 3 },
|
|
210
|
-
);
|
|
211
|
-
```
|
|
212
|
-
|
|
213
|
-
最初のクエリは、小さく検査可能な DynamoDB plan に展開される:
|
|
214
|
-
|
|
215
|
-
```mermaid
|
|
216
|
-
flowchart LR
|
|
217
|
-
Q(["Group.query<br/>groupId: eng"]) --> S1["1 Query<br/>PK=GROUP#eng, SK=META<br/>→ Group"]
|
|
218
|
-
Q --> S2["2 Query<br/>PK=GROUP#eng, SK begins_with USER#<br/>→ members"]
|
|
219
|
-
Q --> S3["3 Query<br/>PK=GROUP#eng, SK begins_with PERM#<br/>→ permissions"]
|
|
220
|
-
S2 -.並列ディスパッチ.- S3
|
|
221
|
-
```
|
|
222
|
-
|
|
223
|
-
独立した relation のサブクエリ(ここでは `members` と `permissions`)は並列にディスパッチされる。
|
|
224
|
-
2番目のクエリの `belongsTo` 解決は N 件の lookup を1回の `BatchGetItem` にバッチ化する。
|
|
225
|
-
並列ディスパッチを示すタイムラインは [example](./examples/user-permissions/) を参照。
|
|
226
|
-
|
|
227
|
-
## 📖 Usage
|
|
228
|
-
|
|
229
|
-
以下のスニペットは [user-permissions](./examples/user-permissions/) の例を圧縮したものである。
|
|
230
|
-
|
|
231
|
-
### Entity Definition
|
|
232
|
-
|
|
233
|
-
TypeScript クラスが単一の真実の源(single source of truth)である。Key と GSI は `k`
|
|
234
|
-
タグ付きテンプレートの **segment** から構築され、key builder の入力型がそのまま
|
|
235
|
-
クエリパラメータの型になる。
|
|
236
|
-
|
|
237
|
-
```ts
|
|
238
|
-
const TABLE = 'UserPermissions';
|
|
239
|
-
|
|
240
|
-
@model({ table: TABLE, prefix: 'USER' })
|
|
241
|
-
class UserModel extends DDBModel {
|
|
242
|
-
static readonly keys = key<{ userId: string }>((c) => ({
|
|
243
|
-
pk: k`USER#${c.userId}`,
|
|
244
|
-
sk: k`PROFILE`,
|
|
245
|
-
}));
|
|
246
|
-
|
|
247
|
-
static readonly emailIndex = gsi<{ email: string }>(
|
|
248
|
-
'GSI1',
|
|
249
|
-
(c) => ({ pk: k`EMAIL#${c.email}`, sk: k`PROFILE` }),
|
|
250
|
-
{ unique: true },
|
|
251
|
-
);
|
|
252
|
-
|
|
253
|
-
@string userId!: string;
|
|
254
|
-
@string name!: string;
|
|
255
|
-
@string email!: string;
|
|
256
|
-
@string status!: string;
|
|
257
|
-
|
|
258
|
-
@hasMany(() => GroupMembershipModel, { userId: 'userId' }, {
|
|
259
|
-
limit: { default: 20, max: 100 },
|
|
260
|
-
})
|
|
261
|
-
groups!: GroupMembershipModel[];
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
@model({ table: TABLE, prefix: 'GROUP' })
|
|
265
|
-
class GroupMembershipModel extends DDBModel {
|
|
266
|
-
static readonly keys = key<{ groupId: string; userId: string }>((c) => ({
|
|
267
|
-
pk: k`GROUP#${c.groupId}`,
|
|
268
|
-
sk: k`USER#${c.userId}`,
|
|
269
|
-
}));
|
|
270
|
-
|
|
271
|
-
static readonly userGroupsIndex = gsi<{ userId: string; groupId: string }>(
|
|
272
|
-
'GSI1',
|
|
273
|
-
(c) => ({ pk: k`USER#${c.userId}`, sk: k`GROUP#${c.groupId}` }),
|
|
274
|
-
);
|
|
275
83
|
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
@string role!: string;
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
export const User = UserModel.asModel();
|
|
282
|
-
export const GroupMembership = GroupMembershipModel.asModel();
|
|
283
|
-
```
|
|
284
|
-
|
|
285
|
-
複数 segment の sort key は segment の配列である(例: `sk: [k\`PERM#${c.resource}\`, k\`${c.action}\`]`)。
|
|
286
|
-
partial key はマッチする segment 境界で `begins_with` にコンパイルされる。完全なキーモデルは
|
|
287
|
-
[仕様書](./docs/spec.md) を参照。
|
|
288
|
-
|
|
289
|
-
### GraphQL-like Queries
|
|
290
|
-
|
|
291
|
-
第1引数は「どうやってデータを見つけるか」を記述する。
|
|
292
|
-
第2引数は「何を返すか」を記述する。
|
|
293
|
-
Runtime は与えられたフィールド名から、どのインデックスを使うかを解決する。
|
|
294
|
-
|
|
295
|
-
```ts
|
|
296
|
-
// email でユーザーを取得し、group membership へ traverse する。
|
|
297
|
-
const alice = await User.query(
|
|
298
|
-
{ email: 'alice@example.com' },
|
|
299
|
-
{
|
|
300
|
-
userId: true,
|
|
301
|
-
name: true,
|
|
302
|
-
groups: {
|
|
303
|
-
select: { groupId: true, role: true },
|
|
304
|
-
limit: 20,
|
|
305
|
-
},
|
|
306
|
-
},
|
|
307
|
-
);
|
|
308
|
-
|
|
309
|
-
// 未定義のキーフィールド -> コンパイルエラー
|
|
310
|
-
User.query({ foo: 'bar' }, { name: true });
|
|
311
|
-
```
|
|
312
|
-
|
|
313
|
-
返り値の型は `select` から推論される:
|
|
314
|
-
|
|
315
|
-
```ts
|
|
316
|
-
// alice: {
|
|
317
|
-
// name: string;
|
|
318
|
-
// groups: {
|
|
319
|
-
// items: { groupId: string; role: string }[];
|
|
320
|
-
// cursor: string | null;
|
|
321
|
-
// };
|
|
322
|
-
// }
|
|
323
|
-
```
|
|
324
|
-
|
|
325
|
-
### List Queries
|
|
326
|
-
|
|
327
|
-
```ts
|
|
328
|
-
const result = await GroupMembership.list(
|
|
329
|
-
{ groupId: 'eng' },
|
|
330
|
-
{ userId: true, role: true },
|
|
331
|
-
{ limit: 20, after: cursor },
|
|
332
|
-
);
|
|
333
|
-
// result.items: GroupMembership[]
|
|
334
|
-
// result.cursor: string | null
|
|
335
|
-
```
|
|
336
|
-
|
|
337
|
-
### Filtering(サーバーサイド `filter`)
|
|
338
|
-
|
|
339
|
-
`filter` は宣言的で型安全な条件であり、DynamoDB の **`FilterExpression`** にコンパイルされ、
|
|
340
|
-
**サーバーサイド** で評価される。AWS AppSync の `ModelFilterInput` をモデルにしている:
|
|
341
|
-
裸の値は等価の省略記法、operator オブジェクトは比較を表現し、`and` / `or` / `not` で
|
|
342
|
-
論理グループを構築する。**エンティティ全体** に対して型付けされるため、`select` に **含まれない**
|
|
343
|
-
属性も参照できる。
|
|
344
|
-
|
|
345
|
-
```ts
|
|
346
|
-
const result = await Order.list(
|
|
347
|
-
{ userId: 'u001' },
|
|
348
|
-
{ orderId: true }, // amount / status は projection 不要
|
|
349
|
-
{
|
|
350
|
-
filter: {
|
|
351
|
-
status: 'confirmed', // #status = :v (等価の省略記法)
|
|
352
|
-
amount: { gt: 100 }, // #amount > :v
|
|
353
|
-
title: { beginsWith: 'A' }, // begins_with(#title, :v)
|
|
354
|
-
shippedAt: { attributeExists: true },
|
|
355
|
-
or: [{ status: 'pending' }, { amount: { lt: 10 } }],
|
|
356
|
-
},
|
|
357
|
-
},
|
|
358
|
-
);
|
|
84
|
+
// Raw base operations on a single item (named after the DynamoDB API).
|
|
85
|
+
await User.putItem({ userId: 'alice', name: 'Alice' });
|
|
359
86
|
```
|
|
360
87
|
|
|
361
|
-
|
|
362
|
-
`
|
|
363
|
-
|
|
364
|
-
`
|
|
365
|
-
|
|
366
|
-
利用可能な operator(フィールド型ごと): `eq`、`ne`、`ge`、`le`、`gt`、`lt`、
|
|
367
|
-
`between`、`in`、`beginsWith`、`contains`、`notContains`、`attributeExists`、
|
|
368
|
-
`attributeType`、`size`。operator は **フィールドの宣言型によって制約される** ——
|
|
369
|
-
例えば数値フィールドへの `beginsWith` / `contains`、あるいは数値への `gt: 'x'` は
|
|
370
|
-
コンパイルエラーになる。
|
|
371
|
-
|
|
372
|
-
operator オブジェクトでは表現できない稀な条件のために、`cond` という raw な escape hatch が
|
|
373
|
-
式 fragment を受け取る。カラム名は bare な文字列ではなく、リファクタ安全な
|
|
374
|
-
`Model.col.<field>` 参照を使わなければならない。値はパラメータ化される:
|
|
88
|
+
Reads center on `query` (a single PK / unique GSI lookup) / `list` (multiple items, with a cursor).
|
|
89
|
+
Writes center on `DDBModel.mutate` (a unified envelope), while `putItem` / `updateItem` / `deleteItem` are
|
|
90
|
+
positioned secondarily as raw base operations named after the DynamoDB API. For the full API and options,
|
|
91
|
+
see [`docs/spec.md`](./docs/spec.md).
|
|
375
92
|
|
|
376
93
|
```ts
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
すべての値はパラメータ化され(`ExpressionAttributeValues`)、すべてのカラム名は
|
|
381
|
-
エイリアス化される(`ExpressionAttributeNames`)—— リテラルの補間は存在しないため、
|
|
382
|
-
コンパイルされた式はインジェクション安全であり、`KeyConditionExpression` /
|
|
383
|
-
`ProjectionExpression` とは独立して付与される。
|
|
384
|
-
|
|
385
|
-
同じ `cond` fragment は read filter としてだけでなく、書き込みの `condition`
|
|
386
|
-
(`putItem` / `updateItem` / `deleteItem`、transaction item、public command に対して)
|
|
387
|
-
としても受け付けられる。
|
|
388
|
-
|
|
389
|
-
> **RCU に関する注意:** `FilterExpression` は read capacity を **削減しない** ——
|
|
390
|
-
> DynamoDB はまずマッチするキーを読み、その後に filter する。`limit` は filter の
|
|
391
|
-
> **前** に適用されるため、1ページが `limit` **未満** のアイテムを返すことがある
|
|
392
|
-
> (そしてページ全体が filter で除外された場合、null でない cursor を持つ空ページになる)。
|
|
393
|
-
> 効率的な絞り込みのためにキーを設計し、`filter` は正しさのために使うこと。
|
|
394
|
-
|
|
395
|
-
### Writes
|
|
396
|
-
|
|
397
|
-
raw な base テーブルの書き込みは `putItem` / `updateItem` / `deleteItem` である ——
|
|
398
|
-
ライフサイクルのセマンティクスを持たないプリミティブな単一アイテム操作:
|
|
399
|
-
|
|
400
|
-
```ts
|
|
401
|
-
await User.putItem({
|
|
402
|
-
userId: 'alice',
|
|
403
|
-
name: 'Alice',
|
|
404
|
-
email: 'alice@example.com',
|
|
405
|
-
status: 'active',
|
|
406
|
-
});
|
|
407
|
-
|
|
408
|
-
await GroupMembership.putItem({
|
|
409
|
-
groupId: 'eng',
|
|
410
|
-
userId: 'alice',
|
|
411
|
-
role: 'admin',
|
|
412
|
-
});
|
|
94
|
+
// query: the 1st argument says "how to find it", the 2nd says "what to return". The return type is inferred from select.
|
|
95
|
+
const alice = await User.query({ userId: 'alice' }, { name: true });
|
|
413
96
|
|
|
414
|
-
|
|
97
|
+
// list: keys that return multiple items use a cursor-based list.
|
|
98
|
+
const page = await GroupMembership.list({ groupId: 'eng' }, { userId: true, role: true }, { limit: 20 });
|
|
99
|
+
// page.items / page.cursor
|
|
415
100
|
```
|
|
416
101
|
|
|
417
|
-
|
|
418
|
-
は raw なプリミティブではなく `DDBModel.mutate` を経由する(後述の
|
|
419
|
-
[The in-process unified envelope](#the-in-process-unified-envelope) を参照)。
|
|
420
|
-
|
|
421
|
-
### The in-process unified envelope
|
|
422
|
-
|
|
423
|
-
`DDBModel.query` と `DDBModel.mutate` は、1回の呼び出しで複数の route を実行する単一の
|
|
424
|
-
**alias-map envelope** を取る。これは GraphQL operation の in-process 版である:
|
|
425
|
-
各 alias は名前付きの route であり、結果は同じ alias でキー付けされる。
|
|
426
|
-
|
|
427
|
-
`DDBModel.query(map)` は各 read route を独立かつ並列に実行する —— route 間の
|
|
428
|
-
一貫性はなく、GraphQL の並列フィールド解決とまったく同じである:
|
|
429
|
-
|
|
430
|
-
```ts
|
|
431
|
-
const { user, members } = await DDBModel.query({
|
|
432
|
-
user: { query: User, key: { userId: 'u1' }, select: { name: true } },
|
|
433
|
-
members: { list: GroupMembership, key: { groupId: 'eng' }, select: { role: true }, options: { limit: 20 } },
|
|
434
|
-
});
|
|
435
|
-
// user: Item | null ; members: { items, cursor }
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
read route の記述子は `{ query | list: Model, key, select, options? }` である
|
|
439
|
-
(`query` / `list` のいずれか一方)。`query` の `options` は
|
|
440
|
-
`{ consistentRead?, maxDepth? }`、`list` の場合は `{ limit?, after?, order?, filter? }` である。
|
|
441
|
-
|
|
442
|
-
`DDBModel.mutate(map, { mode })` は write route を実行する。write route の記述子は
|
|
443
|
-
`{ create | update | remove: Model, key, input?, condition?, result? }` である:
|
|
444
|
-
|
|
445
|
-
```ts
|
|
446
|
-
const res = await DDBModel.mutate({
|
|
447
|
-
a: { create: GroupMembership, key: { groupId: 'eng', userId: 'u1' }, input: { role: 'admin' } },
|
|
448
|
-
b: { update: User, key: { userId: 'u1' }, input: { name: 'Ann' }, condition: { status: 'active' }, result: { select: { name: true } } },
|
|
449
|
-
}, { mode: 'transaction' });
|
|
450
|
-
```
|
|
102
|
+
## 🔎 Inspect Execution Plans (Explain)
|
|
451
103
|
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
- **`condition`** は `WriteCondition` の書き込みゲートである —— read filter と同じ宣言的
|
|
456
|
-
operator のサブセット(`eq`/`ne`/比較/`between`/`in`/文字列操作/`size`/`attributeType` +
|
|
457
|
-
`and`/`or`/`not`)、レガシーな存在プリミティブ(`notExists` / `attributeExists` /
|
|
458
|
-
`attributeNotExists`)、または raw な `cond` fragment。
|
|
459
|
-
- **`result: { select, options? }`** は書き込んだアイテムを read-back する。省略すると
|
|
460
|
-
その route は void を返す。
|
|
461
|
-
- **`mode: 'transaction'`**(デフォルト)は1回の atomic な `TransactWriteItems` である:
|
|
462
|
-
all-or-nothing のロールバック、失敗時に throw、25 件を超える map はエラーになる
|
|
463
|
-
(決して分割されない)。これは GraphQL の all-or-nothing な atomic 契約である。
|
|
464
|
-
- **`mode: 'parallel'`** は非 atomic である: 各 alias は結果オブジェクト内で
|
|
465
|
-
`{ ok }` | `{ error }` として部分的な成否を報告し、GraphQL のフィールドごとの
|
|
466
|
-
部分成功を反映する。独立した操作は並列に実行され、依存関係は `$.alias.field`
|
|
467
|
-
参照で順序付けられる。
|
|
468
|
-
|
|
469
|
-
GraphQL の用語で言えば: transaction は atomic な all-or-nothing、parallel は非 atomic な
|
|
470
|
-
フィールドごとの部分成功、そして `query` route は route 間の一貫性を持たない並列独立 read である。
|
|
471
|
-
|
|
472
|
-
### Inspect Execution Plans
|
|
473
|
-
|
|
474
|
-
`explain()` は、実行される前の DynamoDB 操作を表示する。
|
|
475
|
-
`select` は `ProjectionExpression` に変換されるため、要求した属性のみが読まれる。
|
|
104
|
+
This is what sets GraphDDB apart. `explain()` returns the DynamoDB operations before they run. `select` is
|
|
105
|
+
translated into a `ProjectionExpression`, so only the requested attributes are read. The plan can be used
|
|
106
|
+
for testing, debugging, and RCU estimation:
|
|
476
107
|
|
|
477
108
|
```ts
|
|
478
109
|
const plan = GroupMembership.explain(
|
|
@@ -484,252 +115,144 @@ const plan = GroupMembership.explain(
|
|
|
484
115
|
// type: "Query",
|
|
485
116
|
// tableName: "UserPermissions",
|
|
486
117
|
// keyCondition: { PK: "GROUP#eng" },
|
|
487
|
-
// rangeCondition: {
|
|
488
|
-
//
|
|
489
|
-
// key: "SK",
|
|
490
|
-
// value: "USER#"
|
|
491
|
-
// },
|
|
492
|
-
// limit: 10
|
|
118
|
+
// rangeCondition: { operator: "begins_with", key: "SK", value: "USER#" },
|
|
119
|
+
// limit: 10,
|
|
493
120
|
// }]
|
|
494
121
|
// }
|
|
495
122
|
```
|
|
496
123
|
|
|
497
|
-
|
|
124
|
+
For the complete shape of the plan (such as the `rangeCondition` separation on a GSI partial match), see
|
|
125
|
+
[`docs/spec.md`](./docs/spec.md).
|
|
498
126
|
|
|
499
|
-
|
|
500
|
-
const gsiPlan = GroupMembership.explain(
|
|
501
|
-
{ userId: 'alice' },
|
|
502
|
-
{ select: { groupId: true, role: true }, limit: 10 },
|
|
503
|
-
);
|
|
504
|
-
// {
|
|
505
|
-
// operations: [{
|
|
506
|
-
// type: "Query",
|
|
507
|
-
// keyCondition: { GSI1PK: "USER#alice" },
|
|
508
|
-
// indexName: "GSI1",
|
|
509
|
-
// rangeCondition: {
|
|
510
|
-
// operator: "begins_with",
|
|
511
|
-
// key: "GSI1SK",
|
|
512
|
-
// value: "GROUP#"
|
|
513
|
-
// },
|
|
514
|
-
// ...
|
|
515
|
-
// }]
|
|
516
|
-
// }
|
|
517
|
-
```
|
|
518
|
-
|
|
519
|
-
## 🧠 Core Concepts
|
|
520
|
-
|
|
521
|
-
### Entity = Access Pattern
|
|
522
|
-
|
|
523
|
-
Entity は ORM モデルではない。
|
|
524
|
-
DynamoDB に対する、境界付けられたアクセス表面(bounded access surface)である。
|
|
525
|
-
|
|
526
|
-
Entity は、サービス境界内でデータにアクセスする、サポートされたすべての方法を定義する:
|
|
527
|
-
|
|
528
|
-
- 物理キー
|
|
529
|
-
- GSI
|
|
530
|
-
- relation
|
|
531
|
-
- projection
|
|
532
|
-
- traversal ルール
|
|
127
|
+
## 🧪 Testing (strongly recommended)
|
|
533
128
|
|
|
534
|
-
|
|
535
|
-
|
|
129
|
+
The **in-memory executor** in `graphddb/testing` lets you fully unit-test planner / relation traversal /
|
|
130
|
+
transaction / CDC with **no Docker and no DynamoDB Local**. Because it never touches a real DynamoDB and
|
|
131
|
+
runs in-process, you can verify model-mapping, query-plans, relation resolution, and differential
|
|
132
|
+
aggregation while keeping CI fast and deterministic. See [`docs/testing.md`](./docs/testing.md).
|
|
536
133
|
|
|
537
|
-
|
|
134
|
+
## 🔗 Relation Traversal
|
|
538
135
|
|
|
539
|
-
|
|
540
|
-
|
|
136
|
+
A relation is a query bound to the value of the parent entity. Declare it with `@hasMany` / `@belongsTo` /
|
|
137
|
+
`@hasOne` and traverse it by nesting inside `select`. Independent relation subqueries are dispatched in
|
|
138
|
+
parallel, and N `belongsTo` resolutions are aggregated into a single `BatchGetItem` (no N+1):
|
|
541
139
|
|
|
542
140
|
```ts
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
141
|
+
const alice = await User.query(
|
|
142
|
+
{ email: 'alice@example.com' },
|
|
143
|
+
{
|
|
144
|
+
name: true,
|
|
145
|
+
groups: {
|
|
146
|
+
select: {
|
|
147
|
+
role: true,
|
|
148
|
+
group: { select: { name: true } }, // membership -> Group (belongsTo, BatchGet)
|
|
149
|
+
},
|
|
150
|
+
limit: 20,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{ maxDepth: 2 }, // traversal defaults to depth=1. Deeper traversal must be explicitly allowed.
|
|
550
154
|
);
|
|
551
155
|
```
|
|
552
156
|
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
157
|
+
`hasMany` / `list` require a default / max limit. For the complete relation model, see
|
|
158
|
+
[`docs/spec.md`](./docs/spec.md).
|
|
159
|
+
|
|
160
|
+
## 🧩 Maintained Access Paths
|
|
161
|
+
|
|
162
|
+
Declaring a relation or scalar as a **maintained access path** keeps a *separate* row in sync with the
|
|
163
|
+
lifecycle of the source entity (all of it composed into the same atomic transaction as the source write):
|
|
164
|
+
|
|
165
|
+
- **embedded snapshot** —— `pattern: 'embeddedSnapshot'` on `@hasMany` / `@hasOne` keeps a projected
|
|
166
|
+
snapshot / bounded collection synchronized onto the owner row.
|
|
167
|
+
- **aggregate counter** —— `@aggregate` + `count()` keeps a scalar aggregate synchronized with an atomic `ADD ±1`.
|
|
168
|
+
- **materialized view / sparse view** —— `@model({ kind })` + `@maintainedFrom` declare a view as its own
|
|
169
|
+
model (later phases).
|
|
170
|
+
- **versioned history** —— maintenance of versioned history rows.
|
|
171
|
+
|
|
172
|
+
Synchronized maintenance (`updateMode: 'mutation'`) works in both TS and Python (conformance-verified). For
|
|
173
|
+
how to declare them, the pattern mapping table, and per-phase coverage, see
|
|
174
|
+
[`docs/design-patterns.md`](./docs/design-patterns.md). Examples:
|
|
175
|
+
[embedded-snapshot-pattern](./examples/embedded-snapshot-pattern/) /
|
|
176
|
+
[aggregate-counter](./examples/aggregate-counter/).
|
|
177
|
+
|
|
178
|
+
## ⚙️ Runtime
|
|
179
|
+
|
|
180
|
+
The runtime's responsibilities are short. For details, see [`docs/spec.md`](./docs/spec.md):
|
|
181
|
+
|
|
182
|
+
- **access pattern resolution** —— pick the PK / GSI from the fields you give.
|
|
183
|
+
- **projection** —— compile `select` into a `ProjectionExpression`.
|
|
184
|
+
- **relation traversal** —— dispatch bound queries in parallel (N+1-safe BatchGet).
|
|
185
|
+
- **retry / throttling** —— retry throttle / transient errors with full-jitter exponential backoff, no
|
|
186
|
+
configuration required (set the client to `maxAttempts: 1`, since the library owns retries).
|
|
187
|
+
- **hydration** —— reconstruct raw items into typed partial entities.
|
|
188
|
+
|
|
189
|
+
## 🛠 More Capabilities
|
|
190
|
+
|
|
191
|
+
Each capability is summarized here only. For details, operator lists, and procedures, see each doc:
|
|
192
|
+
|
|
193
|
+
- **Filtering** —— a declarative, type-safe server-side `filter` (compatible with `FilterExpression` /
|
|
194
|
+
AppSync `ModelFilterInput`). `Model.col` + the `cond` raw escape hatch are shared by read filters / write
|
|
195
|
+
`condition`s. For the operator list, see [`docs/spec.md`](./docs/spec.md).
|
|
196
|
+
- **Middleware / Hooks** —— `DDBModel.use` for host-side hooks on reads / writes (logging, metrics, tenant /
|
|
197
|
+
authorization scoping). Host-only and non-serialized. For hook points, see [`docs/middleware.md`](./docs/middleware.md).
|
|
198
|
+
- **Class hydration** —— `options.hydrate` loads read results into host-language domain objects (opt-in,
|
|
199
|
+
Phase 1: `query` top level). [`docs/class-hydration.md`](./docs/class-hydration.md).
|
|
200
|
+
- **Python bridge** —— generate a Python client + runtime with TS as the SSoT, kept in step by a TS↔Python
|
|
201
|
+
conformance suite. [`docs/python-bridge.md`](./docs/python-bridge.md).
|
|
202
|
+
- **CQRS contracts** —— public Query/Command contracts (cardinality matrix, N+1 safety, context boundaries,
|
|
203
|
+
composition across contracts). [`docs/cqrs-contract.md`](./docs/cqrs-contract.md) /
|
|
204
|
+
[`docs/mutation-command-derivation.md`](./docs/mutation-command-derivation.md).
|
|
205
|
+
- **CDC emulator** —— change events equivalent to DynamoDB Streams, to drive and test differential
|
|
206
|
+
aggregation. [`docs/cdc-emulator.md`](./docs/cdc-emulator.md).
|
|
561
207
|
|
|
562
|
-
|
|
563
|
-
@hasMany(() => GroupMembershipModel, { userId: 'userId' }, {
|
|
564
|
-
limit: { default: 20, max: 100 },
|
|
565
|
-
})
|
|
566
|
-
groups!: GroupMembershipModel[];
|
|
567
|
-
```
|
|
568
|
-
|
|
569
|
-
### Field Decorators
|
|
570
|
-
|
|
571
|
-
型安全なセマンティック decorator が DynamoDB の属性型を定義する。
|
|
572
|
-
`ClassFieldDecoratorContext` に基づくコンパイル時チェックが、宣言された TypeScript の型と
|
|
573
|
-
DynamoDB の型の不一致を検出する。
|
|
574
|
-
|
|
575
|
-
```ts
|
|
576
|
-
@string userId!: string; // DynamoDB S
|
|
577
|
-
@number amount!: number; // DynamoDB N
|
|
578
|
-
@datetime createdAt!: Date; // ISO 8601 文字列として保存される
|
|
579
|
-
@boolean isActive!: boolean; // DynamoDB BOOL
|
|
580
|
-
```
|
|
208
|
+
## 📚 Documentation
|
|
581
209
|
|
|
582
|
-
|
|
210
|
+
**The full documentation is bundled in the npm package** (under `docs/`). After installation you can browse
|
|
211
|
+
every doc at `node_modules/graphddb/docs/`, and if you add it as a `devDependency` you have all the
|
|
212
|
+
documentation available offline at hand. On GitHub, follow the links in the table below.
|
|
583
213
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
214
|
+
| Document | Description |
|
|
215
|
+
|----------|-------------|
|
|
216
|
+
| [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. |
|
|
217
|
+
| [Design patterns](./docs/design-patterns.md) | Mapping representative DynamoDB design patterns onto graphddb features (how to declare `pattern` / `@model({ kind })` + `@maintainedFrom` / `@aggregate`, read & write maintenance, per-phase coverage). |
|
|
218
|
+
| [Middleware / Hooks](./docs/middleware.md) | The `DDBModel.use` host-side middleware: hook points read R1–R5 / write W1–W5, `{ context }`, ordering. |
|
|
219
|
+
| [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. |
|
|
220
|
+
| [Mutation → command derivation](./docs/mutation-command-derivation.md) | The internal write-plan composition DSL behind the Command IF (`entityWrites`, fragment composition, derivation of atomic `TransactWriteItems`). |
|
|
221
|
+
| [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` that loads read results into host objects (Phase 1: `query` top level). |
|
|
222
|
+
| [Multi-language / Python bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and a Python runtime; TS↔Python conformance. |
|
|
223
|
+
| [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
|
|
224
|
+
| [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
|
|
225
|
+
| [Benchmark](./benchmark/RESULTS.md) | Library comparison against hand-written AWS SDK, ElectroDB, DynamoDB Toolbox, and OneTable. |
|
|
587
226
|
|
|
588
|
-
|
|
227
|
+
## 💡 Examples
|
|
589
228
|
|
|
590
|
-
|
|
591
|
-
|
|
229
|
+
| Example | Description |
|
|
230
|
+
|---------|-------------|
|
|
231
|
+
| [user-permissions](./examples/user-permissions/) | Manages users, groups, and permissions with Single Table Design. Includes adjacency lists, inverted indexes, relation traversal, and `explain`. |
|
|
232
|
+
| [aggregate-tree-pattern](./examples/aggregate-tree-pattern/) | Differential tree aggregation (`siteScoreAverage`) driven by the CDC emulator: dirty propagation, throttled sweeps, recomputation. |
|
|
233
|
+
| [embedded-snapshot-pattern](./examples/embedded-snapshot-pattern/) | Treating a relation as a maintained access path (Epic #118): `pattern: 'embeddedSnapshot'` keeps a denormalized collection / snapshot synchronized onto the owner row in the same atomic transaction as the source write. |
|
|
234
|
+
| [aggregate-counter](./examples/aggregate-counter/) | The `@aggregate` counter from RFC §5.2: the `ThreadPost.created` / `removed` lifecycle keeps `ThreadCounter.postCount` synchronized via an atomic `ADD ±1` within the same `TransactWriteItems` as the source write. |
|
|
592
235
|
|
|
593
|
-
##
|
|
236
|
+
## 🏛 Architecture
|
|
594
237
|
|
|
595
|
-
|
|
238
|
+
A query travels from the TS Model through the Planner / Runtime to DynamoDB:
|
|
596
239
|
|
|
597
240
|
```mermaid
|
|
598
|
-
flowchart
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
L4["⚡ Layer 4: Executor<br/>DynamoDB DocumentClient ラッパー"]
|
|
603
|
-
L5["💧 Layer 5: Hydrator<br/>型変換 / Embedded 再構築 / メタデータ付与"]
|
|
604
|
-
|
|
605
|
-
L1 -->|EntityMetadata Registry| L2
|
|
606
|
-
L2 -->|ExecutionPlan| L3
|
|
607
|
-
L3 -->|"DynamoDBOperation[]"| L4
|
|
608
|
-
L4 -->|raw Items| L5
|
|
609
|
-
L5 -->|Partial Entity インスタンス| OUT(["✅ 型付き結果"])
|
|
241
|
+
flowchart LR
|
|
242
|
+
M["TS Model<br/>Entity / Key / GSI / Relation"] --> P["Planner<br/>access pattern resolution / projection"]
|
|
243
|
+
P --> R["Runtime<br/>execute / retry / hydrate"]
|
|
244
|
+
R --> D[("DynamoDB")]
|
|
610
245
|
```
|
|
611
246
|
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
| ルール | 内容 |
|
|
615
|
-
|------|-------------|
|
|
616
|
-
| Service Boundary First | 1テーブルは1エンティティではなく、1つのサービス境界を表す。 |
|
|
617
|
-
| Access Pattern First | テーブルや ORM 風のエンティティをモデル化する前に、DynamoDB のアクセスパターンをモデル化する。 |
|
|
618
|
-
| Natural Mapping | Query / Get / BatchGet / Projection / cursor を GraphQL ライクなクエリの形にマップする。 |
|
|
619
|
-
| Inspectable Plans | DynamoDB の操作と実行計画は、実行前に検査可能である。 |
|
|
620
|
-
| Single-item query | `query` は PK または unique GSI の lookup のみ。複数件返りうるキーには `list` を使う。 |
|
|
621
|
-
| Explicit limits | `hasMany` と `list` は default/max limit の指定が必須。 |
|
|
622
|
-
| Bounded depth | Relation traversal はデフォルトで depth=1。より深い traversal は `maxDepth` で明示的に許可する必要がある。 |
|
|
623
|
-
| Parallel relations | 独立した relation のサブクエリ、BatchGet のチャンク、ネストした解決は並列にディスパッチされる(上限あり)。DynamoDB は HTTP ベースのため、直列化すべきコネクションが存在しない。 |
|
|
624
|
-
| No Scan | Full table scan は禁止。linter が検出・ブロックする。 |
|
|
625
|
-
| Partial select | クエリ結果は partial 型。select したフィールドのみが結果の型に現れる。 |
|
|
626
|
-
| Structured keys | キーは `k` segment テンプレートから構築され、partial key は segment 境界で `begins_with` にコンパイルされる。 |
|
|
627
|
-
| Typed consistentRead | `consistentRead` は PK クエリでのみ利用可能。GSI クエリはコンパイル時に拒否される。 |
|
|
628
|
-
| Server-side filter | `filter` は宣言的な DynamoDB `FilterExpression`(AppSync `ModelFilterInput` 互換)で、エンティティ全体に対して型付けされ、projection されていない属性も参照できる。RCU は削減せず、`limit` はその前に適用される。 |
|
|
629
|
-
|
|
630
|
-
## 🚧 Architectural Boundaries
|
|
631
|
-
|
|
632
|
-
Relation traversal は、Entity 上に定義されたアクセスパスのみをたどる。
|
|
633
|
-
未定義の relation、欠けている GSI、limit のない list、許可された depth を超える traversal は、
|
|
634
|
-
実行前にチェックされる。
|
|
635
|
-
|
|
636
|
-
これにより、境界をまたぐクエリや DynamoDB に不向きなアクセスパターンが、コード上で見えやすくなる。
|
|
637
|
-
コードが人間によって書かれたものであれ、AI によって生成されたものであれ、同じアーキテクチャ上の
|
|
638
|
-
制約が適用される。
|
|
639
|
-
|
|
640
|
-
## 🔍 Linter
|
|
641
|
-
|
|
642
|
-
静的解析が、runtime の前に Entity 定義の問題を検出する:
|
|
643
|
-
|
|
644
|
-
- Scan の使用
|
|
645
|
-
- `hasMany` / `list` での limit 未指定
|
|
646
|
-
- 曖昧な GSI 解決
|
|
647
|
-
- unique GSI に対する query/list の境界違反
|
|
648
|
-
|
|
649
|
-
## 🧱 Beyond the Core
|
|
650
|
-
|
|
651
|
-
上記の query / relation / write API がコアレイヤーである。GraphDDB はさらに以下も提供する:
|
|
652
|
-
|
|
653
|
-
- **[CQRS contracts](./docs/cqrs-contract.md)** —— N+1 安全な cardinality matrix、
|
|
654
|
-
contract をまたぐ合成、context 境界の強制を備えた、public な Query/Command contract モデル。
|
|
655
|
-
- **[Mutation → command derivation](./docs/mutation-command-derivation.md)** —— public な
|
|
656
|
-
Command IF の背後にある、内部的な write-plan 合成 DSL。モデルの書き込みセマンティクス
|
|
657
|
-
(`entityWrites`: 参照整合性、一意性、edge effect、派生カウンタ、outbox event、冪等性)を
|
|
658
|
-
一度宣言すると、`mutation` が1つ以上の write fragment を合成し、コンパイラがそれらを単一の
|
|
659
|
-
atomic な `TransactWriteItems` にマージする。すべての書き込みパスは1つの共有された
|
|
660
|
-
`commitTransaction` オーケストレーションを経由する。**リレーション維持アクセスパス**
|
|
661
|
-
(Epic #118)はこれを拡張する: リレーションの `write.maintainedOn` やスカラ `@aggregate`
|
|
662
|
-
フィールドが、source エンティティのライフサイクルに同期して *別の* 行を維持する —— 射影された
|
|
663
|
-
**snapshot**(`SET`)、上限付き **collection**(`list_append`)、スカラ **counter**
|
|
664
|
-
(`@aggregate` `count()` → atomic `ADD ±1`)—— を source write と同一の atomic transaction に
|
|
665
|
-
合成する([aggregate-counter](./examples/aggregate-counter/) 例を参照)。
|
|
666
|
-
- **[Opt-in class hydration](./docs/class-hydration.md)** —— `options.hydrate` ファクトリを
|
|
667
|
-
渡すと、read 結果をデフォルトの Typed Plain Object ではなく、ホスト言語のドメインオブジェクトに
|
|
668
|
-
ロードできる。ホスト専用で、bridge SSoT にシリアライズされることはない。Phase 1
|
|
669
|
-
(`query` のトップレベル)は実装済み。`list` / relation ごとの hydration は今後の予定。
|
|
670
|
-
- **[Multi-language (Python bridge)](./docs/python-bridge.md)** —— TypeScript が単一の真実の源
|
|
671
|
-
であり、同じ定義から Python クライアント + runtime を生成し、TS↔Python の conformance スイートで
|
|
672
|
-
歩調を合わせる。
|
|
673
|
-
- **[In-memory testing](./docs/testing.md)** —— `graphddb/testing` は model-mapping、query-plan、
|
|
674
|
-
relation-traversal、CDC のテストを **Docker なしの in-process** で実行する。
|
|
675
|
-
- **[CDC emulator](./docs/cdc-emulator.md)** —— ローカルでの差分集計を駆動・テストするための、
|
|
676
|
-
DynamoDB Streams 相当の変更イベント。
|
|
247
|
+
From the same TS Model (the SSoT), multiple targets are derived:
|
|
677
248
|
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
| [Mutation → command derivation](./docs/mutation-command-derivation.md) | Command IF の背後にある内部的な write-plan 合成 DSL: モデルの write セマンティクス(`entityWrites`)、fragment 合成、atomic な `TransactWriteItems` の導出。 |
|
|
686
|
-
| [Class hydration](./docs/class-hydration.md) | read 結果をホスト言語のオブジェクトにロードする opt-in な `options.hydrate` ファクトリ(Phase 1: `query` トップレベル; Phase 2–3 は今後)。 |
|
|
687
|
-
| [Multi-language / Python bridge](./docs/python-bridge.md) | TS を SSoT としたコード生成と Python runtime; TS↔Python の conformance。 |
|
|
688
|
-
| [In-memory test adapter](./docs/testing.md) | `graphddb/testing` と `MemoryInspector` による Docker 不要の in-process テスト。 |
|
|
689
|
-
| [CDC emulator](./docs/cdc-emulator.md) | 差分集計パターン向けの変更イベント emulator(dev/test)。 |
|
|
690
|
-
| [Benchmark](./benchmark/RESULTS.md) | 手書きの AWS SDK、ElectroDB、DynamoDB Toolbox、OneTable とのライブラリ比較。 |
|
|
691
|
-
|
|
692
|
-
## 💡 Examples
|
|
693
|
-
|
|
694
|
-
| Example | 内容 |
|
|
695
|
-
|---------|-------------|
|
|
696
|
-
| [user-permissions](./examples/user-permissions/) | Single Table Design でユーザー・グループ・パーミッションを管理する。隣接リスト、転置インデックス、relation traversal、`explain` を含む。 |
|
|
697
|
-
| [aggregate-tree-pattern](./examples/aggregate-tree-pattern/) | CDC emulator で駆動される差分的なツリー集計(`siteScoreAverage`): dirty 伝播、throttle 付き sweep、再計算。 |
|
|
698
|
-
| [embedded-snapshot-pattern](./examples/embedded-snapshot-pattern/) | リレーションを **維持アクセスパス** として扱う(Epic #118): `pattern: 'embeddedSnapshot'` のリレーションが、source write と同一の atomic transaction で、非正規化された collection / snapshot を owner 行に同期維持する。 |
|
|
699
|
-
| [aggregate-counter](./examples/aggregate-counter/) | RFC §5.2 の `@aggregate` **counter**: `ThreadPost.created`/`removed` のライフサイクルが、source write と同一の `TransactWriteItems` 内で atomic な `ADD ±1` により `ThreadCounter.postCount` を同期維持する。 |
|
|
700
|
-
|
|
701
|
-
## ✨ Features
|
|
702
|
-
|
|
703
|
-
- Entity メタデータ / decorator
|
|
704
|
-
- DDBModel 基底クラス / コネクション管理
|
|
705
|
-
- 型システム(SelectableOf, QueryResult, QueryKeyOf)
|
|
706
|
-
- CRUD(put, update, delete, 条件付き書き込み —— `WriteCondition` は宣言的 filter operator の
|
|
707
|
-
フルセットに加え `cond` raw escape hatch を共有する)
|
|
708
|
-
- Query / List(planner, executor, hydrator, cursor pagination)
|
|
709
|
-
- 型安全な filtering: 宣言的なサーバーサイド `filter`(FilterExpression / AppSync
|
|
710
|
-
`ModelFilterInput` 互換)、加えて `Model.col` のカラム参照と `cond` raw escape hatch
|
|
711
|
-
- 構造化 segment キー(`k` タグ): partial key は segment 境界で `begins_with` にコンパイルされる
|
|
712
|
-
- 説明可能な実行計画
|
|
713
|
-
- Relation(hasMany, belongsTo, hasOne, depth 制限, traversal)
|
|
714
|
-
- 維持アクセスパス(Epic #118): `pattern: 'embeddedSnapshot'` のリレーションが非正規化された collection / 単一行 snapshot を owner 行に同期維持し、`@aggregate` `count()` カウンタが atomic な `ADD ±1` で同期する。いずれも source write の atomic transaction に合成される(`maintainedOn` cross-entity トリガ、関数形 `projection`)。同期(`updateMode: 'mutation'`)の維持は TS / Python 両方で動作(conformance 検証済み)。stream / trim / `max()` / `materializedView` / `sparseView` は後続フェーズ
|
|
715
|
-
- BatchGet 最適化
|
|
716
|
-
- 並列 relation 実行(独立したサブクエリ、BatchGet チャンク、ネストした解決を上限付きの
|
|
717
|
-
並列度で同時にディスパッチ)
|
|
718
|
-
- Linter(no-scan, require-limit, query-boundary, GSI ambiguity, 拡張された design rule)
|
|
719
|
-
- ConsistentRead の型制約
|
|
720
|
-
- Transaction DSL
|
|
721
|
-
- Batch 操作
|
|
722
|
-
- CQRS Query/Command contract レイヤー(public IF, N+1 安全な合成, context 境界)—— [docs](./docs/cqrs-contract.md) を参照
|
|
723
|
-
- Mutation → command derivation: Command IF の背後にある内部的な write-plan 合成 DSL
|
|
724
|
-
(`entityWrites` + `mutation`)、複数 fragment の atomic 合成を1つの `TransactWriteItems` に
|
|
725
|
-
—— [docs](./docs/mutation-command-derivation.md) を参照
|
|
726
|
-
- Opt-in class hydration(`options.hydrate`): read 結果をホストオブジェクトにロードする;
|
|
727
|
-
ホスト専用で、シリアライズされない。Phase 1(`query` トップレベル)実装済み
|
|
728
|
-
—— [docs](./docs/class-hydration.md) を参照
|
|
729
|
-
- Multi-language: TS を SSoT とした Python コード生成器 + runtime、TS↔Python の conformance 付き
|
|
730
|
-
—— [docs](./docs/python-bridge.md) を参照
|
|
731
|
-
- In-memory test adapter(`graphddb/testing`): Docker 不要のユニットテスト —— [docs](./docs/testing.md) を参照
|
|
732
|
-
- CDC emulator: dev/test 向けの DynamoDB Streams 相当の変更イベント —— [docs](./docs/cdc-emulator.md) を参照
|
|
249
|
+
```mermaid
|
|
250
|
+
flowchart LR
|
|
251
|
+
M["TS Model (SSoT)"] --> RT["Runtime (TS)"]
|
|
252
|
+
M --> PY["Python client + runtime"]
|
|
253
|
+
M --> OA["OpenAPI"]
|
|
254
|
+
M --> CQ["CQRS Contract"]
|
|
255
|
+
```
|
|
733
256
|
|
|
734
257
|
## 📄 License
|
|
735
258
|
|