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 +240 -163
- package/dist/index-query.d.ts +4 -4
- package/dist/index-query.js +7 -7
- package/dist/index.d.ts +3 -0
- package/dist/index.js +5 -1
- package/dist/partition.d.ts +5 -3
- package/dist/partition.js +19 -22
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,7 +1,30 @@
|
|
|
1
1
|
# DynoQuery
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/dynoquery)
|
|
4
|
+
[](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
|
|
43
|
+
## Quick Start
|
|
20
44
|
|
|
21
|
-
|
|
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
|
|
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#' },
|
|
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
|
-
####
|
|
91
|
+
#### Item Objects (Single-Row Operations)
|
|
68
92
|
|
|
69
|
-
For
|
|
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.
|
|
81
|
-
const johnStat = john.draft('STAT');
|
|
82
|
-
await johnStat.
|
|
104
|
+
// 2. Draft with initial properties
|
|
105
|
+
const johnStat = john.draft('STAT', { views: 100 });
|
|
106
|
+
await johnStat.save();
|
|
83
107
|
|
|
84
|
-
// 3. Get
|
|
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.
|
|
90
|
-
const
|
|
91
|
-
|
|
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
|
|
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#' }
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
//
|
|
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();
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
185
|
+
// GSI attributes are included automatically
|
|
182
186
|
await mouse.save();
|
|
183
187
|
```
|
|
184
188
|
|
|
185
189
|
### 4. Batch Operations
|
|
186
190
|
|
|
187
|
-
|
|
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 (
|
|
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 (
|
|
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();
|
|
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
|
-
|
|
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 (
|
|
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
|
-
//
|
|
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 (
|
|
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
|
-
|
|
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
|
-
| `
|
|
250
|
-
| `
|
|
251
|
-
| `
|
|
252
|
-
| `
|
|
253
|
-
| `
|
|
254
|
-
|
|
255
|
-
|
|
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',
|
|
262
|
-
skName: 'SortKey',
|
|
263
|
-
pkPrefix: 'TENANT#A#',
|
|
264
|
-
endpoint: 'http://localhost:8000',
|
|
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({
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
309
|
-
|
|
310
|
-
|
|
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
|
-
//
|
|
318
|
-
|
|
319
|
-
await
|
|
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
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
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
|
|
package/dist/index-query.d.ts
CHANGED
|
@@ -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
|
|
20
|
+
protected LastEvaluatedKey: any;
|
|
21
21
|
constructor(db: DynoQuery, config: IndexQueryConfig);
|
|
22
22
|
getAll<T = any>(options?: {
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
23
|
+
Limit?: number;
|
|
24
|
+
ScanIndexForward?: boolean;
|
|
25
|
+
ExclusiveStartKey?: any;
|
|
26
26
|
skValue?: string;
|
|
27
27
|
filterBuilder?: ExpressionBuilder;
|
|
28
28
|
FilterExpression?: string;
|
package/dist/index-query.js
CHANGED
|
@@ -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.
|
|
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.
|
|
59
|
-
ScanIndexForward: options === null || options === void 0 ? void 0 : options.
|
|
60
|
-
ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.
|
|
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.
|
|
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({
|
|
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.
|
|
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
|
}
|
package/dist/partition.d.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
35
|
-
|
|
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.
|
|
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.
|
|
173
|
-
ExclusiveStartKey: options === null || options === void 0 ? void 0 : options.
|
|
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.
|
|
175
|
+
if (!(options === null || options === void 0 ? void 0 : options.ExclusiveStartKey) && !response.LastEvaluatedKey) {
|
|
182
176
|
this.isLoaded = true;
|
|
183
177
|
}
|
|
184
|
-
this.
|
|
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.
|
|
328
|
+
return this.LastEvaluatedKey;
|
|
332
329
|
}
|
|
333
330
|
/**
|
|
334
331
|
* Delete all data in this partition.
|