dynoquery 0.1.25 → 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 CHANGED
@@ -1,7 +1,30 @@
1
1
  # DynoQuery
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/dynoquery.svg)](https://www.npmjs.com/package/dynoquery)
4
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
5
+
3
6
  A lightweight wrapper for Amazon DynamoDB using the AWS SDK v3, specifically designed for **Single-Table Design** patterns.
4
7
 
8
+ ## Table of Contents
9
+
10
+ - [Installation](#installation)
11
+ - [Features](#features)
12
+ - [Quick Start](#quick-start)
13
+ - [1. Working with Partitions (Models)](#1-working-with-partitions-models)
14
+ - [2. Global Secondary Indexes (findBy)](#2-global-secondary-indexes-findby)
15
+ - [3. Creating Items with GSI Support](#3-creating-items-with-gsi-support)
16
+ - [4. Batch Operations](#4-batch-operations)
17
+ - [5. Transaction Operations](#5-transaction-operations)
18
+ - [6. Time To Live (TTL)](#6-time-to-live-ttl)
19
+ - [Configuration](#configuration)
20
+ - [DynoQuery Options](#dynoquery-options)
21
+ - [findBy Index Options](#findby-index-options)
22
+ - [Advanced Usage](#advanced-usage)
23
+ - [Pagination](#pagination)
24
+ - [Expression Builder](#expression-builder)
25
+ - [API Reference](#api-reference)
26
+ - [License](#license)
27
+
5
28
  ## Installation
6
29
 
7
30
  ```bash
@@ -14,11 +37,12 @@ npm install dynoquery
14
37
  - Optimized for **Single-Table Design** (Partitions and GSIs)
15
38
  - Automatic result mapping to partition models
16
39
  - Built-in caching for partition instances
40
+ - Batch and transaction support with automatic chunking
17
41
  - TypeScript support
18
42
 
19
- ## Quick Start (Basic Usage)
43
+ ## Quick Start
20
44
 
21
- First, initialize the client with your table name.
45
+ Initialize the client with your table name and define your models.
22
46
 
23
47
  ```typescript
24
48
  import { DynoQuery } from 'dynoquery';
@@ -31,77 +55,70 @@ const db = new DynoQuery({
31
55
 
32
56
  ### 1. Working with Partitions (Models)
33
57
 
34
- Define your models to handle different types of data within your single table.
58
+ Define models to handle different entity types within your single table. Each model maps to a PK prefix (e.g. `USER#john@example.com`).
35
59
 
36
60
  ```typescript
37
61
  const db = new DynoQuery({
38
62
  region: 'us-east-1',
39
63
  tableName: 'MyTable',
40
64
  models: {
41
- User: { pkPrefix: 'USER#' }, // Resulting PK: USER#<id>
42
- Product: { pkPrefix: 'PROD#' }
65
+ User: { pkPrefix: 'USER#' }, // PK: USER#<id>
66
+ Product: { pkPrefix: 'PROD#' } // PK: PROD#<id>
43
67
  }
44
68
  });
45
69
 
46
70
  async function userExample() {
47
- const john = db.User('john@example.com');
48
-
71
+ const john = db.User('john@example.com'); // PK: USER#john@example.com
72
+
49
73
  // Create an item (SK: PROFILE)
50
74
  await john.create('PROFILE', { name: 'John Doe', email: 'john@example.com' });
51
-
75
+
52
76
  // Get an item (uses cache if already loaded)
53
77
  const profile = await john.get('PROFILE');
54
78
  console.log(profile.name); // 'John Doe'
55
79
 
56
- // Update an item (partial update)
80
+ // Update an item (partial update — merges with existing data)
57
81
  await john.update('PROFILE', { theme: 'dark' });
58
82
 
59
83
  // Delete an item
60
84
  await john.delete('PROFILE');
61
-
85
+
62
86
  // Fetch all items in this partition
63
87
  const allData = await john.getAll();
64
88
  }
65
89
  ```
66
90
 
67
- #### Second-Level Objects (Single-Row Operations)
91
+ #### Item Objects (Single-Row Operations)
68
92
 
69
- For a more flexible way to work with individual rows, you can use `draft()` and `get()` to obtain an `Item` object, then call `create()`, `update()`, or `save()` on it directly. You can also use a shorthand by passing a second argument to your model function (as shown in the examples below).
93
+ For fine-grained control over individual rows, use `draft()` to get an `Item` object and call `save()` on it directly. Pass a second argument to the model function as a shorthand.
70
94
 
71
95
  ```typescript
72
- const john = db.User('john@example.com');
96
+ const john = db.User('john@example.com'); // PK: USER#john@example.com
73
97
 
74
- // 1. Create a new row
98
+ // 1. Create a new row via draft
75
99
  const johnMeta = john.draft('METADATA');
76
100
  johnMeta.name = 'John Doe';
77
101
  johnMeta.email = 'johndoe@johnmail.com';
78
102
  await johnMeta.save();
79
103
 
80
- // 2. Create with properties in arguments
81
- const johnStat = john.draft('STAT');
82
- await johnStat.create({ views: 100 });
104
+ // 2. Draft with initial properties
105
+ const johnStat = john.draft('STAT', { views: 100 });
106
+ await johnStat.save();
83
107
 
84
- // 3. Get and update an existing row
108
+ // 3. Get an existing row, modify, and save
85
109
  const johnBio = await john.get('BIO');
86
110
  johnBio.birthYear = 1986;
87
111
  await johnBio.save();
88
112
 
89
- // 4. Partial update without fetching
90
- const johnFriend1 = john.draft('FRIEND#1');
91
- await johnFriend1.update({ Name: 'Alice', rank: 1 });
92
-
93
- // 5. Set properties during initialization
94
- const johnPref = john.draft('PREF', { theme: 'dark' });
113
+ // 4. Shorthand: pass SK as second argument to get an Item directly
114
+ const johnPref = db.User('john@example.com', 'PREF'); // PK: USER#john@example.com, SK: PREF
115
+ johnPref.theme = 'dark';
95
116
  await johnPref.save();
96
-
97
- // 6. Shorthand access (returns an Item object directly)
98
- const johnMeta = db.User('john@example.com', 'METADATA');
99
- await johnMeta.update({ name: 'John Doe' });
100
117
  ```
101
118
 
102
119
  ### 2. Global Secondary Indexes (findBy)
103
120
 
104
- Use `findBy` to query your GSIs easily.
121
+ Use `findBy` to query GSIs. By default, the GSI PK attribute name is `{indexName}PK` and the SK attribute name is `{indexName}SK`.
105
122
 
106
123
  ```typescript
107
124
  const db = new DynoQuery({
@@ -111,23 +128,22 @@ const db = new DynoQuery({
111
128
  Product: { pkPrefix: 'PROD#' }
112
129
  },
113
130
  findBy: {
114
- Category: { indexName: 'GSI1', pkPrefix: 'CAT#' }, // pkName defaults to GSI1PK, skName defaults to GSI1SK
131
+ Category: { indexName: 'GSI1', pkPrefix: 'CAT#' }
132
+ // pkName defaults to 'GSI1PK', skName defaults to 'GSI1SK'
115
133
  }
116
134
  });
117
135
 
118
136
  async function indexExample() {
119
- // Query by Category PK: CAT#ELECTRONICS
120
- const electronics = db.findByCategory('ELECTRONICS');
121
-
137
+ const electronics = db.findByCategory('ELECTRONICS'); // GSI1PK: CAT#ELECTRONICS
138
+
122
139
  // Get all items in this category
123
140
  const items = await electronics.getAll();
124
-
125
- // If results match a registered item prefix, they are automatically mapped
141
+
142
+ // Results are automatically mapped to registered models
126
143
  items.forEach(async item => {
127
144
  if (item.__model === 'Product') {
128
- const productPartition = item.getPartition(); // Returns a Partition instance
129
-
130
- // Now you can also edit and save directly if it matches a item
145
+ const productPartition = item.getPartition();
146
+
131
147
  item.price = 45;
132
148
  await item.save();
133
149
  }
@@ -135,70 +151,58 @@ async function indexExample() {
135
151
  }
136
152
  ```
137
153
 
154
+ > **Note:** When passing a `skValue` to `getAll()`, it filters using `begins_with` on the sort key. For example, `getAll({ skValue: 'RANK#' })` returns all items whose GSI SK starts with `RANK#`.
155
+
138
156
  ### 3. Creating Items with GSI Support
139
157
 
140
- You can pass index queries directly to `create()` or `update()` to automatically populate GSI attributes. This works on both the Partition level and the Item level.
158
+ Pass index query objects to `create()` or `update()` to automatically populate GSI attributes.
141
159
 
142
160
  #### Using Partition.create() and Partition.update()
161
+
143
162
  ```typescript
144
- const electronics = db.findByCategory('ELECTRONICS', 'RANK#1');
163
+ const electronics = db.findByCategory('ELECTRONICS', 'RANK#1'); // GSI1PK: CAT#ELECTRONICS, GSI1SK: RANK#1
145
164
 
146
- // This will automatically set GSI1PK='CAT#ELECTRONICS' and GSI1SK='RANK#1'
147
- await db.Product('p123').create('INFO', {
165
+ await db.Product('p123').create('INFO', { // PK: PROD#p123
148
166
  name: 'Gaming Mouse',
149
167
  price: 50
150
168
  }, [electronics]);
151
169
 
152
170
  // Partial update with GSI support
153
- await db.Product('p123').update('INFO', { price: 45 }, [electronics]);
171
+ await db.Product('p123').update('INFO', { price: 45 }, [electronics]); // PK: PROD#p123
154
172
  ```
155
173
 
156
- #### Using Item.create() and Item.update()
157
- ```typescript
158
- const electronics = db.findByCategory('ELECTRONICS', 'RANK#1');
159
- const mouse = db.Product('p123', 'INFO');
174
+ #### Using setIndex() for Persistence
160
175
 
161
- // Pass data and indices directly to create()
162
- await mouse.create({
163
- name: 'Gaming Mouse',
164
- price: 50
165
- }, [electronics]);
166
-
167
- // Or use update() directly on the item for partial updates
168
- await mouse.update({ price: 40 }, [electronics]);
169
- ```
170
-
171
- #### Using setIndex() for persistence
172
- You can also use `setIndex()` to attach indices to an item so they are used automatically whenever you call `save()`.
176
+ Attach indices to an `Item` so they are included automatically on every `save()`.
173
177
 
174
178
  ```typescript
175
- const electronics = db.findByCategory('ELECTRONICS', 'RANK#1');
176
- const mouse = db.Product('p123', 'INFO');
179
+ const electronics = db.findByCategory('ELECTRONICS', 'RANK#1'); // GSI1PK: CAT#ELECTRONICS, GSI1SK: RANK#1
180
+ const mouse = db.Product('p123', 'INFO'); // PK: PROD#p123, SK: INFO
177
181
 
178
182
  mouse.setIndex(electronics);
179
183
  mouse.price = 50;
180
184
 
181
- // save() will now automatically include GSI attributes from the attached index
185
+ // GSI attributes are included automatically
182
186
  await mouse.save();
183
187
  ```
184
188
 
185
189
  ### 4. Batch Operations
186
190
 
187
- DynoQuery provides `batchWrite` and `batchRead` for processing multiple items across partitions.
191
+ `batchWrite` and `batchRead` handle multiple items across partitions. Requests are automatically chunked to respect DynamoDB limits (25 per write, 100 per read).
188
192
 
189
193
  ```typescript
190
- // 1. Batch Write (Create/Replace multiple items)
191
- const user1 = db.User('john@example.com', 'METADATA');
194
+ // 1. Batch Write (create/replace multiple items)
195
+ const user1 = db.User('john@example.com', 'METADATA'); // PK: USER#john@example.com, SK: METADATA
192
196
  user1.name = 'John Doe';
193
197
 
194
- const user2 = db.User('jane@example.com', 'METADATA');
198
+ const user2 = db.User('jane@example.com', 'METADATA'); // PK: USER#jane@example.com, SK: METADATA
195
199
  user2.name = 'Jane Doe';
196
200
 
197
201
  await db.batchWrite([user1, user2]);
198
202
 
199
- // 2. Batch Read (Fetch multiple items or index queries)
200
- const userDraft = db.User('john@example.com', 'METADATA');
201
- const categoryQuery = db.findByCategory('ELECTRONICS', 'p123');
203
+ // 2. Batch Read (fetch multiple items or index queries)
204
+ const userDraft = db.User('john@example.com', 'METADATA'); // PK: USER#john@example.com, SK: METADATA
205
+ const categoryQuery = db.findByCategory('ELECTRONICS', 'p123'); // GSI1PK: CAT#ELECTRONICS, GSI1SK: p123
202
206
 
203
207
  const results = await db.batchRead([userDraft, categoryQuery]);
204
208
 
@@ -206,35 +210,35 @@ results.forEach(item => {
206
210
  console.log(item.__model); // Automatically mapped to models
207
211
  if (item.__model === 'User') {
208
212
  item.status = 'active';
209
- item.save(); // Second-level methods are available
213
+ item.save();
210
214
  }
211
215
  });
212
216
 
213
217
  // 3. Batch Delete
214
- const userToDelete = db.User('john@example.com').draftDelete('METADATA');
218
+ const userToDelete = db.User('john@example.com').draftDelete('METADATA'); // PK: USER#john@example.com
215
219
  await db.batchWrite([userToDelete]);
216
220
  ```
217
221
 
218
222
  ### 5. Transaction Operations
219
223
 
220
- DynoQuery supports `transactWrite` and `transactRead` for atomic operations across multiple items.
224
+ `transactWrite` and `transactRead` perform atomic operations. Requests are automatically chunked to respect DynamoDB's 100-item limit per transaction.
221
225
 
222
226
  ```typescript
223
- // 1. Transaction Write (All operations succeed or all fail)
224
- const user1 = db.User('john@example.com', 'METADATA');
227
+ // 1. Transaction Write (all operations succeed or all fail)
228
+ const user1 = db.User('john@example.com', 'METADATA'); // PK: USER#john@example.com, SK: METADATA
225
229
  user1.name = 'John Doe';
226
230
 
227
- const userToDelete = db.User('olduser@example.com').draftDelete('METADATA');
231
+ const userToDelete = db.User('olduser@example.com').draftDelete('METADATA'); // PK: USER#olduser@example.com
228
232
 
229
- // You can even set conditions for items in a transaction
230
- const criticalItem = db.User('admin@example.com', 'METADATA');
233
+ // Conditional write: only succeeds if status is 'ACTIVE'
234
+ const criticalItem = db.User('admin@example.com', 'METADATA'); // PK: USER#admin@example.com, SK: METADATA
231
235
  criticalItem.lastLogin = new Date().toISOString();
232
236
  criticalItem.setCondition(attr('status').equals('ACTIVE'));
233
237
 
234
238
  await db.transactWrite([user1, userToDelete, criticalItem]);
235
239
 
236
- // 2. Transaction Read (Read multiple items atomically)
237
- const userDraft = db.User('john@example.com', 'METADATA');
240
+ // 2. Transaction Read (read multiple items atomically)
241
+ const userDraft = db.User('john@example.com', 'METADATA'); // PK: USER#john@example.com, SK: METADATA
238
242
  const items = await db.transactRead([userDraft]);
239
243
 
240
244
  if (items[0]) {
@@ -242,26 +246,54 @@ if (items[0]) {
242
246
  }
243
247
  ```
244
248
 
245
- ## Optional Configuration Parameters
249
+ ### 6. Time To Live (TTL)
250
+
251
+ Configure `ttlAttributeName` to enable TTL support. DynoQuery sets the TTL value on items — you must separately enable TTL on the DynamoDB table itself (via the AWS Console, CLI, or CloudFormation).
252
+
253
+ ```typescript
254
+ const db = new DynoQuery({
255
+ region: 'us-east-1',
256
+ tableName: 'MyTable',
257
+ ttlAttributeName: 'expireAt'
258
+ });
259
+
260
+ async function ttlExample() {
261
+ const session = db.User('john@example.com').draft('SESSION'); // PK: USER#john@example.com
262
+
263
+ // Set TTL to 1 hour from now (Unix timestamp in seconds)
264
+ const ttl = Math.floor(Date.now() / 1000) + 3600;
265
+ session.ttl(ttl);
266
+
267
+ session.data = 'some session data';
268
+ await session.save();
269
+ }
270
+ ```
271
+
272
+ ## Configuration
273
+
274
+ ### DynoQuery Options
246
275
 
247
276
  | Parameter | Type | Default | Description |
248
277
  | :--- | :--- | :--- | :--- |
249
- | `pkName` | `string` | `'PK'` | Custom attribute name for Partition Key. |
250
- | `skName` | `string` | `'SK'` | Custom attribute name for Sort Key. |
251
- | `pkPrefix` | `string` | `''` | Global prefix for all partitions (useful for multitenancy, e.g., `TENANT#A#`). |
252
- | `endpoint` | `string` | - | Optional endpoint for local development (e.g., `http://localhost:8000`). |
253
- | `credentials` | `object` | - | Custom AWS credentials (`{ accessKeyId, secretAccessKey, sessionToken? }`). |
254
-
255
- ### Example with Optional Parameters
278
+ | `tableName` | `string` | - | The DynamoDB table name. |
279
+ | `region` | `string` | - | AWS region. |
280
+ | `pkName` | `string` | `'PK'` | Attribute name for the table Partition Key. |
281
+ | `skName` | `string` | `'SK'` | Attribute name for the table Sort Key. |
282
+ | `pkPrefix` | `string` | `''` | Global prefix for all partition keys (useful for multitenancy, e.g., `TENANT#A#`). |
283
+ | `ttlAttributeName` | `string` | - | Attribute name configured for DynamoDB TTL. |
284
+ | `endpoint` | `string` | - | Custom endpoint for local development (e.g., `http://localhost:8000`). |
285
+ | `credentials` | `object` | - | AWS credentials `{ accessKeyId, secretAccessKey, sessionToken? }`. |
286
+ | `models` | `object` | - | Map of model names to `{ pkPrefix: string }`. |
287
+ | `findBy` | `object` | - | Map of index names to index config (see below). |
256
288
 
257
289
  ```typescript
258
290
  const db = new DynoQuery({
259
291
  region: 'us-east-1',
260
292
  tableName: 'MyTable',
261
- pkName: 'PartitionKey', // Custom PK name
262
- skName: 'SortKey', // Custom SK name
263
- pkPrefix: 'TENANT#A#', // Global prefix for multitenancy
264
- endpoint: 'http://localhost:8000', // For local DynamoDB
293
+ pkName: 'PartitionKey',
294
+ skName: 'SortKey',
295
+ pkPrefix: 'TENANT#A#',
296
+ endpoint: 'http://localhost:8000',
265
297
  credentials: {
266
298
  accessKeyId: 'MY_ACCESS_KEY',
267
299
  secretAccessKey: 'MY_SECRET_KEY'
@@ -269,112 +301,157 @@ const db = new DynoQuery({
269
301
  });
270
302
  ```
271
303
 
304
+ ### findBy Index Options
305
+
306
+ | Parameter | Type | Default | Description |
307
+ | :--- | :--- | :--- | :--- |
308
+ | `indexName` | `string` | Required | The DynamoDB index name (e.g., `'GSI1'`). |
309
+ | `pkName` | `string` | `'{indexName}PK'` | Attribute name for the GSI partition key. |
310
+ | `skName` | `string` | `'{indexName}SK'` | Attribute name for the GSI sort key. |
311
+ | `pkPrefix` | `string` | `''` | Prefix applied to PK values for this index. |
312
+
313
+ ```typescript
314
+ const db = new DynoQuery({
315
+ // ...
316
+ findBy: {
317
+ // GSI1PK / GSI1SK auto-derived from indexName
318
+ Category: { indexName: 'GSI1', pkPrefix: 'CAT#' },
319
+
320
+ // Override attribute names explicitly
321
+ Status: { indexName: 'StatusIndex', pkName: 'StatusPK', skName: 'StatusSK' }
322
+ }
323
+ });
324
+ ```
325
+
272
326
  ## Advanced Usage
273
327
 
274
328
  ### Pagination
275
329
 
276
- Both `Partition.getAll()` and `IndexQuery.getAll()` support pagination.
330
+ Both `Partition.getAll()` and `IndexQuery.getAll()` support pagination via `Limit` and `ExclusiveStartKey`.
277
331
 
278
332
  ```typescript
279
- const index = db.findByCategory('ELECTRONICS');
280
- const items = await index.getAll({ limit: 10 });
333
+ const index = db.findByCategory('ELECTRONICS'); // GSI1PK: CAT#ELECTRONICS
334
+ const items = await index.getAll({ Limit: 10 });
281
335
 
282
336
  const token = index.getLastEvaluatedKey();
283
337
  if (token) {
284
- // Fetch next page
285
- const nextItems = await index.getAll({ limit: 10, exclusiveStartKey: token });
338
+ const nextItems = await index.getAll({ Limit: 10, ExclusiveStartKey: token });
286
339
  }
287
340
  ```
288
341
 
289
- ### Expression Builder (Filters & Conditions)
342
+ Use `ScanIndexForward: false` to return results in descending sort key order:
343
+
344
+ ```typescript
345
+ const latest = await index.getAll({ Limit: 5, ScanIndexForward: false });
346
+ ```
290
347
 
291
- Use `ExpressionBuilder` to build complex filter and condition expressions in a type-safe way.
348
+ ### Expression Builder
349
+
350
+ Use `attr()` and `ExpressionBuilder` to build type-safe filter and condition expressions.
292
351
 
293
352
  ```typescript
294
353
  import { attr, ExpressionBuilder } from 'dynoquery';
295
354
 
296
355
  // 1. Filtering in queries
297
356
  const builder = attr('age').greaterThan(25).and(attr('status').equals('ACTIVE'));
298
- const activeUsers = await db.User('some-id').getAll({ filterBuilder: builder });
299
-
300
- // 2. Conditional Updates
301
- const johnMeta = db.User('john@example.com', 'METADATA');
302
- const condition = attr('version').equals(1);
357
+ const activeUsers = await db.User('some-id').getAll({ filterBuilder: builder }); // PK: USER#some-id
303
358
 
304
- johnMeta.setCondition(condition);
359
+ // 2. Conditional save — fails if version is not 1
360
+ const johnMeta = db.User('john@example.com', 'METADATA'); // PK: USER#john@example.com, SK: METADATA
361
+ johnMeta.setCondition(attr('version').equals(1));
305
362
  johnMeta.name = 'John New Name';
306
363
  johnMeta.version = 2;
364
+ await johnMeta.save();
307
365
 
308
- await johnMeta.save(); // Fails if version is not 1
309
-
310
- // 3. Raw Condition Expressions
311
- await db.User('john@example.com').update({ status: 'INACTIVE' }, [], {
312
- ConditionExpression: '#v = :v',
313
- ExpressionAttributeNames: { '#v': 'version' },
314
- ExpressionAttributeValues: { ':v': 2 }
366
+ // 3. Conditional create/update on the Partition level
367
+ await db.User('john@example.com').create('METADATA', { name: 'John' }, [], { // PK: USER#john@example.com
368
+ conditionBuilder: attr('email').notExists()
315
369
  });
316
370
 
317
- // Or using Item object
318
- johnMeta.setCondition(attr('name').exists());
319
- await johnMeta.save();
320
-
321
- // 4. Supported Operators
322
- // - attr('name').equals('val') / notEquals('val')
323
- // - .lessThan(val) / lessThanOrEqual(val)
324
- // - .greaterThan(val) / greaterThanOrEqual(val)
325
- // - .between(start, end)
326
- // - .in([val1, val2])
327
- // - logical: .and(otherBuilder), .or(otherBuilder), ExpressionBuilder.not(builder)
328
-
329
- // 4. Supported Functions
330
- // - attr('field').exists()
331
- // - attr('field').notExists()
332
- // - attr('field').type('S')
333
- // - attr('field').beginsWith('prefix')
334
- // - attr('field').contains('value')
335
- // - attr('field').size().greaterThan(5)
371
+ // 4. NOT combinator
372
+ const notPremium = ExpressionBuilder.not(attr('tier').equals('PREMIUM'));
373
+ const users = await db.User('some-id').getAll({ filterBuilder: notPremium }); // PK: USER#some-id
336
374
  ```
337
375
 
376
+ **Supported operators:**
377
+
378
+ | Method | DynamoDB Expression |
379
+ | :--- | :--- |
380
+ | `.equals(val)` | `= val` |
381
+ | `.notEquals(val)` | `<> val` |
382
+ | `.lessThan(val)` | `< val` |
383
+ | `.lessThanOrEqual(val)` | `<= val` |
384
+ | `.greaterThan(val)` | `> val` |
385
+ | `.greaterThanOrEqual(val)` | `>= val` |
386
+ | `.between(start, end)` | `BETWEEN start AND end` |
387
+ | `.in([val1, val2])` | `IN (val1, val2)` |
388
+ | `.and(otherBuilder)` | `(a) AND (b)` |
389
+ | `.or(otherBuilder)` | `(a) OR (b)` |
390
+ | `ExpressionBuilder.not(builder)` | `NOT (a)` |
391
+
392
+ **Supported functions:**
393
+
394
+ | Method | DynamoDB Function |
395
+ | :--- | :--- |
396
+ | `attr('field').exists()` | `attribute_exists(field)` |
397
+ | `attr('field').notExists()` | `attribute_not_exists(field)` |
398
+ | `attr('field').type('S')` | `attribute_type(field, 'S')` |
399
+ | `attr('field').beginsWith('prefix')` | `begins_with(field, 'prefix')` |
400
+ | `attr('field').contains('value')` | `contains(field, 'value')` |
401
+ | `attr('field').size().greaterThan(5)` | `size(field) > 5` |
402
+
338
403
  ## API Reference
339
404
 
340
405
  ### DynoQuery
341
- - `create(params)`: Low-level PutCommand wrapper.
342
- - `get(params)`: Low-level GetCommand wrapper.
343
- - `update(params)`: Low-level UpdateCommand wrapper.
344
- - `delete(params)`: Low-level DeleteCommand wrapper.
345
- - `query(params)`: Low-level QueryCommand wrapper.
346
- - `scan(params)`: Low-level ScanCommand wrapper.
347
- - `batchWrite(items)`: Batch persists multiple `Item` objects.
348
- - `batchRead(items)`: Batch fetches multiple `Item` or `IndexQuery` objects.
349
- - `transactWrite(items)`: Performs atomic write operations (Put/Delete) for up to 100 items.
350
- - `transactRead(items)`: Performs atomic read operations (Get) for up to 100 items.
351
- - `[ModelName](id, skValue?)`: Returns a `Partition` instance for the given ID. If `skValue` is provided, returns an `Item` object directly.
352
- - `findBy[IndexName](id, skValue?)`: Returns an `IndexQuery` instance.
406
+
407
+ | Method | Description |
408
+ | :--- | :--- |
409
+ | `[ModelName](id, skValue?)` | Returns a `Partition` for the given ID. If `skValue` is provided, returns an `Item` directly. |
410
+ | `findBy[IndexName](id, skValue?)` | Returns an `IndexQuery` for the given ID. |
411
+ | `batchWrite(items)` | Persists multiple `Item` objects. Auto-chunks at 25 per DynamoDB limit. |
412
+ | `batchRead(items)` | Fetches multiple `Item` objects. Auto-chunks at 100 per DynamoDB limit. |
413
+ | `transactWrite(items)` | Atomic write (Put/Delete) for up to 100 items per chunk. |
414
+ | `transactRead(items)` | Atomic read (Get) for up to 100 items per chunk. |
415
+ | `create(params)` | Low-level `PutCommand` wrapper. |
416
+ | `get(params)` | Low-level `GetCommand` wrapper. |
417
+ | `update(params)` | Low-level `UpdateCommand` wrapper. |
418
+ | `delete(params)` | Low-level `DeleteCommand` wrapper. |
419
+ | `query(params)` | Low-level `QueryCommand` wrapper. |
420
+ | `scan(params)` | Low-level `ScanCommand` wrapper. |
353
421
 
354
422
  ### Partition
355
- - `get(skValue)`: Fetches data for a specific Sort Key value (returns a Promise).
356
- - `getAll(options?)`: Fetches items in the partition. Options: `{ limit, exclusiveStartKey, filterBuilder, FilterExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
357
- - `create(skValue, data, indices?, options?)`: Creates an item. `options`: `{ conditionBuilder, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
358
- - `update(skValue, data, indices?, options?)`: Partial update of an item. `options`: `{ conditionBuilder, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
359
- - `delete(skValue, options?)`: Deletes an item. `options`: `{ conditionBuilder, ConditionExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
360
- - `draft(skValue, data?)`: Returns an `Item` object initialized with `data` (optional).
361
- - `draftDelete(skValue)`: Returns an `Item` object marked for deletion (for use with `batchWrite`).
362
- - `deleteAll()`: Deletes all items in the partition.
363
- - `getLastEvaluatedKey()`: Returns the pagination token from the last `getAll()`.
423
+
424
+ | Method | Description |
425
+ | :--- | :--- |
426
+ | `get(skValue)` | Fetches a single item by SK. Returns an `Item` or `null`. |
427
+ | `getAll(options?)` | Queries all items in the partition. Options: `Limit`, `ExclusiveStartKey`, `filterBuilder`, `FilterExpression`, `ExpressionAttributeNames`, `ExpressionAttributeValues`. |
428
+ | `create(skValue, data, indices?, options?)` | Creates/replaces an item. `options`: `conditionBuilder`, `ConditionExpression`, `ExpressionAttributeNames`, `ExpressionAttributeValues`. |
429
+ | `update(skValue, data, indices?, options?)` | Merges `data` with existing item and saves. Same `options` as `create`. |
430
+ | `delete(skValue, options?)` | Deletes an item by SK. Same `options` as `create`. |
431
+ | `draft(skValue, data?)` | Returns an unsaved `Item` initialized with optional `data`. |
432
+ | `draftDelete(skValue)` | Returns an `Item` marked for deletion (for use with `batchWrite`). |
433
+ | `deleteAll()` | Deletes all items in the partition. |
434
+ | `getLastEvaluatedKey()` | Returns the pagination token from the last `getAll()`. |
364
435
 
365
436
  ### IndexQuery
366
- - `get(skValue?)`: Get a single item from the index.
367
- - `getAll(options?)`: Query index. Options: `{ limit, scanIndexForward, exclusiveStartKey, skValue, filterBuilder, FilterExpression, ExpressionAttributeNames, ExpressionAttributeValues }`.
368
- - `getLastEvaluatedKey()`: Returns the pagination token from the last `getAll()`.
369
-
370
- ### Item (returned by Partition.get or draft)
371
- - `create(data?, indices?)`: Persists the item as a new record with the provided data. Supports GSI indices and internal `conditionBuilder`.
372
- - `update(data, indices?)`: Partial update of the item. Supports GSI indices and internal `conditionBuilder`.
373
- - `save()`: Persists the current state of the item. Uses indices attached via `setIndex()` and internal `conditionBuilder`.
374
- - `getData()`: Returns a clean data object containing only the database attributes (filters out internal state and methods).
375
- - `setIndex(indices)`: Attaches one or more `IndexQuery` objects to the item.
376
- - `setFilter(builder)`: Sets a filter expression builder for the item.
377
- - `setCondition(builder)`: Sets a condition for the item using an `ExpressionBuilder`.
437
+
438
+ | Method | Description |
439
+ | :--- | :--- |
440
+ | `getAll(options?)` | Queries the index. Options: `Limit`, `ScanIndexForward`, `ExclusiveStartKey`, `skValue` (uses `begins_with`), `filterBuilder`, `FilterExpression`, `ExpressionAttributeNames`, `ExpressionAttributeValues`. |
441
+ | `get(skValue?)` | Returns the first item matching the optional SK prefix. |
442
+ | `getLastEvaluatedKey()` | Returns the pagination token from the last `getAll()`. |
443
+
444
+ ### Item
445
+
446
+ | Method | Description |
447
+ | :--- | :--- |
448
+ | `save()` | Persists the item. Includes any attached indices and condition. |
449
+ | `ttl(timestamp)` | Sets the TTL attribute value (requires `ttlAttributeName` in config). |
450
+ | `getData()` | Returns a plain object with only the item's data attributes. |
451
+ | `setIndex(index)` | Attaches one or more `IndexQuery` objects (included on every `save()`). |
452
+ | `setFilter(builder)` | Sets a filter expression builder for the item. |
453
+ | `setCondition(builder)` | Sets a condition expression applied on `save()`. |
454
+ | `getPartition()` | Returns the parent `Partition` instance. |
378
455
 
379
456
  ## License
380
457
 
@@ -17,12 +17,12 @@ export declare class IndexQuery {
17
17
  protected skName: string;
18
18
  protected pkValue: string;
19
19
  protected skValue?: string;
20
- protected lastEvaluatedKey: any;
20
+ protected LastEvaluatedKey: any;
21
21
  constructor(db: DynoQuery, config: IndexQueryConfig);
22
22
  getAll<T = any>(options?: {
23
- limit?: number;
24
- scanIndexForward?: boolean;
25
- exclusiveStartKey?: any;
23
+ Limit?: number;
24
+ ScanIndexForward?: boolean;
25
+ ExclusiveStartKey?: any;
26
26
  skValue?: string;
27
27
  filterBuilder?: ExpressionBuilder;
28
28
  FilterExpression?: string;
@@ -12,7 +12,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.IndexQuery = void 0;
13
13
  class IndexQuery {
14
14
  constructor(db, config) {
15
- this.lastEvaluatedKey = null;
15
+ this.LastEvaluatedKey = null;
16
16
  this.db = db;
17
17
  this.tableName = config.tableName || db.getTableName() || "";
18
18
  this.indexName = config.indexName;
@@ -55,19 +55,19 @@ class IndexQuery {
55
55
  FilterExpression: filterExpression,
56
56
  ExpressionAttributeNames: expressionAttributeNames,
57
57
  ExpressionAttributeValues: expressionAttributeValues,
58
- Limit: options === null || options === void 0 ? void 0 : options.limit,
59
- ScanIndexForward: options === null || options === void 0 ? void 0 : options.scanIndexForward,
60
- ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.exclusiveStartKey,
58
+ Limit: options === null || options === void 0 ? void 0 : options.Limit,
59
+ ScanIndexForward: options === null || options === void 0 ? void 0 : options.ScanIndexForward,
60
+ ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.ExclusiveStartKey,
61
61
  });
62
62
  const items = (response.Items || []);
63
63
  const mappedItems = items.map(item => this.db.mapItemToModelItem(item));
64
- this.lastEvaluatedKey = response.LastEvaluatedKey || null;
64
+ this.LastEvaluatedKey = response.LastEvaluatedKey || null;
65
65
  return mappedItems;
66
66
  });
67
67
  }
68
68
  get(skValue) {
69
69
  return __awaiter(this, void 0, void 0, function* () {
70
- const items = yield this.getAll({ limit: 1, skValue });
70
+ const items = yield this.getAll({ Limit: 1, skValue });
71
71
  return items.length > 0 ? items[0] : null;
72
72
  });
73
73
  }
@@ -84,7 +84,7 @@ class IndexQuery {
84
84
  return this.skValue;
85
85
  }
86
86
  getLastEvaluatedKey() {
87
- return this.lastEvaluatedKey;
87
+ return this.LastEvaluatedKey;
88
88
  }
89
89
  }
90
90
  exports.IndexQuery = IndexQuery;
package/dist/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export interface DynoQueryConfig {
10
10
  region?: string;
11
11
  endpoint?: string;
12
12
  pkPrefix?: string;
13
+ ttlAttributeName?: string;
13
14
  credentials?: {
14
15
  accessKeyId: string;
15
16
  secretAccessKey: string;
@@ -32,6 +33,7 @@ export declare class DynoQuery {
32
33
  private globalPkPrefix;
33
34
  private pkName;
34
35
  private skName;
36
+ private ttlAttributeName?;
35
37
  private registeredModels;
36
38
  [key: string]: any;
37
39
  constructor(config?: DynoQueryConfig);
@@ -83,6 +85,7 @@ export declare class DynoQuery {
83
85
  getPkPrefix(): string;
84
86
  getPkName(): string;
85
87
  getSkName(): string;
88
+ getTtlAttributeName(): string | undefined;
86
89
  getRegisteredModels(): Record<string, {
87
90
  pkPrefix: string;
88
91
  }>;
package/dist/index.js CHANGED
@@ -45,7 +45,7 @@ Object.defineProperty(exports, "attr", { enumerable: true, get: function () { re
45
45
  class DynoQuery {
46
46
  constructor(config = {}) {
47
47
  this.registeredModels = {};
48
- const { tableName, pkName, skName, pkPrefix, models, findBy } = config, clientConfig = __rest(config, ["tableName", "pkName", "skName", "pkPrefix", "models", "findBy"]);
48
+ const { tableName, pkName, skName, pkPrefix, ttlAttributeName, models, findBy } = config, clientConfig = __rest(config, ["tableName", "pkName", "skName", "pkPrefix", "ttlAttributeName", "models", "findBy"]);
49
49
  this.client = new client_dynamodb_1.DynamoDBClient(clientConfig);
50
50
  this.docClient = lib_dynamodb_1.DynamoDBDocumentClient.from(this.client, {
51
51
  marshallOptions: {
@@ -56,6 +56,7 @@ class DynoQuery {
56
56
  this.globalPkPrefix = pkPrefix || "";
57
57
  this.pkName = pkName || "PK";
58
58
  this.skName = skName || "SK";
59
+ this.ttlAttributeName = ttlAttributeName;
59
60
  if (models) {
60
61
  this.registeredModels = models;
61
62
  Object.entries(models).forEach(([name, def]) => {
@@ -423,6 +424,9 @@ class DynoQuery {
423
424
  getSkName() {
424
425
  return this.skName;
425
426
  }
427
+ getTtlAttributeName() {
428
+ return this.ttlAttributeName;
429
+ }
426
430
  getRegisteredModels() {
427
431
  return this.registeredModels;
428
432
  }
@@ -24,15 +24,16 @@ export declare class Partition {
24
24
  protected skName: string;
25
25
  protected cache: Record<string, any>;
26
26
  protected isLoaded: boolean;
27
- protected lastEvaluatedKey: any;
27
+ protected LastEvaluatedKey: any;
28
+ protected ttlAttributeName?: string;
28
29
  constructor(db: DynoQuery, config: PartitionConfig, id?: string);
29
30
  /**
30
31
  * Fetches all items in the partition and caches them.
31
32
  * Returns the data and caches it.
32
33
  */
33
34
  getAll<T = any>(options?: {
34
- limit?: number;
35
- exclusiveStartKey?: any;
35
+ Limit?: number;
36
+ ExclusiveStartKey?: any;
36
37
  filterBuilder?: ExpressionBuilder;
37
38
  FilterExpression?: string;
38
39
  ExpressionAttributeNames?: Record<string, string>;
@@ -86,6 +87,7 @@ export declare class Partition {
86
87
  draftDelete<T = any>(skValue: string): T;
87
88
  getTableName(): string;
88
89
  getPkValue(): string;
90
+ getTtlAttributeName(): string | undefined;
89
91
  getLastEvaluatedKey(): any;
90
92
  /**
91
93
  * Delete all data in this partition.
package/dist/partition.js CHANGED
@@ -47,22 +47,6 @@ class Item {
47
47
  });
48
48
  };
49
49
  }
50
- if (prop === "create") {
51
- return (data, indices) => {
52
- const dataToSave = data || {};
53
- const finalIndices = indices || self._indices;
54
- return partition.create(skValue, dataToSave, finalIndices, {
55
- conditionBuilder: self._conditionBuilder,
56
- });
57
- };
58
- }
59
- if (prop === "update") {
60
- return (data, indices) => {
61
- return partition.update(skValue, data, indices, {
62
- conditionBuilder: self._conditionBuilder,
63
- });
64
- };
65
- }
66
50
  if (prop === "setFilter") {
67
51
  return (builder) => {
68
52
  self._filterBuilder = builder;
@@ -92,6 +76,15 @@ class Item {
92
76
  return receiver;
93
77
  };
94
78
  }
79
+ if (prop === "ttl") {
80
+ return (timestamp) => {
81
+ const ttlAttr = partition.getTtlAttributeName();
82
+ if (ttlAttr) {
83
+ self[ttlAttr] = timestamp;
84
+ }
85
+ return receiver;
86
+ };
87
+ }
95
88
  if (prop === "getPartition") {
96
89
  return () => partition;
97
90
  }
@@ -111,11 +104,12 @@ class Partition {
111
104
  constructor(db, config, id) {
112
105
  this.cache = {};
113
106
  this.isLoaded = false;
114
- this.lastEvaluatedKey = null;
107
+ this.LastEvaluatedKey = null;
115
108
  this.db = db;
116
109
  this.tableName = config.tableName || db.getTableName();
117
110
  this.pkName = db.getPkName();
118
111
  this.skName = db.getSkName();
112
+ this.ttlAttributeName = db.getTtlAttributeName();
119
113
  if (config.pk) {
120
114
  this.pkValue = config.pk;
121
115
  }
@@ -169,8 +163,8 @@ class Partition {
169
163
  FilterExpression: filterExpression,
170
164
  ExpressionAttributeNames: expressionAttributeNames,
171
165
  ExpressionAttributeValues: expressionAttributeValues,
172
- Limit: options === null || options === void 0 ? void 0 : options.limit,
173
- ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.exclusiveStartKey,
166
+ Limit: options === null || options === void 0 ? void 0 : options.Limit,
167
+ ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.ExclusiveStartKey,
174
168
  });
175
169
  const items = (response.Items || []);
176
170
  items.forEach((item) => {
@@ -178,10 +172,10 @@ class Partition {
178
172
  this.cache[item[this.skName]] = item;
179
173
  }
180
174
  });
181
- if (!(options === null || options === void 0 ? void 0 : options.exclusiveStartKey) && !response.LastEvaluatedKey) {
175
+ if (!(options === null || options === void 0 ? void 0 : options.ExclusiveStartKey) && !response.LastEvaluatedKey) {
182
176
  this.isLoaded = true;
183
177
  }
184
- this.lastEvaluatedKey = response.LastEvaluatedKey || null;
178
+ this.LastEvaluatedKey = response.LastEvaluatedKey || null;
185
179
  return items.map((item) => new Item(this, item[this.skName], item));
186
180
  });
187
181
  }
@@ -327,8 +321,11 @@ class Partition {
327
321
  getPkValue() {
328
322
  return this.pkValue;
329
323
  }
324
+ getTtlAttributeName() {
325
+ return this.ttlAttributeName;
326
+ }
330
327
  getLastEvaluatedKey() {
331
- return this.lastEvaluatedKey;
328
+ return this.LastEvaluatedKey;
332
329
  }
333
330
  /**
334
331
  * Delete all data in this partition.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dynoquery",
3
- "version": "0.1.25",
3
+ "version": "0.2.1",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "git+https://github.com/devspikejs/dynoquery.git"