graphddb 0.4.1 → 0.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/README.md +339 -482
  2. package/package.json +1 -1
package/README.md CHANGED
@@ -1,62 +1,32 @@
1
1
  # GraphDDB
2
2
 
3
- **DynamoDB のための型安全な Graph Query Runtime**
3
+ **A type-safe Graph Query Runtime for DynamoDB**
4
4
 
5
- GraphDDB DynamoDB のネイティブなアクセスパターンを GraphQL ライクなクエリモデルにマッピングする。
6
- PKGSI、Projection、Relation、Cursor、そして操作の上限(limit)はすべて隠さず可視に保たれる。
5
+ GraphDDB maps DynamoDB's native access patterns onto a GraphQL-like query model.
6
+ PK / GSI / projection / relation / cursor / operation limits are all kept visible rather than hidden.
7
7
 
8
- GraphDDB ORM ではない。
9
- TypeScript の型を使って DynamoDB のアクセスパターンを定義・検証・実行するための Runtime である。
8
+ It is not an ORM. It is a Runtime for **defining, validating, and executing** DynamoDB access patterns through TypeScript types.
10
9
 
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
- ```
34
-
35
- Partition Key は Graph のエントリポイントであり、relation は明示的な traversal パスであり、
36
- projection は selection set である。GraphDDB はクエリ言語を発明しない —— よく設計された
37
- アクセスパターンを直接実行し、plan・limit・relation 解決を通じて曖昧さを可視化する。
10
+ Reads center on `query` / `list`, writes on `DDBModel.mutate` (a unified envelope), while
11
+ `putItem` / `updateItem` / `deleteItem` are positioned secondarily as raw base operations named after the DynamoDB API.
38
12
 
39
13
  ## 📦 Install
40
14
 
41
15
  ```bash
42
16
  npm install graphddb
43
- ```
44
-
45
- AWS SDK v3 は peer dependency である —— 使用するクライアントをインストールすること:
46
-
47
- ```bash
17
+ # AWS SDK v3 は peer dependency。使うクライアントを入れる:
48
18
  npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
49
19
  ```
50
20
 
51
- `tsx` optional peer dependency で、コード生成 CLI(`graphddb generate …`)を
52
- TypeScript の定義ファイルに対して直接実行する場合にのみ必要となる。ライブラリの利用者には
53
- 不要である(そのため esbuild がランタイムバンドルに混入することはない):
21
+ `tsx` is an optional peer dependency, needed only when running the code-generation CLI
22
+ (`graphddb generate …`) directly against TypeScript definitions. Library consumers do not need it
23
+ (esbuild keeps it out of the runtime bundle):
54
24
 
55
25
  ```bash
56
26
  npm install -D tsx
57
27
  ```
58
28
 
59
- Node.js ≥ 22 が必要。
29
+ Node.js ≥ 22 is required.
60
30
 
61
31
  ## 🚀 Quick Start
62
32
 
@@ -76,168 +46,40 @@ class UserModel extends DDBModel {
76
46
  }
77
47
  const User = UserModel.asModel();
78
48
 
79
- // DynamoDB クライアントは一度だけ接続する。GraphDDB が throttle / transient の retry
80
- // 所有するため(後述の "Retry & throttling" を参照)、クライアントは `maxAttempts: 1` に設定し、
81
- // 二重 retry(SDK × ライブラリ)を避けること。
49
+ // クライアントは一度だけ接続。GraphDDB が throttle / transient の retry を所有するため
50
+ // `maxAttempts: 1` にして二重 retry(SDK × ライブラリ)を避ける(後述 "Retry & throttling")。
82
51
  DDBModel.setClient(new DynamoDBClient({ maxAttempts: 1 }));
83
52
 
84
- // Read —— 結果の型に現れるのは select したフィールドのみ。
53
+ // READ —— 結果型に現れるのは select したフィールドのみ。
85
54
  const user = await User.query({ userId: 'alice' }, { name: true });
86
55
 
87
- // 単一アイテムの書き込み —— DynamoDB API にちなんで名付けられた raw base 操作。
88
- await User.putItem({ userId: 'alice', name: 'Alice' });
89
-
90
- // 複数アイテムの atomic な書き込み —— 統一 envelope(デフォルト `mode: 'transaction'`)。
56
+ // WRITE —— 複数アイテムを atomic に書く統一 envelope(デフォルト mode: 'transaction')。
91
57
  await DDBModel.mutate({
92
58
  user: { update: User, key: { userId: 'alice' }, input: { name: 'Alice B.' } },
93
59
  });
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
60
 
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
61
+ // 単一アイテムの raw な base 操作(DynamoDB API 名に倣う)。
62
+ await User.putItem({ userId: 'alice', name: 'Alice' });
221
63
  ```
222
64
 
223
- 独立した relation のサブクエリ(ここでは `members` `permissions`)は並列にディスパッチされる。
224
- 2番目のクエリの `belongsTo` 解決は N 件の lookup を1回の `BatchGetItem` にバッチ化する。
225
- 並列ディスパッチを示すタイムラインは [example](./examples/user-permissions/) を参照。
65
+ Cross-service contracts, the Python bridge, atomic mutation of multiple fragments, and more are
66
+ covered in the sections below and in [`docs/spec.md`](./docs/spec.md).
226
67
 
227
68
  ## 📖 Usage
228
69
 
229
- 以下のスニペットは [user-permissions](./examples/user-permissions/) の例を圧縮したものである。
70
+ The snippets below are condensed from the [user-permissions](./examples/user-permissions/) example.
230
71
 
231
72
  ### Entity Definition
232
73
 
233
- TypeScript クラスが単一の真実の源(single source of truth)である。Key GSI `k`
234
- タグ付きテンプレートの **segment** から構築され、key builder の入力型がそのまま
235
- クエリパラメータの型になる。
74
+ The TypeScript class is the single source of truth. Keys / GSIs are built from the **segments** of the
75
+ `k` tagged template, and the input type of the key builder becomes the type of the query parameters.
76
+ `@model({ description })` is an optional human-readable description (#154) that is pure documentation
77
+ metadata, propagated into the manifest and the generated Python docstrings.
236
78
 
237
79
  ```ts
238
80
  const TABLE = 'UserPermissions';
239
81
 
240
- @model({ table: TABLE, prefix: 'USER' })
82
+ @model({ table: TABLE, prefix: 'USER', description: 'アプリのユーザー' })
241
83
  class UserModel extends DDBModel {
242
84
  static readonly keys = key<{ userId: string }>((c) => ({
243
85
  pk: k`USER#${c.userId}`,
@@ -282,18 +124,27 @@ export const User = UserModel.asModel();
282
124
  export const GroupMembership = GroupMembershipModel.asModel();
283
125
  ```
284
126
 
285
- 複数 segment sort key segment の配列である(例: `sk: [k\`PERM#${c.resource}\`, k\`${c.action}\`]`)。
286
- partial key はマッチする segment 境界で `begins_with` にコンパイルされる。完全なキーモデルは
287
- [仕様書](./docs/spec.md) を参照。
127
+ A multi-segment sort key is an array of segments (e.g. `sk: [k\`PERM#${c.resource}\`, k\`${c.action}\`]`).
128
+ A partial key is compiled to `begins_with` at the matching segment boundary. For the complete key model,
129
+ see [`docs/spec.md`](./docs/spec.md).
130
+
131
+ Field decorators define the DynamoDB attribute type and catch mismatches with the declared TS type at compile time:
132
+
133
+ ```ts
134
+ @string userId!: string; // DynamoDB S
135
+ @number amount!: number; // DynamoDB N
136
+ @datetime createdAt!: Date; // ISO 8601 文字列として保存
137
+ @boolean isActive!: boolean; // DynamoDB BOOL
138
+ ```
288
139
 
289
- ### GraphQL-like Queries
140
+ ### Read: `query` / `list`
290
141
 
291
- 第1引数は「どうやってデータを見つけるか」を記述する。
292
- 第2引数は「何を返すか」を記述する。
293
- Runtime は与えられたフィールド名から、どのインデックスを使うかを解決する。
142
+ The first argument describes "how to find the data"; the second describes "what to return". The runtime
143
+ resolves which index (PK / GSI) to use from the given field names. The return type is inferred from
144
+ `select`, and only the requested fields appear in the result.
294
145
 
295
146
  ```ts
296
- // email でユーザーを取得し、group membership へ traverse する。
147
+ // email(unique GSI)でユーザーを取得し、group membership へ traverse する。
297
148
  const alice = await User.query(
298
149
  { email: 'alice@example.com' },
299
150
  {
@@ -305,24 +156,13 @@ const alice = await User.query(
305
156
  },
306
157
  },
307
158
  );
159
+ // alice: { name: string; groups: { items: { groupId; role }[]; cursor: string | null } }
308
160
 
309
161
  // 未定義のキーフィールド -> コンパイルエラー
310
162
  User.query({ foo: 'bar' }, { name: true });
311
163
  ```
312
164
 
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
165
+ `query` is for a **single** lookup of a PK or unique GSI only. For keys that may return multiple items, use `list` (with a cursor):
326
166
 
327
167
  ```ts
328
168
  const result = await GroupMembership.list(
@@ -330,17 +170,15 @@ const result = await GroupMembership.list(
330
170
  { userId: true, role: true },
331
171
  { limit: 20, after: cursor },
332
172
  );
333
- // result.items: GroupMembership[]
334
- // result.cursor: string | null
173
+ // result.items: GroupMembership[] ; result.cursor: string | null
335
174
  ```
336
175
 
337
- ### Filtering(サーバーサイド `filter`)
176
+ ### Filtering (server-side `filter`)
338
177
 
339
- `filter` は宣言的で型安全な条件であり、DynamoDB **`FilterExpression`** にコンパイルされ、
340
- **サーバーサイド** で評価される。AWS AppSync `ModelFilterInput` をモデルにしている:
341
- 裸の値は等価の省略記法、operator オブジェクトは比較を表現し、`and` / `or` / `not`
342
- 論理グループを構築する。**エンティティ全体** に対して型付けされるため、`select` **含まれない**
343
- 属性も参照できる。
178
+ `filter` is a declarative, type-safe condition that compiles to DynamoDB's **`FilterExpression`** and is
179
+ evaluated **server-side** (modeled on AWS AppSync's `ModelFilterInput`). A bare value is shorthand for
180
+ equality, an operator object is a comparison, and `and` / `or` / `not` form logical groups. Because it is
181
+ typed against the **entire entity**, it can reference attributes **not** included in `select`.
344
182
 
345
183
  ```ts
346
184
  const result = await Order.list(
@@ -358,74 +196,57 @@ const result = await Order.list(
358
196
  );
359
197
  ```
360
198
 
361
- Relation は対象モデルの `Model.relation(...)` を通じて同じ `filter` を取る。サーバーサイドの
362
- `FilterExpression` で表現できないロジックには、返された projection に対して
363
- `result.items.filter(...)` を呼ぶこと —— select フィールドの宣言済みスカラー型が保たれるため、
364
- `o.amount > 100` はキャスト不要である(組み込みの post-load 述語は存在しない)。
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>` 参照を使わなければならない。値はパラメータ化される:
199
+ Available operators: `eq` / `ne` / `ge` / `le` / `gt` / `lt` / `between` / `in` / `beginsWith` /
200
+ `contains` / `notContains` / `attributeExists` / `attributeType` / `size`. Operators are
201
+ **constrained by the declared type of the field** (`beginsWith` on a number, `gt: 'x'`, etc. are compile
202
+ errors). For the rare condition an operator cannot express, write it with the raw escape hatch `cond`
203
+ using `Model.col.<field>` (a refactor-safe column reference). All values are parameterized and column
204
+ names are aliased, so it is injection-safe:
375
205
 
376
206
  ```ts
377
207
  filter: cond`${Order.col.amount} > ${100} AND attribute_exists(${Order.col.status})`
378
208
  ```
379
209
 
380
- すべての値はパラメータ化され(`ExpressionAttributeValues`)、すべてのカラム名は
381
- エイリアス化される(`ExpressionAttributeNames`)—— リテラルの補間は存在しないため、
382
- コンパイルされた式はインジェクション安全であり、`KeyConditionExpression` /
383
- `ProjectionExpression` とは独立して付与される。
210
+ The same `cond` fragment works not only for read filters but also for write `condition`s (a `mutate`
211
+ route, `putItem` / `updateItem` / `deleteItem`, a transaction item, a public command).
384
212
 
385
- 同じ `cond` fragment read filter としてだけでなく、書き込みの `condition`
386
- (`putItem` / `updateItem` / `deleteItem`、transaction item、public command に対して)
387
- としても受け付けられる。
213
+ > **RCU note:** `FilterExpression` does **not** reduce read capacity —— DynamoDB reads the keys first
214
+ > and filters afterward. `limit` is applied **before** the filter, so a single page may return fewer
215
+ > than `limit`, or an entire page may be excluded, yielding an empty page with a non-null cursor.
216
+ > Do efficient narrowing through key design and use `filter` for correctness.
388
217
 
389
- > **RCU に関する注意:** `FilterExpression` read capacity を **削減しない** ——
390
- > DynamoDB はまずマッチするキーを読み、その後に filter する。`limit` は filter の
391
- > **前** に適用されるため、1ページが `limit` **未満** のアイテムを返すことがある
392
- > (そしてページ全体が filter で除外された場合、null でない cursor を持つ空ページになる)。
393
- > 効率的な絞り込みのためにキーを設計し、`filter` は正しさのために使うこと。
218
+ ### Write: `DDBModel.mutate` (unified envelope)
394
219
 
395
- ### Writes
220
+ The primary write path is `DDBModel.mutate`. It is an **alias-map envelope** that runs multiple write
221
+ routes in a single call —— the in-process counterpart of a GraphQL operation. Each alias is a named
222
+ route, and the result is keyed by the same alias. Lifecycle-aware writes (conditional gates, read-back,
223
+ referential side effects) go through here.
396
224
 
397
- raw base テーブルの書き込みは `putItem` / `updateItem` / `deleteItem` である ——
398
- ライフサイクルのセマンティクスを持たないプリミティブな単一アイテム操作:
225
+ A write route descriptor is `{ create | update | remove: Model, key, input?, condition?, result? }`:
399
226
 
400
227
  ```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
- });
413
-
414
- await User.updateItem({ userId: 'alice' }, { status: 'disabled' });
228
+ const res = await DDBModel.mutate({
229
+ a: { create: GroupMembership, key: { groupId: 'eng', userId: 'u1' }, input: { role: 'admin' } },
230
+ b: { update: User, key: { userId: 'u1' }, input: { name: 'Ann' }, condition: { status: 'active' }, result: { select: { name: true } } },
231
+ }, { mode: 'transaction' });
415
232
  ```
416
233
 
417
- ライフサイクルを意識した単一書き込み —— 条件ゲート、read-back、参照整合的な副作用 ——
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 の並列フィールド解決とまったく同じである:
234
+ - **`key`** is a single object, or an **array of objects** (key-array bulk).
235
+ - **`condition`** is the write gate of `WriteCondition` —— the same declarative operator subset as the
236
+ read filter (`eq` / `ne` / comparisons / `between` / `in` / string operations / `size` / `attributeType`
237
+ + `and` / `or` / `not`), the existence primitives (`notExists` / `attributeExists` / `attributeNotExists`),
238
+ or a raw `cond` fragment.
239
+ - **`result: { select, options? }`** reads back the written item. Omit it for void.
240
+ - **`mode: 'transaction'`** (default) is a single atomic `TransactWriteItems`: all-or-nothing, throws on
241
+ failure, a map of more than 25 items is an error (it is not split). Equivalent to GraphQL's
242
+ all-or-nothing contract.
243
+ - **`mode: 'parallel'`** is a non-atomic `BatchWriteItem` (chunking + `UnprocessedItems` retry): each
244
+ alias reports partial success/failure as `{ ok }` | `{ error }`. Independent operations run in parallel;
245
+ dependencies are ordered via `$.alias.field` references.
246
+
247
+ The read side has a symmetric `DDBModel.query(map)`. Each read route
248
+ (`{ query | list: Model, key, select, options? }`) runs **independently and in parallel**, with no
249
+ consistency across routes (just like GraphQL's parallel field resolution):
429
250
 
430
251
  ```ts
431
252
  const { user, members } = await DDBModel.query({
@@ -435,44 +256,60 @@ const { user, members } = await DDBModel.query({
435
256
  // user: Item | null ; members: { items, cursor }
436
257
  ```
437
258
 
438
- read route の記述子は `{ query | list: Model, key, select, options? }` である
439
- (`query` / `list` のいずれか一方)。`query` の `options` は
440
- `{ consistentRead?, maxDepth? }`、`list` の場合は `{ limit?, after?, order?, filter? }` である。
259
+ ### Raw base operations: `putItem` / `updateItem` / `deleteItem`
441
260
 
442
- `DDBModel.mutate(map, { mode })` write route を実行する。write route の記述子は
443
- `{ create | update | remove: Model, key, input?, condition?, result? }` である:
261
+ `putItem` / `updateItem` / `deleteItem` are **raw single-item operations** named after the DynamoDB API,
262
+ primitives with no lifecycle semantics. They carry no atomic composition of multiple fragments and no
263
+ derived side effects, unlike `mutate`:
444
264
 
445
265
  ```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' });
266
+ await User.putItem({ userId: 'alice', name: 'Alice', email: 'alice@example.com', status: 'active' });
267
+ await User.updateItem({ userId: 'alice' }, { status: 'disabled' });
268
+ await User.deleteItem({ userId: 'alice' });
450
269
  ```
451
270
 
452
- - **`key`** は単一のオブジェクト、または **オブジェクトの配列**(key-array bulk)である。
453
- transaction は `TransactWriteItems` にコンパイルされ、parallel モードは
454
- `BatchWriteItem`(チャンク分割され、`UnprocessedItems` retry 付き)にコンパイルされる。
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 である。
271
+ If you need conditional gates, read-back, or referential side effects, use `DDBModel.mutate` rather than the raw primitives.
272
+
273
+ ### Relation Traversal
274
+
275
+ A relation is a query bound to the value of the parent entity. Declare it with `@hasMany` / `@hasOne` /
276
+ `@belongsTo` and traverse it by nesting inside `select`. `hasMany` / `list` require a default / max limit.
277
+ Independent relation subqueries are dispatched in parallel, and N `belongsTo` resolutions are aggregated
278
+ into a single `BatchGetItem` (no N+1):
279
+
280
+ ```ts
281
+ // email でユーザーを探し、所属グループ -> 各グループ本体 -> そのパーミッションへ traverse。
282
+ const alice = await User.query(
283
+ { email: 'alice@example.com' },
284
+ {
285
+ userId: true,
286
+ name: true,
287
+ groups: {
288
+ select: {
289
+ groupId: true,
290
+ role: true,
291
+ group: { // membership -> Group (belongsTo, BatchGet)
292
+ select: {
293
+ name: true,
294
+ permissions: { // group -> permissions (hasMany)
295
+ select: { resource: true, action: true },
296
+ filter: { resource: { attributeExists: true } },
297
+ },
298
+ },
299
+ },
300
+ },
301
+ limit: 20,
302
+ },
303
+ },
304
+ { maxDepth: 3 }, // traversal はデフォルト depth=1。深い traversal は明示許可が必要。
305
+ );
306
+ ```
471
307
 
472
308
  ### Inspect Execution Plans
473
309
 
474
- `explain()` は、実行される前の DynamoDB 操作を表示する。
475
- `select` `ProjectionExpression` に変換されるため、要求した属性のみが読まれる。
310
+ `explain()` returns the DynamoDB operations before they run. `select` is translated into a
311
+ `ProjectionExpression`, so only the requested attributes are read. The plan can be used for testing,
312
+ debugging, and RCU estimation:
476
313
 
477
314
  ```ts
478
315
  const plan = GroupMembership.explain(
@@ -484,115 +321,186 @@ const plan = GroupMembership.explain(
484
321
  // type: "Query",
485
322
  // tableName: "UserPermissions",
486
323
  // keyCondition: { PK: "GROUP#eng" },
487
- // rangeCondition: {
488
- // operator: "begins_with",
489
- // key: "SK",
490
- // value: "USER#"
491
- // },
492
- // limit: 10
324
+ // rangeCondition: { operator: "begins_with", key: "SK", value: "USER#" },
325
+ // limit: 10,
493
326
  // }]
494
327
  // }
495
328
  ```
496
329
 
497
- partial GSI キーマッチの場合、SK 条件は `rangeCondition` として分離される:
330
+ For a partial GSI key match, the SK condition is separated out as a `rangeCondition`
331
+ (`keyCondition: { GSI1PK: "USER#alice" }`, `indexName: "GSI1"`, `rangeCondition: { begins_with GSI1SK "GROUP#" }`).
498
332
 
499
- ```ts
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
- ```
333
+ ### Maintained Access Paths (0.4.x / Epic #118)
334
+
335
+ Declaring a relation or scalar as a **maintained access path** keeps a *separate* row in sync with the
336
+ lifecycle of the source entity —— and all of it is composed into the same atomic transaction as the source write.
518
337
 
519
- ## 🧠 Core Concepts
338
+ - **`pattern` on `@hasMany` / `@hasOne`** —— `embeddedSnapshot` (keeping a projected snapshot / bounded
339
+ collection synchronized onto the owner row), versioned, sparse, etc. Declare the read shape with `read`,
340
+ the cross-entity trigger with `write.maintainedOn`, and the attributes to capture with `projection`:
520
341
 
521
- ### Entity = Access Pattern
342
+ ```ts
343
+ @hasMany(() => PostModel, { threadId: 'threadId' }, {
344
+ pattern: 'embeddedSnapshot',
345
+ read: { maxItems: 3, order: 'DESC' },
346
+ write: { maintainedOn: ['Post.created'] },
347
+ projection: { postId: 'postId', textPreview: preview('$.entity.body', 120) },
348
+ })
349
+ latestPosts!: PostModel[];
350
+ ```
522
351
 
523
- Entity ORM モデルではない。
524
- DynamoDB に対する、境界付けられたアクセス表面(bounded access surface)である。
352
+ - **`@aggregate` counter** —— keeps a scalar aggregate (`count()`, etc.) synchronized with an atomic `ADD ±1`:
525
353
 
526
- Entity は、サービス境界内でデータにアクセスする、サポートされたすべての方法を定義する:
354
+ ```ts
355
+ @aggregate(() => ThreadPostModel, { threadId: 'threadId' }, {
356
+ pattern: 'counter',
357
+ value: count(),
358
+ write: { maintainedOn: ['ThreadPost.created', 'ThreadPost.removed'] },
359
+ })
360
+ postCount!: number;
361
+ ```
527
362
 
528
- - 物理キー
529
- - GSI
530
- - relation
531
- - projection
532
- - traversal ルール
363
+ - **`@model({ kind })` + `@maintainedFrom`** —— declare a view as *its own model* and bind each source
364
+ with a class-level `@maintainedFrom` (`materializedView` / `sparseView`).
533
365
 
534
- 物理的には、Entity DynamoDB item にマップされる。
535
- アーキテクチャ的には、サービスのその部分に許可されたアクセス表面を定義する。
366
+ Synchronized maintenance (`updateMode: 'mutation'`) works in both TS and Python (conformance-verified).
367
+ `stream` / `max()` / `materializedView` / `sparseView` are later phases. For details and the pattern
368
+ mapping table, see [`docs/design-patterns.md`](./docs/design-patterns.md). Examples:
369
+ [embedded-snapshot-pattern](./examples/embedded-snapshot-pattern/) /
370
+ [aggregate-counter](./examples/aggregate-counter/).
536
371
 
537
- ### Key / GSI = Structured Segments
372
+ ### Middleware / Hooks (`DDBModel.use`)
538
373
 
539
- `key()` `gsi()` `k` タグ付きテンプレートの **segment** からキーを構築する。builder
540
- 入力型がキー解決に必要なフィールドを定義し、同時にクエリパラメータの型にもなる。
374
+ You can register **host-side middleware** that runs at fixed points in reads / writes. This handles
375
+ cross-cutting concerns —— logging, metrics, tracing, tenant / authorization scoping, soft-delete,
376
+ result post-processing —— without embedding them in queries or model definitions. It is host-only, never
377
+ touches the planner or the bridge SSoT, and is **not serialized**:
541
378
 
542
379
  ```ts
543
- // 1つの定義が2つの役割を果たす:
544
- // 1. 書き込みパス: Entity 保存時に PK/SK を生成
545
- // 2. クエリパス: User.query({ email: ... }) のパラメータ型を定義
546
- static readonly emailIndex = gsi<{ email: string }>(
547
- 'GSI1',
548
- (c) => ({ pk: k`EMAIL#${c.email}`, sk: k`PROFILE` }),
549
- { unique: true },
550
- );
380
+ import { DDBModel } from 'graphddb';
381
+
382
+ DDBModel.use({
383
+ name: 'tenant-scope',
384
+ read: { before(ctx) { /* ctx を観測 / 変更、throw でキャンセル */ } },
385
+ write: { before(ctx) { /* ctx.input / ctx.kind を変更可 */ } },
386
+ });
387
+ DDBModel.clearMiddleware(); // 全解除(テスト teardown 等)
551
388
  ```
552
389
 
553
- キーは不透明な文字列ではなく構造化された segment であるため、partial key —— sort-key
554
- segment の先頭プレフィックスのみを与えたもの —— はその segment 境界で `begins_with`
555
- コンパイルされる。これが relation prefix クエリの解決方法である。
390
+ `before*` runs in registration order (FIFO), `after*` / `onError` in reverse order (LIFO), so the
391
+ first-registered middleware is the outermost (onion nesting without `next()`). Request-scoped values
392
+ (tenant / actor / trace id) become visible from `ctx.context` when you pass `{ context }` to any
393
+ read / write. Hooks can observe, mutate `ctx`, cancel by throwing, and recover in `onError`. For the
394
+ full set of hook points (read R1–R5 / write W1–W5), see [`docs/middleware.md`](./docs/middleware.md).
395
+
396
+ ### Retry & throttling
556
397
 
557
- ### Relation = Bound Query
398
+ GraphDDB retries throttle / transient errors for **all** single-item operations
399
+ (`GetItem` / `Query` / `PutItem` / `UpdateItem` / `DeleteItem`), the fan-out of relation reads, and
400
+ `TransactWriteItems`, with **no configuration required**. The default is exponential backoff with full
401
+ jitter (`min(1000, 50·2^(n-1))` ms), up to 10 attempts, for throttle / 5xx / transient only
402
+ (`ValidationException` and condition-check failures are never retried).
558
403
 
559
- Relation は、親 Entity の値にバインドされたクエリである。
560
- 同じ解決ロジックが適用される: runtime はフィールド名を Key / GSI 定義に照合する。
404
+ > This is the library's own **error retry** and is distinct from the partial-batch `UnprocessedKeys` /
405
+ > `UnprocessedItems` retry that `batchGet` / `batchWrite` already perform (they use the same backoff
406
+ > ramp but are independent).
407
+
408
+ **Set the client to `maxAttempts: 1`.** Since the library owns retries, leaving the SDK at its default
409
+ (`maxAttempts: 3`) would *multiply* the retries. The policy can be tuned globally or per call:
561
410
 
562
411
  ```ts
563
- @hasMany(() => GroupMembershipModel, { userId: 'userId' }, {
564
- limit: { default: 20, max: 100 },
565
- })
566
- groups!: GroupMembershipModel[];
412
+ DDBModel.setRetryPolicy({ maxAttempts: 5, jitter: 'full' }); // null でデフォルトに戻る
413
+ await User.putItem({ userId: 'alice', name: 'Alice' }, { retry: false });
414
+ await User.query({ userId: 'alice' }, { name: true }, { retry: { maxAttempts: 3 } });
567
415
  ```
568
416
 
569
- ### Field Decorators
417
+ ## 👥 Example: User Permissions
570
418
 
571
- 型安全なセマンティック decorator DynamoDB の属性型を定義する。
572
- `ClassFieldDecoratorContext` に基づくコンパイル時チェックが、宣言された TypeScript の型と
573
- DynamoDB の型の不一致を検出する。
419
+ [user-permissions](./examples/user-permissions/) models users, groups, memberships, and permissions in a
420
+ single DynamoDB table. The table is physical, but the query surface has the shape of a graph:
574
421
 
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
422
+ ```text
423
+ UserPermissions table
424
+
425
+ Entity PK SK GSI1PK GSI1SK
426
+ Group GROUP#<groupId> META
427
+ User USER#<userId> PROFILE EMAIL#<email> PROFILE
428
+ GroupMembership GROUP#<groupId> USER#<userId> USER#<userId> GROUP#<groupId>
429
+ Permission GROUP#<groupId> PERM#<resource>#<action>
580
430
  ```
581
431
 
582
- ### Select = Projection + Type Inference
432
+ `GroupMembership` is an adjacency-list pattern: `PK/SK` reads a group's members, and `GSI1PK/GSI1SK`
433
+ reads a user's groups. The same physical table supports both directions without hiding the keys.
434
+ Fetching a group, its members, and its permissions in a single `query` expands into a small, inspectable plan:
435
+
436
+ ```mermaid
437
+ flowchart LR
438
+ Q(["Group.query<br/>groupId: eng"]) --> S1["1 Query<br/>PK=GROUP#eng, SK=META<br/>→ Group"]
439
+ Q --> S2["2 Query<br/>PK=GROUP#eng, SK begins_with USER#<br/>→ members"]
440
+ Q --> S3["3 Query<br/>PK=GROUP#eng, SK begins_with PERM#<br/>→ permissions"]
441
+ S2 -.並列ディスパッチ.- S3
442
+ ```
443
+
444
+ The independent relation subqueries (`members` and `permissions`) are dispatched in parallel. For a
445
+ timeline illustrating the parallel dispatch, see the [example](./examples/user-permissions/).
446
+
447
+ ## 🤔 Design Philosophy (in brief)
448
+
449
+ DynamoDB is fast but easy to misdesign. Teams often treat it like an RDBMS (one table per entity,
450
+ in-app joins, access patterns hidden behind an ORM abstraction) and end up with unstable performance,
451
+ unnecessary GSIs, and unclear service boundaries. GraphDDB does the opposite: it expresses access
452
+ patterns **directly in code** and keeps PK / GSI, projection, relation, cursor, and limits visible.
453
+ This makes the correct design the easiest path to take while writing code.
583
454
 
584
- `select` オブジェクトは DynamoDB `ProjectionExpression` 生成と TypeScript の返り値型推論の
585
- 両方を制御する。
586
- 要求したフィールドだけが返り、結果の型にもそのフィールドだけが現れる。
455
+ At its core is the natural correspondence between GraphQL's query model and DynamoDB's access patterns:
587
456
 
588
- ### Planner = Inspectable Execution Plan
457
+ ```text
458
+ GraphQL Query Model DynamoDB
459
+ arguments ----> PK / GSI lookup
460
+ selection ----> ProjectionExpression
461
+ relation ----> Query / Get / BatchGet
462
+ connection ----> Cursor pagination
463
+ complexity ----> Operation limits
464
+ ```
465
+
466
+ - **Entity = access surface** —— not an ORM model, but the set of all supported ways to access data within
467
+ a service boundary (physical keys / GSIs / relations / projection / traversal rules).
468
+ - **Key / GSI = structured segments** —— built from `k` tag segments. The builder's input type doubles as
469
+ the query parameter type, and a partial key compiles to `begins_with` at a segment boundary.
470
+ - **Relation = bound query** —— a query bound to the parent's value. The same key/GSI resolution logic applies.
471
+ - **Select = projection + type inference** —— only the requested fields are returned, and only those
472
+ fields appear in the result type.
473
+ - **Planner = inspectable execution plan** —— a plan is produced before touching DynamoDB
474
+ (testing / debugging / RCU estimation).
589
475
 
590
- DynamoDB にアクセスする前に、runtime が実行計画(execution plan)を生成する。
591
- plan はテスト・デバッグ・RCU 見積もりに利用できる。
476
+ Relation traversal follows only the access paths defined on the Entity. Undefined relations, missing GSIs,
477
+ limit-less lists, and traversals exceeding the allowed depth are checked before execution. The same
478
+ architectural constraints apply whether the code was written by a human or AI-generated.
592
479
 
593
- ## 🏗️ Architecture
480
+ ### Design Rules
594
481
 
595
- GraphDDB のクエリは 5 つのレイヤーを通って実行される:
482
+ | Rule | Description |
483
+ |------|-------------|
484
+ | Service Boundary First | A single table represents one service boundary, not one entity. |
485
+ | Access Pattern First | Model the access patterns before tables or ORM-style entities. |
486
+ | Natural Mapping | Map Query / Get / BatchGet / Projection / cursor onto a GraphQL-like shape. |
487
+ | Inspectable Plans | DynamoDB operations and execution plans are inspectable before execution. |
488
+ | Single-item query | `query` is for PK or unique GSI lookups only. Use `list` for keys that return multiple items. |
489
+ | Explicit limits | `hasMany` and `list` require a default / max limit. |
490
+ | Bounded depth | Traversal defaults to depth=1. Deeper traversal must be explicitly allowed with `maxDepth`. |
491
+ | Parallel relations | Independent relation subqueries, BatchGet chunks, and nested resolutions are dispatched in parallel (bounded). |
492
+ | No Scan | Full table scans are forbidden. The linter detects and blocks them. |
493
+ | Partial select | Query results are partial types. Only the selected fields appear in the result type. |
494
+ | Structured keys | Keys are built from `k` segment templates. A partial key compiles to `begins_with` at a segment boundary. |
495
+ | Typed consistentRead | `consistentRead` is for PK queries only. GSI queries are rejected at compile time. |
496
+ | Server-side filter | `filter` is a declarative `FilterExpression` (compatible with AppSync `ModelFilterInput`). It does not reduce RCU, and `limit` is applied before it. |
497
+
498
+ The linter detects problems in Entity definitions before runtime (use of Scan, missing limits on
499
+ `hasMany` / `list`, ambiguous GSI resolution, query/list boundary violations against a unique GSI).
500
+
501
+ ### Architecture
502
+
503
+ A query executes through 5 layers:
596
504
 
597
505
  ```mermaid
598
506
  flowchart TD
@@ -609,127 +517,76 @@ flowchart TD
609
517
  L5 -->|Partial Entity インスタンス| OUT(["✅ 型付き結果"])
610
518
  ```
611
519
 
612
- ## 📐 Design Rules
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
520
  ## 🧱 Beyond the Core
650
521
 
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 相当の変更イベント。
522
+ The query / relation / write APIs above are the core layer. GraphDDB additionally provides:
523
+
524
+ - **[CQRS contracts](./docs/cqrs-contract.md)** —— a public Query/Command contract model with an N+1-safe
525
+ cardinality matrix, composition across contracts, and enforcement of context boundaries.
526
+ - **[Mutation → command derivation](./docs/mutation-command-derivation.md)** —— the internal write-plan
527
+ composition DSL behind the public Command IF. Declare a model's write semantics (`entityWrites`) once,
528
+ and `mutation` composes multiple fragments while the compiler merges them into a single atomic
529
+ `TransactWriteItems`.
530
+ - **[Class hydration](./docs/class-hydration.md)** —— load read results into host-language domain objects
531
+ via the `options.hydrate` factory (host-only, non-serialized; Phase 1 = `query` top level implemented).
532
+ - **[Multi-language / Python bridge](./docs/python-bridge.md)** —— generate a Python client + runtime with
533
+ TS as the SSoT, kept in step by a TS↔Python conformance suite.
534
+ - **[In-memory testing](./docs/testing.md)** —— `graphddb/testing` runs model-mapping / query-plan /
535
+ relation-traversal / CDC tests in-process without Docker.
536
+ - **[CDC emulator](./docs/cdc-emulator.md)** —— change events equivalent to DynamoDB Streams, to drive and
537
+ test differential aggregation locally.
677
538
 
678
539
  ## 📚 Documentation
679
540
 
680
- | ドキュメント | 内容 |
541
+ | Document | Description |
681
542
  |----------|-------------|
682
- | [Specification](./docs/spec.md) | コア API: entity、structured keys/GSIsquery/filter、relation、batch/transactiondesign rule、runtime の挙動。 |
683
- | [Design patterns](./docs/design-patterns.md) | DynamoDB の代表的な 10 設計パターン(RFC #118 §1)を graphddb の機能にマッピング: same-partition / embedded snapshot / edge / materialized view / sparse view / aggregate counter / versioned / reverse lookup / external projection。各パターンの用途・宣言方法(`pattern` / `@model({ kind })` + `@maintainedFrom` / `@hasOne`・`@hasMany` の versioned pattern / `@aggregate`)・read & write 維持の挙動・Phase 別の対応状況。 |
684
- | [CQRS contract layer](./docs/cqrs-contract.md) | public Query/Command contract、cardinality matrix、N+1 安全性、contract をまたぐ合成、context 境界。 |
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 とのライブラリ比較。 |
543
+ | [Specification](./docs/spec.md) | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. |
544
+ | [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). |
545
+ | [Middleware / Hooks](./docs/middleware.md) | The `DDBModel.use` host-side middleware: hook points read R1–R5 / write W1–W5, `{ context }`, ordering. |
546
+ | [CQRS contract layer](./docs/cqrs-contract.md) | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. |
547
+ | [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`). |
548
+ | [Class hydration](./docs/class-hydration.md) | Opt-in `options.hydrate` that loads read results into host objects (Phase 1: `query` top level). |
549
+ | [Multi-language / Python bridge](./docs/python-bridge.md) | Code generation with TS as the SSoT and a Python runtime; TS↔Python conformance. |
550
+ | [In-memory test adapter](./docs/testing.md) | Docker-free in-process testing with `graphddb/testing` and `MemoryInspector`. |
551
+ | [CDC emulator](./docs/cdc-emulator.md) | A change-event emulator for differential aggregation patterns (dev/test). |
552
+ | [Benchmark](./benchmark/RESULTS.md) | Library comparison against hand-written AWS SDK, ElectroDB, DynamoDB Toolbox, and OneTable. |
691
553
 
692
554
  ## 💡 Examples
693
555
 
694
- | Example | 内容 |
556
+ | Example | Description |
695
557
  |---------|-------------|
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` を同期維持する。 |
558
+ | [user-permissions](./examples/user-permissions/) | Manages users, groups, and permissions with Single Table Design. Includes adjacency lists, inverted indexes, relation traversal, and `explain`. |
559
+ | [aggregate-tree-pattern](./examples/aggregate-tree-pattern/) | Differential tree aggregation (`siteScoreAverage`) driven by the CDC emulator: dirty propagation, throttled sweeps, recomputation. |
560
+ | [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. |
561
+ | [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. |
700
562
 
701
563
  ## ✨ Features
702
564
 
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) を参照
565
+ - Entity metadata / decorators (`@model({ description })` propagates into the manifest / Python docstrings)
566
+ - DDBModel base class / connection management
567
+ - Type system (SelectableOf, QueryResult, QueryKeyOf)
568
+ - Read: `query` (PK / unique GSI single lookup) / `list` (cursor pagination), planner / executor / hydrator
569
+ - Write: `DDBModel.mutate` unified envelope (transaction / parallel, atomic composition of multiple fragments,
570
+ key-array bulk, `result` read-back) as the primary path, with raw `putItem` / `updateItem` / `deleteItem`
571
+ as secondary base operations
572
+ - Type-safe filtering: declarative server-side `filter` (FilterExpression / AppSync `ModelFilterInput`
573
+ compatible), `Model.col` column references and the `cond` raw escape hatch (shared by read filter / write `condition`)
574
+ - Structured segment keys (`k` tag): a partial key compiles to `begins_with` at a segment boundary
575
+ - Explainable execution plans (`explain`)
576
+ - Relations (hasMany, belongsTo, hasOne, depth limits, parallel traversal, N+1-safe BatchGet)
577
+ - Maintained access paths (Epic #118): `pattern: 'embeddedSnapshot'` relations, `@aggregate` `count()` counters
578
+ (atomic `ADD ±1`), `@model({ kind })` + `@maintainedFrom`. Synchronized maintenance (`updateMode: 'mutation'`)
579
+ works in both TS / Python (conformance-verified). stream / `max()` / `materializedView` / `sparseView` are later
580
+ - Middleware / Hooks (`DDBModel.use`): host-side hooks read R1–R5 / write W1–W5, `{ context }` —— non-serialized
581
+ - Retry / throttling (single op / fan-out / transaction, full-jitter exponential backoff, global / per-call tuning)
582
+ - Linter (no-scan, require-limit, query-boundary, GSI ambiguity)
583
+ - Typed constraints on ConsistentRead, the Transaction DSL, Batch operations
584
+ - CQRS Query/Command contract layer —— [docs](./docs/cqrs-contract.md)
585
+ - Mutation → command derivation (`entityWrites` + `mutation`) —— [docs](./docs/mutation-command-derivation.md)
586
+ - Opt-in class hydration (`options.hydrate`, Phase 1 implemented) —— [docs](./docs/class-hydration.md)
587
+ - Multi-language: a Python code generator + runtime with TS as the SSoT, TS↔Python conformance —— [docs](./docs/python-bridge.md)
588
+ - In-memory test adapter (`graphddb/testing`) —— [docs](./docs/testing.md)
589
+ - CDC emulator —— [docs](./docs/cdc-emulator.md)
733
590
 
734
591
  ## 📄 License
735
592
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "graphddb",
3
- "version": "0.4.1",
3
+ "version": "0.4.2",
4
4
  "description": "Graph data modeling on DynamoDB with adjacency list pattern",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",