Example: Scan Operations
A product and review catalog that demonstrates scan operations — reading all items from a table without targeting a specific partition key. Scans are the broadest read operation in DynamoDB, and effect-dynamodb makes them composable with the same BoundQuery combinators used for index queries.
What you’ll learn:
db.entities.Entity.scan()— full-table scan returning a BoundQuery- BoundQuery combinators:
.filter(),.limit(),.pageSize(),.consistentRead()on scans - Terminals:
.collect(),.fetch(),.paginate()for executing scans - Entity-type isolation — scans automatically filter by
__edd_e__so each entity only sees its own items - When to use scan vs query
Step 1: Models
Section titled “Step 1: Models”Two entity types sharing a single table — products and reviews:
class Product extends Schema.Class<Product>("Product")({ productId: Schema.String, name: Schema.NonEmptyString, category: Schema.String, price: Schema.Number, inStock: Schema.Boolean,}) {}
class Review extends Schema.Class<Review>("Review")({ reviewId: Schema.String, productId: Schema.String, rating: Schema.Number, comment: Schema.String,}) {}Step 2: Schema, Entities, and Table
Section titled “Step 2: Schema, Entities, and Table”Both entities share one table with GSI indexes for category/product lookups:
const AppSchema = DynamoSchema.make({ name: "scan-demo", version: 1 })
const Products = Entity.make({ model: Product, entityType: "Product", primaryKey: { pk: { field: "pk", composite: ["productId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byCategory: { name: "gsi1", pk: { field: "gsi1pk", composite: ["category"] }, sk: { field: "gsi1sk", composite: ["productId"] }, }, }, timestamps: true,})
const Reviews = Entity.make({ model: Review, entityType: "Review", primaryKey: { pk: { field: "pk", composite: ["reviewId"] }, sk: { field: "sk", composite: [] }, }, indexes: { byProduct: { name: "gsi1", pk: { field: "gsi1pk", composite: ["productId"] }, sk: { field: "gsi1sk", composite: ["reviewId"] }, }, }, timestamps: true,})
const MainTable = Table.make({ schema: AppSchema, entities: { Products, Reviews } })Note that both entities overload GSI1 — Products use it for category lookups while Reviews use it for product lookups. This is standard single-table design.
Step 3: Seed Data
Section titled “Step 3: Seed Data”yield* db.entities.Products.put({ productId: "p-1", name: "Wireless Mouse", category: "electronics", price: 29.99, inStock: true,})yield* db.entities.Products.put({ productId: "p-2", name: "Mechanical Keyboard", category: "electronics", price: 89.99, inStock: true,})yield* db.entities.Products.put({ productId: "p-3", name: "USB-C Cable", category: "accessories", price: 9.99, inStock: false,})yield* db.entities.Products.put({ productId: "p-4", name: "Monitor Stand", category: "accessories", price: 49.99, inStock: true,})yield* db.entities.Reviews.put({ reviewId: "r-1", productId: "p-1", rating: 5, comment: "Great mouse!",})yield* db.entities.Reviews.put({ reviewId: "r-2", productId: "p-2", rating: 4, comment: "Good keyboard",})The table now has 6 items: 4 products and 2 reviews.
Step 4: Operations
Section titled “Step 4: Operations”Basic Scan
Section titled “Basic Scan”db.entities.Products.scan() returns a BoundQuery that reads the entire table. Call .collect() to execute it. Despite sharing a table with reviews, the scan only returns product items — the ORM automatically adds a FilterExpression on __edd_e__:
const allProducts = yield* db.entities.Products.scan().collect()Scan with Filter
Section titled “Scan with Filter”Scans support all BoundQuery combinators, including .filter(). Chain combinators on the BoundQuery before calling .collect(). The filter is applied server-side after items are read, reducing network transfer:
const inStockProducts = yield* db.entities.Products.scan().filter({ inStock: true }).collect()Note that filters do not reduce read capacity — DynamoDB still reads every item in the table. Use queries with partition keys when you need efficient reads.
Scan with Limit
Section titled “Scan with Limit”.limit() bounds the result: at most n items come back, however many requests that takes. To size the requests themselves — DynamoDB’s Limit — use .pageSize():
const firstTwo = yield* db.entities.Products.scan().limit(2).collect()Scan with Consistent Read
Section titled “Scan with Consistent Read”.consistentRead() passes ConsistentRead: true to DynamoDB, ensuring you read the most recent data. This costs 2x the read capacity of eventually-consistent reads:
const consistent = yield* db.entities.Products.scan().consistentRead().collect()Entity-Type Isolation
Section titled “Entity-Type Isolation”The key feature of scans in a single-table design: each entity’s scan only returns its own items. The __edd_e__ discriminator attribute is automatically included in the FilterExpression:
const productScan = yield* db.entities.Products.scan().collect()const reviewScan = yield* db.entities.Reviews.scan().collect()Even though both scans read the same physical table, each returns only items matching its entity type.
Scan vs Query
Section titled “Scan vs Query”| Scan | Query | |
|---|---|---|
| Reads | Entire table (or index) | Single partition key |
| Cost | Proportional to table size | Proportional to items in partition |
| Use case | Analytics, admin tools, data export | Application access patterns |
| FilterExpression | Reduces network transfer, NOT read capacity | Same behavior |
Prefer queries for application access patterns. Use scans for administrative operations, data migrations, or when you genuinely need all items of a given entity type.
Running the Example
Section titled “Running the Example”The complete runnable example is at examples/scan.ts in the repository.
Start DynamoDB Local:
docker run -d -p 8000:8000 amazon/dynamodb-localRun the example:
npx tsx examples/scan.tsLayer Setup
Section titled “Layer Setup”const AppLayer = Layer.mergeAll( DynamoClient.layer({ region: "us-east-1", endpoint: "http://localhost:8000", credentials: { accessKeyId: "local", secretAccessKey: "local" }, }), MainTable.layer({ name: "scan-demo-table" }),)
const main = program.pipe(Effect.provide(AppLayer))
Effect.runPromise(main).then( () => console.log("\nDone."), (err) => console.error("\nFailed:", err),)Key Takeaways
Section titled “Key Takeaways”| Concept | How it’s used |
|---|---|
db.entities.Entity.scan() | Returns a BoundQuery — composable with all BoundQuery combinators |
.collect() / .paginate() | Terminals on BoundQuery — execute a scan (or query) |
.filter() | Server-side FilterExpression on scans — reduces network transfer |
.limit() | Caps the number of items returned |
.pageSize() | Caps the rows examined per DynamoDB request (Limit) |
.consistentRead() | Strongly consistent reads at 2x RCU cost |
| Entity-type isolation | __edd_e__ filter ensures scans return only the matching entity type |
| Scan vs Query | Scans read the whole table; queries target a partition — prefer queries for app patterns |
What’s Next?
Section titled “What’s Next?”- Example: Conditional Writes & Filters — Conditional puts, updates, deletes, and filter operators
- Example: Projections — Selective attribute retrieval with ProjectionExpression
- Queries Guide — Index queries, pagination, sort key conditions, and collections
- Example: Human Resources — Full single-table design with 3 entities, collections, and transactions