graphddb 0.1.1 → 0.2.1

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 CHANGED
@@ -37,6 +37,64 @@ A partition key is a graph entry point, a relation an explicit traversal path, a
37
37
  selection set. GraphDDB does not invent a query language — it executes well-designed access
38
38
  patterns directly and makes ambiguity visible through plans, limits, and relation resolution.
39
39
 
40
+ ## Install
41
+
42
+ ```bash
43
+ npm install graphddb
44
+ ```
45
+
46
+ The AWS SDK v3 is a peer dependency — install the clients you use:
47
+
48
+ ```bash
49
+ npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb
50
+ ```
51
+
52
+ `tsx` is an optional peer dependency, needed only to run the code-generation CLI
53
+ (`graphddb generate …`) directly against TypeScript definition files. Library
54
+ consumers don't need it (so it never pulls esbuild into your runtime bundle):
55
+
56
+ ```bash
57
+ npm install -D tsx
58
+ ```
59
+
60
+ Requires Node.js ≥ 22.
61
+
62
+ ## Quick Start
63
+
64
+ ```ts
65
+ import { DDBModel, model, string, key, k } from 'graphddb';
66
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
67
+
68
+ @model({ table: 'AppTable', prefix: 'USER' })
69
+ class UserModel extends DDBModel {
70
+ static readonly keys = key<{ userId: string }>((c) => ({
71
+ pk: k`USER#${c.userId}`,
72
+ sk: k`PROFILE`,
73
+ }));
74
+
75
+ @string userId!: string;
76
+ @string name!: string;
77
+ }
78
+ const User = UserModel.asModel();
79
+
80
+ // Wire up the DynamoDB client once (configure retries/region on the client itself).
81
+ DDBModel.setClient(new DynamoDBClient({}));
82
+
83
+ // Read — only the selected fields appear in the result type.
84
+ const user = await User.query({ userId: 'alice' }, { name: true });
85
+
86
+ // Single-item write — raw base op, named after the DynamoDB API.
87
+ await User.putItem({ userId: 'alice', name: 'Alice' });
88
+
89
+ // Multi-item atomic write — the unified envelope (default `mode: 'transaction'`).
90
+ await DDBModel.mutate({
91
+ user: { update: User, key: { userId: 'alice' }, input: { name: 'Alice B.' } },
92
+ });
93
+ ```
94
+
95
+ For cross-service contracts, the Python bridge, multi-fragment atomic mutations,
96
+ and more, see the sections below and [`docs/spec.md`](./docs/spec.md).
97
+
40
98
  ## User Permissions Example
41
99
 
42
100
  The [user-permissions](./examples/user-permissions/) example models users, groups, memberships, and permissions in a single DynamoDB table.
@@ -217,11 +275,8 @@ The return type is inferred from `select`:
217
275
  ```ts
218
276
  const result = await GroupMembership.list(
219
277
  { groupId: 'eng' },
220
- {
221
- select: { userId: true, role: true },
222
- limit: 20,
223
- after: cursor,
224
- },
278
+ { userId: true, role: true },
279
+ { limit: 20, after: cursor },
225
280
  );
226
281
  // result.items: GroupMembership[]
227
282
  // result.cursor: string | null
@@ -238,8 +293,8 @@ logical groups. It is typed against the **full entity**, so it can reference att
238
293
  ```ts
239
294
  const result = await Order.list(
240
295
  { userId: 'u001' },
296
+ { orderId: true }, // amount / status need not be projected
241
297
  {
242
- select: { orderId: true }, // amount / status need not be projected
243
298
  filter: {
244
299
  status: 'confirmed', // #status = :v (equality shorthand)
245
300
  amount: { gt: 100 }, // #amount > :v
@@ -275,6 +330,10 @@ are aliased (`ExpressionAttributeNames`) — there is no literal interpolation,
275
330
  the compiled expression is injection-free, and it is attached independently of
276
331
  the `KeyConditionExpression` / `ProjectionExpression`.
277
332
 
333
+ The same `cond` fragment is also accepted as a write `condition` (on
334
+ `putItem` / `updateItem` / `deleteItem`, transaction items, and public commands),
335
+ not just as a read filter.
336
+
278
337
  > **RCU note:** A `FilterExpression` does **not** reduce read capacity —
279
338
  > DynamoDB reads matching keys first, then filters. `limit` is applied
280
339
  > **before** the filter, so a single page may return **fewer** than `limit`
@@ -283,23 +342,84 @@ the `KeyConditionExpression` / `ProjectionExpression`.
283
342
 
284
343
  ### Writes
285
344
 
345
+ Raw base-table writes are `putItem` / `updateItem` / `deleteItem` — the primitive
346
+ single-item operations with no lifecycle semantics:
347
+
286
348
  ```ts
287
- await User.put({
349
+ await User.putItem({
288
350
  userId: 'alice',
289
351
  name: 'Alice',
290
352
  email: 'alice@example.com',
291
353
  status: 'active',
292
354
  });
293
355
 
294
- await GroupMembership.put({
356
+ await GroupMembership.putItem({
295
357
  groupId: 'eng',
296
358
  userId: 'alice',
297
359
  role: 'admin',
298
360
  });
299
361
 
300
- await User.update(alice, { status: 'disabled' });
362
+ await User.updateItem({ userId: 'alice' }, { status: 'disabled' });
363
+ ```
364
+
365
+ Lifecycle-aware single writes — conditional gates, read-back, referential
366
+ effects — go through `DDBModel.mutate`, not the raw primitives (see
367
+ [The in-process unified envelope](#the-in-process-unified-envelope) below).
368
+
369
+ ### The in-process unified envelope
370
+
371
+ `DDBModel.query` and `DDBModel.mutate` take a single **alias-map envelope** that
372
+ runs multiple routes in one call. This is the in-process analog of a GraphQL
373
+ operation: each alias is a named route, and the result is keyed by the same
374
+ aliases.
375
+
376
+ `DDBModel.query(map)` runs each read route independently and in parallel — there
377
+ is no cross-route consistency, exactly like GraphQL's parallel field resolution:
378
+
379
+ ```ts
380
+ const { user, members } = await DDBModel.query({
381
+ user: { query: User, key: { userId: 'u1' }, select: { name: true } },
382
+ members: { list: GroupMembership, key: { groupId: 'eng' }, select: { role: true }, options: { limit: 20 } },
383
+ });
384
+ // user: Item | null ; members: { items, cursor }
385
+ ```
386
+
387
+ A read route descriptor is `{ query | list: Model, key, select, options? }`
388
+ (exactly one of `query` / `list`). `options` for `query` is
389
+ `{ consistentRead?, maxDepth? }`; for `list` it is `{ limit?, after?, order?, filter? }`.
390
+
391
+ `DDBModel.mutate(map, { mode })` runs write routes. A write route descriptor is
392
+ `{ create | update | remove: Model, key, input?, condition?, result? }`:
393
+
394
+ ```ts
395
+ const res = await DDBModel.mutate({
396
+ a: { create: GroupMembership, key: { groupId: 'eng', userId: 'u1' }, input: { role: 'admin' } },
397
+ b: { update: User, key: { userId: 'u1' }, input: { name: 'Ann' }, condition: { status: 'active' }, result: { select: { name: true } } },
398
+ }, { mode: 'transaction' });
301
399
  ```
302
400
 
401
+ - **`key`** is a single object or an **array of objects** (key-array bulk). A
402
+ transaction compiles to `TransactWriteItems`; parallel mode compiles to
403
+ `BatchWriteItem` (chunked, with `UnprocessedItems` retry).
404
+ - **`condition`** is a `WriteCondition` write gate — the same declarative
405
+ operator subset as read filters (`eq`/`ne`/comparisons/`between`/`in`/string
406
+ ops/`size`/`attributeType` + `and`/`or`/`not`), the legacy existence primitives
407
+ (`notExists` / `attributeExists` / `attributeNotExists`), or a raw `cond`
408
+ fragment.
409
+ - **`result: { select, options? }`** reads the written item back; omit it and the
410
+ route returns void.
411
+ - **`mode: 'transaction'`** (DEFAULT) is one atomic `TransactWriteItems`:
412
+ all-or-nothing rollback, throws on failure, and a map of more than 25 items
413
+ errors (it is never split). This is GraphQL's all-or-nothing atomic contract.
414
+ - **`mode: 'parallel'`** is non-atomic: each alias reports partial success in the
415
+ result object as `{ ok }` | `{ error }`, mirroring GraphQL's per-field partial
416
+ success. Independent ops run in parallel; dependencies are ordered by
417
+ `$.alias.field` references.
418
+
419
+ In GraphQL terms: a transaction is atomic all-or-nothing; parallel is non-atomic
420
+ per-field partial success; and `query` routes are parallel independent reads with
421
+ no cross-route consistency.
422
+
303
423
  ### Inspect Execution Plans
304
424
 
305
425
  `explain()` shows the DynamoDB operations before they are executed.
@@ -461,7 +581,6 @@ Layer 5: Hydrator
461
581
  | Structured keys | Keys are built from `k` segment templates; partial keys compile to `begins_with` at a segment boundary. |
462
582
  | Typed consistentRead | `consistentRead` is only available for PK queries. GSI queries are rejected at compile time. |
463
583
  | Server-side filter | `filter` is a declarative DynamoDB `FilterExpression` (AppSync `ModelFilterInput`-compatible), typed against the full entity and able to reference unprojected attributes. It does not reduce RCU, and `limit` is applied before it. |
464
- | Post-load filtering | No built-in post-load predicate; use `result.items.filter(...)` on the typed projection. Efficient narrowing belongs in key design. |
465
584
 
466
585
  ## Architectural Boundaries
467
586
 
@@ -529,7 +648,7 @@ The query / relation / write API above is the core layer. GraphDDB also ships:
529
648
  - Entity metadata / decorators
530
649
  - DDBModel base class / connection management
531
650
  - Type system (SelectableOf, QueryResult, QueryKeyOf)
532
- - CRUD (put, update, delete, conditional writes)
651
+ - CRUD (put, update, delete, conditional writes — `WriteCondition` shares the full declarative filter operator set plus the `cond` raw escape hatch)
533
652
  - Query / List (planner, executor, hydrator, cursor pagination)
534
653
  - Type-safe filtering: declarative server-side `filter` (FilterExpression / AppSync `ModelFilterInput`-compatible), plus `Model.col` column refs and the `cond` raw escape hatch
535
654
  - Structured segment keys (`k` tag): partial keys compile to `begins_with` at a segment boundary