npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

graphddb

v1.0.0

Published

Graph data modeling on DynamoDB with adjacency list pattern

Readme

GraphDDB

Portable, contract-first DynamoDB access patterns.

GraphDDB lets you define your DynamoDB access patterns once in TypeScript, validate them at build time, inspect the exact execution plan before anything runs, and execute the same contract from five languages — TypeScript, Python, Rust, Go, and PHP. It is not an ORM: it is a contract compiler + runtime for explicit DynamoDB access patterns.

The access patterns become a portable, executable artifact. TypeScript is the single source of truth; a static planner lowers each contract to a language-neutral IR (manifest.json + operations.json), and every language runtime executes that IR with results pinned byte-identical by a shared conformance suite. GraphQL-like traversal is one of the authoring surfaces — ergonomic, but not the point. The point is that PK / GSI / projection / relation / cursor / operation limits stay explicit, verifiable, and portable rather than hidden behind an abstraction.

✨ What GraphDDB does — in three layers

1. Define explicit DynamoDB access patterns

  • Type-safe models —— the TS class is the single source of truth. Keys / GSIs / field types are checked at compile time.
  • Graph-style traversal —— nest @hasMany / @belongsTo / @hasOne inside select (an ergonomic authoring surface — arguments → key lookup, selection → projection).
  • CQRS contracts —— public Query/Command contracts as the read/write authoring surface (N+1-safe, context boundaries, composition across contracts).
  • Maintained access paths —— declare embedded snapshots / aggregate counters kept synchronized atomically with the source write.

2. Validate & explain before anything touches DynamoDB

  • Compile-time safety —— undefined relations, missing GSIs, limit-less lists, and over-depth traversals are rejected at compile time / by the linter — equally for human- and AI-written code.
  • Execution plan (Explain) —— explain() returns the exact DynamoDB operations, so RCU shape is inspectable up front.
  • Automatic access-pattern resolution —— the runtime picks the PK / GSI from the fields you give; selected fields are the only ones read and the only ones in the result type.
  • In-memory testing —— unit-test planner / traversal / transaction / CDC with no Docker, no DynamoDB Local.

3. Execute the same contract, portably, from five languages

  • Portable executable IR —— the static planner lowers each contract to a language-neutral bridge bundle (manifest.json + operations.json, a components[] graph on the shared behavior-contracts vocabulary).
  • 5 language runtimes —— generate clients + runtimes for Python / Rust / Go / PHP (TypeScript runs the IR natively), all executing the same access patterns.
  • 5-language conformance —— TS / Python / Rust / Go / PHP run the same cases against DynamoDB Local; CI pins the results byte-identical.

Operational surface (all built on the layers above): Middleware (host-side read/write hooks) · CDC emulator (local DynamoDB-Streams-equivalent change events) · CDC projection (@cdcProjected() + fromChange / subscribe into typed records) · Prepared statements (compile-once / execute-many, AOT static plans) · CloudFormation generation · Docs generation (Markdown + Mermaid, Handlebars-overridable).

🤔 Philosophy

  • Access patterns as portable contracts —— an access pattern is defined once and becomes a language-neutral, executable artifact. The same contract is validated, explained, and then executed identically across languages, instead of being re-implemented per stack.
  • Native DynamoDB access patterns —— express access patterns directly in code instead of hiding them behind an ORM abstraction. Because PK / GSI, projection, and limits stay visible, the correct design becomes the easiest path.
  • Compile-time safety —— undefined relations, missing GSIs, limit-less lists, and traversals exceeding the allowed depth are rejected before execution at compile time / by the linter. The same constraints apply equally to human-written and AI-generated code.
  • Runtime transparency —— the runtime is not a black box. explain() shows the execution plan, and the plan reveals the shape of RCU usage.
  • Graph-style traversal is a surface, not the essence —— GraphQL's query model (arguments → key lookup, selection → projection, relation → Query/Get/BatchGet) is a convenient way to author access patterns; the essence is the explicit, portable contract underneath.

📦 Install

npm install graphddb
# AWS SDK v3 is a peer dependency. Install the clients you use:
npm install @aws-sdk/client-dynamodb @aws-sdk/lib-dynamodb

tsx is an optional peer dependency, needed only when running the code-generation CLI (graphddb generate …) directly against TypeScript definitions (library consumers do not need it):

npm install -D tsx

Node.js ≥ 22 is required.

🚀 Quick Start

import { DDBModel, graphddb, model, string, key, k } from 'graphddb';
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';

@model({ table: 'AppTable', prefix: 'USER' })
class UserModel extends DDBModel {
  static readonly keys = key<{ userId: string }>((c) => ({
    pk: k`USER#${c.userId}`,
    sk: k`PROFILE`,
  }));

  @string userId!: string;
  @string name!: string;
}
const User = UserModel.asModel();

// Connect the client once. GraphDDB owns throttle / transient retries, so set
// `maxAttempts: 1` to avoid double retries (SDK × library) — see "Retry & throttling" below.
graphddb.config.client(new DynamoDBClient({ maxAttempts: 1 }));

// READ —— only the fields you select appear in the result type.
const user = await User.query({ userId: 'alice' }, { name: true });

// WRITE —— a unified envelope that writes multiple items atomically (default mode: 'transaction').
await graphddb.mutate({
  user: { update: User, key: { userId: 'alice' }, input: { name: 'Alice B.' } },
});

// Raw base operations on a single item (named after the DynamoDB API).
await User.putItem({ userId: 'alice', name: 'Alice' });

Reads center on query (a single PK / unique GSI lookup) / list (multiple items, with a cursor). Writes center on graphddb.mutate (a unified envelope), while putItem / updateItem / deleteItem are positioned secondarily as raw base operations named after the DynamoDB API. For the full API and options, see docs/spec.md.

// query: the 1st argument says "how to find it", the 2nd says "what to return". The return type is inferred from select.
const alice = await User.query({ userId: 'alice' }, { name: true });

// list: keys that return multiple items use a cursor-based list.
const page = await GroupMembership.list({ groupId: 'eng' }, { userId: true, role: true }, { limit: 20 });
// page.items / page.cursor

🔎 Inspect Execution Plans (Explain)

This is what sets GraphDDB apart. explain() returns the DynamoDB operations before they run. select is translated into a ProjectionExpression, so only the requested attributes are read. The plan can be used for testing, debugging, and RCU estimation:

const plan = GroupMembership.explain(
  { groupId: 'eng' },
  { select: { userId: true, role: true }, limit: 10 },
);
// {
//   operations: [{
//     type: "Query",
//     tableName: "UserPermissions",
//     keyCondition: { PK: "GROUP#eng" },
//     rangeCondition: { operator: "begins_with", key: "SK", value: "USER#" },
//     limit: 10,
//   }]
// }

For the complete shape of the plan (such as the rangeCondition separation on a GSI partial match), see docs/spec.md.

🧪 Testing (strongly recommended)

The in-memory executor in graphddb/testing lets you fully unit-test planner / relation traversal / transaction / CDC with no Docker and no DynamoDB Local. Because it never touches a real DynamoDB and runs in-process, you can verify model-mapping, query-plans, relation resolution, and differential aggregation while keeping CI fast and deterministic. See docs/testing.md.

🔗 Relation Traversal

A relation is a query bound to the value of the parent entity. Declare it with @hasMany / @belongsTo / @hasOne and traverse it by nesting inside select. Independent relation subqueries are dispatched in parallel, and N belongsTo resolutions are aggregated into a single BatchGetItem (no N+1):

const alice = await User.query(
  { email: '[email protected]' },
  {
    name: true,
    groups: {
      select: {
        role: true,
        group: { select: { name: true } },   // membership -> Group (belongsTo, BatchGet)
      },
      limit: 20,
    },
  },
  { maxDepth: 2 },   // traversal defaults to depth=1. Deeper traversal must be explicitly allowed.
);

hasMany / list require a default / max limit. For the complete relation model, see docs/spec.md.

🧩 Maintained Access Paths

Declaring a relation or scalar as a maintained access path keeps a separate row in sync with the lifecycle of the source entity (all of it composed into the same atomic transaction as the source write):

  • embedded snapshot —— pattern: 'embeddedSnapshot' on @hasMany / @hasOne keeps a projected snapshot / bounded collection synchronized onto the owner row.
  • aggregate counter —— @aggregate + count() keeps a scalar aggregate synchronized with an atomic ADD ±1.
  • materialized view / sparse view —— @model({ kind }) + @maintainedFrom declare a view as its own model (later phases).
  • versioned history —— maintenance of versioned history rows.

Synchronized maintenance (updateMode: 'mutation') works in both TS and Python (conformance-verified). For how to declare them, the pattern mapping table, and per-phase coverage, see docs/design-patterns.md. Examples: embedded-snapshot-pattern / aggregate-counter.

⚙️ Runtime

The runtime's responsibilities are short. For details, see docs/spec.md:

  • access pattern resolution —— pick the PK / GSI from the fields you give.
  • projection —— compile select into a ProjectionExpression.
  • relation traversal —— dispatch bound queries in parallel (N+1-safe BatchGet).
  • retry / throttling —— retry throttle / transient errors with full-jitter exponential backoff, no configuration required (set the client to maxAttempts: 1, since the library owns retries).
  • hydration —— reconstruct raw items into typed partial entities.

🛠 More Capabilities

Each capability is summarized here only. For details, operator lists, and procedures, see each doc:

  • Filtering —— a declarative, type-safe server-side filter (compatible with FilterExpression / AppSync ModelFilterInput). Model.col + the cond raw escape hatch are shared by read filters / write conditions. For the operator list, see docs/spec.md.
  • Middleware / Hooks —— graphddb.config.use for host-side hooks on reads / writes (logging, metrics, tenant / authorization scoping). Host-only and non-serialized. For hook points, see docs/middleware.md.
  • Class hydration —— options.hydrate loads read results into host-language domain objects (opt-in, Phase 1: query top level). docs/class-hydration.md.
  • Multi-language bridge —— generate Python / Rust / Go / PHP clients + runtimes with TS as the SSoT, kept in step by a 5-language conformance suite. docs/python-bridge.md.
  • Prepared statements —— graphddb.prepare($ => ({...})).execute(params) (unified read/write). "Contract-free precompilation": compile a declarative route once and execute it repeatedly with different params. With the true build-time compilation (AOT) of graphddb transform prepared --aot, plans are emitted as static artifacts and zero runtime compilation happens, including the first call (a stale plan is loud-rejected at load; drift is caught in CI). Without the transform, behavior stays identical through the lazy-slot + structural-memoization fallback. no-runtime-capture is loudly enforced by a build-time lint + a runtime guard. docs/prepared-statements.md.
  • CQRS contracts —— public Query/Command contracts (cardinality matrix, N+1 safety, context boundaries, composition across contracts). docs/cqrs-contract.md / docs/mutation-command-derivation.md.
  • CDC emulator —— change events equivalent to DynamoDB Streams, to drive and test differential aggregation. docs/cdc-emulator.md.
  • CDC projection —— a typed-consumer-IF that parses the CDC change events of a @cdcProjected() source model into typed records. Model.fromChange(event) returns [old, new], and graphddb.subscribe(handlers) (the sibling of query / mutate) produces a ChangeHandler the consumer mounts on its own stream. graphddb owns parse→typed record only; subscription, sink delivery, and idempotency are the consumer's. docs/cdc-projection.md.
  • Multi-language codegen —— execution keeps 3 paths + 1 consumer surface, coexisting permanently: (1) dynamic JSON path (default) — the components[] of operations.json are loaded at runtime and interpreted by the shared run_behavior (for swapping behavior without a deploy). (2) codegen-static path (opt-in)graphddb generate behaviors --lang <ts|python|go|rust|php> hands the same IR to the shared upstream behavior-contracts generator, emitting modules with the IR baked in as native literals. Execution is still interpreted by the shared run_behaviorthe per-op path is identical to the dynamic path (only the IR source differs; no native execution logic is generated). A generated module checks its fingerprint against the live registry and loud-rejects when stale. Rust generated modules use std::sync::LazyLock, so Rust >= 1.80 is required. (3) typed-native path — genuine native-execution codegen (Go / Rust)--typed-native skips the interpreter entirely and emits native per-node execution code (run_native_raw_struct_<comp>: direct field access on concrete structs, a local error type, no run_behavior, zero bc-runtime import). Because the generated code has the same shape a careful engineer hand-writes against the SDK (build key → call → unmarshal into a struct → fan out), it runs at SDK-floor parity (≈0.9–1.05× the hand-written SDK baseline in both Go and Rust) and 2–3× faster than the interpreter. Runtime-freeness is compiler-verified (scripts/native-codegen-purity.mjs); shapes not natively coverable fail closed (never silently boxed); Rust can emit async (ioModel=async). Measured results: benchmark/CROSS-LANG.md. (4) typed bindings (generate python/php/rust/go) — the consumer-UX surface (typed repositories / DTOs), still graphddb-owned; not an execution engine — it delegates to the paths above.
  • CloudFormation generation —— graphddb generate cloudformation emits a CloudFormation template for the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / PROVISIONED / Auto Scaling). docs/cloudformation.md.
  • Docs generation —— graphddb generate docs renders a human-facing model specification (Table Overview / ER diagram / Key Schema / Property Matrix / Entity Details / Access Patterns / Mutation Contracts / Maintained Access Paths / CDC / CloudFormation) as Markdown + Mermaid. The layout is defined by bundled Handlebars templates that users can customize with --template-dir (per-partial override) and --helpers (extra helpers). docs/docs-generation.md.

📚 Documentation

The full documentation is bundled in the npm package (under docs/). After installation you can browse every doc at node_modules/graphddb/docs/, and if you add it as a devDependency you have all the documentation available offline at hand. On GitHub, follow the links in the table below.

| Document | Description | |----------|-------------| | Specification | Core API: entities, structured keys/GSIs, query/filter, relations, batch/transaction, design rules, runtime behavior. | | Design patterns | Mapping representative DynamoDB design patterns onto graphddb features (how to declare pattern / @model({ kind }) + @maintainedFrom / @aggregate, read & write maintenance, per-phase coverage). | | Middleware / Hooks | The graphddb.config.use host-side middleware: hook points read R1–R5 / write W1–W5, { context }, ordering. | | Prepared statements | graphddb.prepare / .execute (unified read/write prepared statements), the no-runtime-capture constraint, graphddb transform prepared (AOT static plans / compile-time hoisting + build-time lint + drift detection): choosing between the 3 tiers AOT > lazy slot + structural memoization > ad-hoc. | | CQRS contract layer | Public Query/Command contracts, cardinality matrix, N+1 safety, composition across contracts, context boundaries. | | Mutation → command derivation | The internal write-plan composition DSL behind the Command IF (entityWrites, fragment composition, derivation of atomic TransactWriteItems). | | Class hydration | Opt-in options.hydrate that loads read results into host objects (Phase 1: query top level). | | Multi-language bridge | Code generation with TS as the SSoT and Python / Rust / Go / PHP runtimes; 5-language conformance. | | In-memory test adapter | Docker-free in-process testing with graphddb/testing and MemoryInspector. | | CDC emulator | A change-event emulator for differential aggregation patterns (dev/test). | | CDC projection | The @cdcProjected() + fromChange / subscribe typed-consumer-IF: a contract that parses CDC change events into typed records. Boundary (parse→typed is graphddb; subscription, sink delivery, idempotency are the consumer's). | | CloudFormation generation | The generate cloudformation command: emit a CloudFormation template for the DynamoDB table(s) a model set maps onto (GSI union / Streams / TTL / PITR / TableClass / SSE / PROVISIONED / Auto Scaling). All options, derivation rules, and scope boundary. | | Docs generation | The generate docs command: generate a model specification (Markdown + Mermaid) from the TS models. All options, output sections, and customizing the Handlebars templates with --template-dir / --helpers. | | Benchmark | Library comparison against hand-written AWS SDK, ElectroDB, DynamoDB Toolbox, and OneTable. |

💡 Examples

| Example | Description | |---------|-------------| | user-permissions | Manages users, groups, and permissions with Single Table Design. Includes adjacency lists, inverted indexes, relation traversal, and explain. | | aggregate-tree-pattern | Differential tree aggregation (siteScoreAverage) driven by the CDC emulator: dirty propagation, throttled sweeps, recomputation. | | embedded-snapshot-pattern | Treating a relation as a maintained access path: pattern: 'embeddedSnapshot' keeps a denormalized collection / snapshot synchronized onto the owner row in the same atomic transaction as the source write. | | 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. |

🏛 Architecture

A query travels from the TS Model through the Planner / Runtime to DynamoDB:

Query execution pipeline: TS Model → Planner → Runtime → DynamoDB

From the same TS Model (the SSoT), multiple targets are derived. The language runtimes are fanned out to 4 languages through the portable IR (the components[] graph in manifest.json + operations.json), and the execution results of all 5 languages — TS included — are pinned byte-identical by the conformance suite:

SSoT derivation: TS Model → TS runtime / portable IR → 4 language runtimes / CQRS / CloudFormation / docs

| Runtime | Package | Distribution | |---------|---------|--------------| | TypeScript | graphddb | npm | | Python | graphddb-runtime | PyPI | | Rust | graphddb_runtime | crates.io | | Go | github.com/foo-log-inc/graphddb/go | go module (go/vX.Y.Z tags) | | PHP | graphddb/runtime | bundled in-repo (not on composer) |

Multi-language execution keeps 4 permanent paths — 3 execution paths (dynamic JSON / codegen-static / typed-native) + 1 consumer surface (see "Multi-language codegen" under More Capabilities):

The multi-language execution paths: authoring → static planner → portable IR → dynamic JSON / codegen-static / typed bindings

The figures are self-contained SVGs drawn with diagram-contracts (Draw TSX). Sources live in diagrams/; regenerate with diagrams/build.sh.

📄 License

MIT