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

dynamo-query-engine

v1.0.9

Published

Type-safe DynamoDB query builder for GraphQL with support for filtering, sorting, pagination, and relation expansion

Readme

DynamoDB Query Engine

Type-safe DynamoDB query builder for GraphQL with support for filtering, sorting, pagination, and relation expansion. Built on top of Dynamoose for seamless integration with AWS Lambda and serverless applications.

Features

  • DynamoDB-native: Only keys can be filtered/sorted (following DynamoDB best practices)
  • Cursor-based pagination: Efficient pagination with query hash validation
  • Type-safe: JSDoc type definitions for excellent IntelliSense support
  • Expand support: Declarative relation expansion with configurable limits
  • Zero external config: All metadata defined in your Dynamoose schemas
  • AWS Lambda ready: Optimized for serverless environments
  • GraphQL integration: Works seamlessly with GraphQL resolvers

Installation

npm install dynamo-query-engine

Peer Dependencies

npm install dynamoose graphql graphql-parse-resolve-info

Quick Start

1. Define Models with Grid Configuration

Note: The library supports any hash key field name (e.g., pk, tenantId, userId, etc.). It automatically detects the hash key from your schema.

import dynamoose from "dynamoose";

const UserSchema = new dynamoose.Schema({
  pk: {
    type: String,
    hashKey: true,
  },
  createdAt: {
    type: String,
    rangeKey: true,
    query: {
      sort: { type: "key" },
      filter: {
        type: "key",
        operators: ["between", "gte", "lte"],
      },
    },
  },
  status: {
    type: String,
    index: {
      name: "status-createdAt-index",
      global: true,
      rangeKey: "createdAt",
    },
    query: {
      filter: {
        type: "key",
        operators: ["eq"],
        index: "status-createdAt-index",
      },
    },
  },
  teamIds: {
    type: Array,
    schema: [String],
    query: {
      expand: {
        type: "Team",
        relation: "MANY",
        defaultLimit: 5,
        maxLimit: 20,
      },
    },
  },
});

export const UserModel = dynamoose.model("Users", UserSchema);

Example with custom hash key name:

// The library works with any hash key field name
const ReservationSchema = new dynamoose.Schema({
  tenantId: {
    type: String,
    hashKey: true,  // Automatically detected by getHashKey()
  },
  reservationId: {
    type: String,
    rangeKey: true,  // Automatically detected by getRangeKey()
  },
  // ... other fields
});

export const ReservationModel = dynamoose.model("Reservations", ReservationSchema);

Extracting keys programmatically:

import { getHashKey, getRangeKey } from "dynamo-query-engine";

// Get the hash key field name from your schema
const hashKeyField = getHashKey(ReservationModel.schema);
console.log(hashKeyField); // "tenantId"

// Get the range key field name (or null if none exists)
const rangeKeyField = getRangeKey(ReservationModel.schema);
console.log(rangeKeyField); // "reservationId"

2. Register Models

import { modelRegistry } from "dynamo-query-engine";
import { UserModel } from "./models/user.model.js";
import { TeamModel } from "./models/team.model.js";

modelRegistry.register("User", UserModel);
modelRegistry.register("Team", TeamModel);

3. Use in Lambda Resolver

import { 
  buildGridQuery, 
  resolveExpands, 
  encodeCursor 
} from "dynamo-query-engine";
import { parseResolveInfo } from "graphql-parse-resolve-info";

export const handler = async (event) => {
  const args = event.arguments;

  // Build query
  const { query, queryHash } = buildGridQuery({
    model: UserModel,
    partitionKeyValue: "ORG#123",
    filterModel: args.filterModel,
    sortModel: args.sortModel,
    paginationModel: args.paginationModel,
    cursor: args.cursor,
  });

  // Execute
  const users = await query.exec();

  // Resolve relations
  const parsed = parseResolveInfo(event.info);
  await resolveExpands(users, UserModel, parsed?.fieldsByTypeName);

  // Return with cursor
  return {
    rows: users,
    nextCursor: users.lastKey 
      ? encodeCursor(users.lastKey, queryHash) 
      : null,
  };
};

4. Query from Client

query UsersGrid(
  $paginationModel: PaginationInput!
  $sortModel: [SortInput!]
  $filterModel: FilterModelInput
  $cursor: String
) {
  usersGrid(
    paginationModel: $paginationModel
    sortModel: $sortModel
    filterModel: $filterModel
    cursor: $cursor
  ) {
    rows {
      firstName
      lastName
      email
      teamIds {
        name
        status
      }
    }
    nextCursor
  }
}
{
  paginationModel: { pageSize: 20 },
  sortModel: [{ field: "createdAt", sort: "desc" }],
  filterModel: {
    items: [
      { field: "status", operator: "eq", value: "active" }
    ]
  }
}

Grid Configuration Options

Filter Configuration

query: {
  filter: {
    type: "key",                    // Only "key" supported
    operators: ["eq", "gte", "lte"], // Allowed operators
    index: "status-index"           // Optional GSI
  }
}

Supported Operators: eq, ne, gt, gte, lt, lte, between, beginsWith, contains

Sort Configuration

query: {
  sort: { type: "key" } // Only range keys can be sorted
}

Note: DynamoDB only supports sorting by one field (the range key).

Expand Configuration

query: {
  expand: {
    type: "Team",           // Model name (must be registered)
    relation: "MANY",       // "ONE" or "MANY"
    defaultLimit: 5,        // Default fetch limit
    maxLimit: 20            // Maximum allowed limit
  }
}

API Reference

Core Functions

buildGridQuery(options)

Builds a DynamoDB query from grid parameters.

Parameters:

  • model - Dynamoose model
  • partitionKeyValue - Partition key value
  • filterModel - Filter configuration (optional)
  • sortModel - Sort configuration (optional)
  • paginationModel - Pagination configuration (required)
  • cursor - Pagination cursor (optional)

Returns: { query, queryHash }

resolveExpand(options)

Resolves a single relation expansion.

Parameters:

  • parentItems - Array of parent items
  • parentModel - Parent Dynamoose model
  • field - Field name to expand
  • args - Expansion arguments (filter, sort, pagination)

resolveExpands(parentItems, parentModel, fieldsByTypeName)

Resolves multiple expansions from GraphQL resolve info.

Parameters:

  • parentItems - Array of parent items
  • parentModel - Parent Dynamoose model
  • fieldsByTypeName - Parsed GraphQL resolve info fields

Utilities

encodeCursor(lastKey, queryHash)

Encodes pagination cursor to Base64.

decodeCursor(cursor)

Decodes Base64 cursor.

modelRegistry.register(name, model)

Registers a Dynamoose model.

modelRegistry.get(name)

Retrieves a registered model.

getHashKey(schema)

Extracts the hash key (partition key) field name from a Dynamoose schema.

Returns: string - The hash key field name

Throws: Error if no hash key is found in the schema

getRangeKey(schema)

Extracts the range key (sort key) field name from a Dynamoose schema.

Returns: string | null - The range key field name, or null if no range key exists

Architecture

┌─────────────┐
│ GraphQL     │
│ Query       │
└──────┬──────┘
       │
       ▼
┌─────────────┐
│ Lambda      │
│ Resolver    │
└──────┬──────┘
       │
       ├──────► buildGridQuery() ──► DynamoDB
       │                              │
       │                              ▼
       │                          ┌──────┐
       │                          │ Items│
       │                          └───┬──┘
       │                              │
       └──────► resolveExpands() ◄────┘
                     │
                     ├──► ModelRegistry
                     │
                     └──► buildGridQuery() ──► DynamoDB (nested)

Examples

Check out the examples directory for complete working examples:

See the examples README for detailed documentation.

Best Practices

1. Use Global Secondary Indexes for Filters

status: {
  type: String,
  index: {
    name: "status-createdAt-index",
    global: true,
    rangeKey: "createdAt",
  },
  query: {
    filter: {
      type: "key",
      operators: ["eq"],
      index: "status-createdAt-index", // Use the GSI
    },
  },
}

2. Validate User Input

if (args.paginationModel.pageSize > 100) {
  throw new Error("Page size cannot exceed 100");
}

3. Handle Errors Gracefully

try {
  const { query } = buildGridQuery({...});
  return await query.exec();
} catch (error) {
  console.error("Query failed:", error);
  throw new Error(`Failed to fetch data: ${error.message}`);
}

4. Limit Expand Depth

Don't allow deeply nested expansions as they can cause performance issues and high DynamoDB costs.

5. Monitor DynamoDB Costs

Each expand creates additional queries. Use CloudWatch to monitor your read capacity units.

DynamoDB Limitations

This library respects DynamoDB's constraints:

  • Only key attributes (hash key, range key, or GSI keys) can be filtered
  • Only range keys can be sorted
  • Only one sort field is allowed per query
  • No attribute-level filters (use scan sparingly for those cases)

These limitations ensure efficient queries and predictable performance.

Error Handling

The library provides descriptive errors:

  • "Filtering not allowed on field 'X'" - Field doesn't have filter config
  • "Sorting not allowed on field 'X'" - Field doesn't have sort config
  • "Operator 'X' not allowed for field 'Y'" - Operator not in allowed list
  • "Only one sort field supported" - Multiple sort fields specified
  • "Cursor does not match query" - Query params changed between pages
  • "Model 'X' not found in registry" - Model not registered

Testing

The package includes comprehensive unit tests with Vitest.

Run Tests

# Run all tests
npm test

# Run tests with UI
npm run test:ui

# Run tests once (CI mode)
npm run test:run

# Generate coverage report
npm run coverage

Test Coverage

  • Cursor Utils: ~80% coverage
  • Validation: ~70% coverage
  • GridQueryBuilder: ~50% coverage
  • Overall: ~50-60% coverage

See tests/README.md for detailed testing documentation.

Contributing

Contributions are welcome! Please open an issue or pull request.

Development Setup

# Install dependencies
npm install

# Run tests
npm test

# Check coverage
npm run coverage

License

MIT © Sascha Eckstein

Links