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

@grest-ts/db-dynamodb

v0.0.58

Published

DynamoDB database utilities for Grest Framework

Readme

Part of the grest-ts framework. Documentation | All packages

@grest-ts/db-dynamodb

Thin wrapper around the AWS SDK v3 DynamoDB client to make it easier to use within grest-ts. Handles client lifecycle, configuration via @grest-ts/config, service registration via @grest-ts/locator, and schema-validated table writes via @grest-ts/schema.

The package gives you three layers:

  • GGDynamoDbConfig — config wrapper. Defines the GGResource (region/endpoint) and GGSecret (credentials) keys for one DynamoDB connection. Use inside GGConfig.define().
  • GGDynamoDb — the connection. Owns the SDK client, exposes raw get / put / query / scan / delete primitives plus a conditional-put helper.
  • GGDynamoDbTable<T, PK> — schema-bound table gateway. Validates against a GGSchema on every write. Subclass it to add typed helpers for an entity (e.g. getByOrgId).

Defining the Config

DynamoDB configuration is defined inside GGConfig.define(). Each GGDynamoDbConfig creates two config keys: a GGResource for region/endpoint and a GGSecret for credentials.

// MyConfig.ts
import { GGConfig } from "@grest-ts/config"
import { GGDynamoDbConfig } from "@grest-ts/db-dynamodb"

export const MyConfig = GGConfig.define("/my-service/", () => ({
    db: new GGDynamoDbConfig("main-db"),
}))

Providing Config Values

Local development

Use GGConfigStoreLocal with createLocalConfig for type-safe local values. Point endpoint at a local DynamoDB (e.g. dynamodb-local on http://localhost:8000); credentials can be left blank — the package injects placeholders so the SDK does not hang reaching for IMDS.

// config/local.ts
import { createLocalConfig } from "@grest-ts/config"
import { MyConfig } from "../MyConfig"

export default createLocalConfig(MyConfig, {
    db: {
        host: { region: "eu-north-1", endpoint: "http://localhost:8000" },
        user: { accessKeyId: undefined, secretAccessKey: undefined },
    }
})

Production

Use a production-safe store (e.g. AWS Secrets Manager). The store interface is the same, only the backing implementation changes. In production you typically leave endpoint empty and omit credentials so the SDK uses its default credential chain (IAM role on EC2/Beanstalk, env vars, shared profile).

Wiring in Runtime

Create the config locator and DynamoDB client inside your runtime's compose() method:

// my-service.ts
import { GGConfigLocator, GGConfigStoreLocal, GGResource, GGSecret } from "@grest-ts/config"
import { MyConfig } from "./MyConfig"
import localConfig from "./config/local"

class MyRuntime extends GGRuntime {
    protected compose(): void {
        // 1. Wire config store
        new GGConfigLocator(MyConfig)
            .add([GGResource, GGSecret], new GGConfigStoreLocal(MyConfig, localConfig))

        // 2. Create DynamoDB client (registers with GGLocator, starts/stops with runtime)
        const db = MyConfig.db.newDynamoDb()

        // 3. Use it in your services
        const userService = new UserService(db)
    }
}

The client automatically:

  • Registers with GGLocator for dependency injection
  • Starts on runtime.start() (verifies access with a cheap ListTables call) and tears down on shutdown
  • Watches for config changes and rebuilds the SDK client when region, endpoint, or credentials change

Raw Primitives

// GET
const user = await db.get<UserRow>("users", { userId: "u_123" })

// PUT
await db.put("users", { userId: "u_123", name: "Alice" })

// Conditional PUT — returns false if condition fails (another writer beat us)
const ok = await db.putConditional(
    "users",
    { userId: "u_123", name: "Alice", version: 2 },
    "#v = :expected",
    { ":expected": 1 },
    { "#v": "version" }, // attributeNames needed for reserved keyword "version"
)

// QUERY (PK or GSI)
const rows = await db.query<UserRow>(
    "users",
    "by-org-index",          // GSI name; pass undefined for the base table
    "orgId = :org",
    { ":org": "o_abc" },
)

// DELETE
await db.delete("users", { userId: "u_123" })

// SCAN — small tables / admin only
const all = await db.scan<UserRow>("users")

Schema-Bound Tables

For application code, prefer GGDynamoDbTable. It binds a table to a schema and validates every write — shape mistakes throw at the boundary instead of becoming silent DynamoDB corruption.

import { GGDynamoDbTable } from "@grest-ts/db-dynamodb"
import { IsObject, IsString, IsNumber } from "@grest-ts/schema"

const IsUser = IsObject({
    userId: IsString,
    orgId: IsString,
    name: IsString,
    version: IsNumber,
})
type User = typeof IsUser.infer

class UserTable extends GGDynamoDbTable<User, "userId"> {
    constructor(db: GGDynamoDb) {
        super(db, "users", IsUser, "userId")
    }

    // Typed helpers for index queries
    async getByOrgId(orgId: string): Promise<User[]> {
        return this.query("by-org-index", "orgId = :org", { ":org": orgId })
    }
}

const users = new UserTable(db)
await users.put({ userId: "u_1", orgId: "o_1", name: "Alice", version: 1 })
const u = await users.get("u_1")

Composite Keys

Pass the sort-key field name as the fifth constructor argument. Override get / delete in subclasses to tighten the sort-key type:

class TaskMessageTable extends GGDynamoDbTable<TaskMessage, "taskId"> {
    constructor(db: GGDynamoDb) {
        super(db, "task_messages", IsTaskMessage, "taskId", "messageId")
    }

    override async get(taskId: string, messageId: string) {
        return super.get(taskId, messageId)
    }

    override async delete(taskId: string, messageId: string) {
        return super.delete(taskId, messageId)
    }
}

Single-Result Queries

For unique-index lookups (e.g. username-index), use queryOne so the limit reaches the wire:

const user = await users.queryOne(
    "username-index",
    "username = :u",
    { ":u": "alice" },
)

Admin / Table Creation

GGDynamoDb.getRawClient() returns a fresh low-level DynamoDBClient (no DocumentClient marshalling). Use it for CreateTable / DescribeTable / etc. — operations the document layer doesn't cover. It reads config at call time, so it works before start() has run, which is what seed scripts need to create tables before any data exists.

import { CreateTableCommand } from "@aws-sdk/client-dynamodb"

const raw = db.getRawClient()
await raw.send(new CreateTableCommand({ /* ... */ }))

Reads Are Not Validated

Schema validation is applied on put and putConditional only. Reads (get, query, scan) return whatever the table holds, cast to T. This is deliberate — it keeps the read path fast and avoids forcing a migration when a column is added. If you need read-side validation, wrap the call site.

Config Shape Reference

Host config (GGResource):

| Field | Type | Description | |---|---|---| | region | string | AWS region (e.g. eu-north-1) | | endpoint | string? | Custom endpoint URL (e.g. http://localhost:8000 for dynamodb-local). Empty/undefined = real AWS |

User config (GGSecret):

| Field | Type | Description | |---|---|---| | accessKeyId | string? | AWS access key. Omit to use the SDK's default credential chain | | secretAccessKey | string? | AWS secret key. Omit to use the SDK's default credential chain |

When both credential fields are missing, the SDK uses its default credential chain (IAM role on EC2/Beanstalk, env vars, shared profile). When endpoint is set but credentials are missing, placeholder credentials (local/local) are injected so the SDK does not hang on IMDS in dev.