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 +130 -11
- package/dist/{chunk-6LEHSX45.js → chunk-4B4MUPUJ.js} +3037 -1143
- package/dist/{chunk-347U24SB.js → chunk-PWV7JDMR.js} +353 -37
- package/dist/{chunk-UNRQ5YJT.js → chunk-SBNP62H7.js} +1 -1
- package/dist/cli.js +2 -2
- package/dist/index.d.ts +61 -3334
- package/dist/index.js +187 -1881
- package/dist/testing/index.d.ts +2 -2
- package/dist/testing/index.js +2 -2
- package/dist/types-DPJ4tPjX.d.ts +4607 -0
- package/package.json +1 -1
- package/dist/types-CDrWiPxp.d.ts +0 -1203
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
|
-
|
|
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.
|
|
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.
|
|
356
|
+
await GroupMembership.putItem({
|
|
295
357
|
groupId: 'eng',
|
|
296
358
|
userId: 'alice',
|
|
297
359
|
role: 'admin',
|
|
298
360
|
});
|
|
299
361
|
|
|
300
|
-
await User.
|
|
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
|