@eresearchqut/ddb-repository 1.5.8 โ†’ 1.13.4

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
@@ -8,139 +8,244 @@ A TypeScript library providing a generic repository pattern implementation for A
8
8
  ## Features
9
9
 
10
10
  - ๐Ÿš€ Generic repository pattern for type-safe DynamoDB operations
11
- - ๐Ÿ“ฆ Simple and intuitive API
12
- - ๐Ÿ” Support for common CRUD operations (Create, Read, Update, Delete)
13
- - ๐ŸŽฏ Batch operations support
11
+ - ๐Ÿ” Rich query support with filter expressions and projections
12
+ - ๐ŸŽฏ Batch get with automatic retry for unprocessed keys
13
+ - ๐Ÿ“„ Paginated queries with limit and sort order control
14
+ - ๐Ÿ—‚๏ธ [JSON Pointer Repository](#jsonpointerrepository) for storing structured JSON documents
14
15
  - ๐Ÿงช Fully tested with Jest and Testcontainers
15
16
  - ๐Ÿ’ช Written in TypeScript with full type safety
16
17
  - โšก Built on top of AWS SDK v3
17
18
 
18
19
  ## Installation
20
+
19
21
  ```sh
20
22
  npm install @eresearchqut/ddb-repository
21
23
  ```
24
+
22
25
  ## Prerequisites
23
26
 
24
- - Node.js
27
+ - Node.js LTS
25
28
  - AWS credentials configured (for production use)
26
- - DynamoDB table with appropriate schema
29
+ - DynamoDB table with the appropriate key schema
27
30
 
28
31
  ## Usage
29
32
 
30
33
  ### Basic Example
34
+
31
35
  ```typescript
32
- import { DynamoDbRepository } from 'ddb-repository';
36
+ import { DynamoDbRepository, FilterOperator } from '@eresearchqut/ddb-repository';
33
37
  import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
34
38
 
35
- // Define your entity type
39
+ interface UserKey {
40
+ id: string;
41
+ }
42
+
36
43
  interface User {
37
- id: string;
38
- name: string;
39
- email: string;
40
- createdAt: string;
44
+ id: string;
45
+ name: string;
46
+ email?: string;
47
+ age?: number;
41
48
  }
42
49
 
43
- // Initialize DynamoDB client
44
50
  const client = new DynamoDBClient({ region: 'us-east-1' });
45
51
 
46
- // Create repository instance
47
- const userRepository = new DynamoDbRepository<User>(
48
- client,
49
- 'users-table',
50
- 'id' // partition key
51
- );
52
+ const userRepository = new DynamoDbRepository<UserKey, User>({
53
+ client,
54
+ tableName: 'users',
55
+ hashKey: 'id',
56
+ });
57
+
58
+ // Write an item
59
+ await userRepository.putItem({ id: '123' }, { id: '123', name: 'Alice', email: 'alice@example.com' });
52
60
 
53
- // Create a new user
54
- await userRepository.create({
55
- id: '123',
56
- name: 'John Doe',
57
- email: 'john@example.com',
58
- createdAt: new Date().toISOString()
61
+ // Read an item
62
+ const user = await userRepository.getItem({ id: '123' });
63
+
64
+ // Update specific fields (SET name, REMOVE email)
65
+ await userRepository.updateItem({ id: '123' }, { name: 'Alicia' }, ['email']);
66
+
67
+ // Delete an item
68
+ await userRepository.deleteItem({ id: '123' });
69
+ ```
70
+
71
+ ### Querying Items
72
+
73
+ `getItems` performs a DynamoDB `Query` and returns all matching items. It paginates automatically and stops early once `limit` items are accumulated.
74
+
75
+ ```typescript
76
+ // All items for a partition key
77
+ const items = await userRepository.getItems({ id: 'user-123' });
78
+
79
+ // With filter expressions
80
+ const activeAdults = await userRepository.getItems({
81
+ id: 'user-123',
82
+ filterExpressions: [
83
+ { attribute: 'age', operator: FilterOperator.GREATER_THAN_OR_EQUALS, value: 18 },
84
+ { attribute: 'status', operator: FilterOperator.EQUALS, value: 'active' },
85
+ ],
59
86
  });
60
87
 
61
- // Find a user by ID
62
- const user = await userRepository.findById('123');
88
+ // With projection (only return selected attributes)
89
+ const names = await userRepository.getItems({
90
+ id: 'user-123',
91
+ projectedAttributes: ['id', 'name'],
92
+ });
63
93
 
64
- // Update a user
65
- await userRepository.update('123', {
66
- email: 'newemail@example.com'
94
+ // With limit and sort order
95
+ const recent = await userRepository.getItems({
96
+ id: 'user-123',
97
+ limit: 10,
98
+ sortOrder: 'DESC',
67
99
  });
68
100
 
69
- // Delete a user
70
- await userRepository.delete('123');
101
+ // Query a Global Secondary Index
102
+ const byStatus = await userRepository.getItems({
103
+ status: 'active',
104
+ index: 'status-index',
105
+ });
71
106
  ```
72
107
 
73
- ## API Reference
108
+ ### Filter Operators
74
109
 
75
- ### Constructor
76
- ```
77
- typescript
78
- new DynamoDbRepository<T>(client: DynamoDBClient, tableName: string, partitionKey: string, sortKey?: string)
79
- ```
80
- ### Methods
110
+ | Operator | DynamoDB Expression | Notes |
111
+ |---|---|---|
112
+ | `EQUALS` | `#attr = :val` | |
113
+ | `NOT_EQUALS` | `#attr <> :val` | |
114
+ | `GREATER_THAN` | `#attr > :val` | |
115
+ | `GREATER_THAN_OR_EQUALS` | `#attr >= :val` | |
116
+ | `LESS_THAN` | `#attr < :val` | |
117
+ | `LESS_THAN_OR_EQUALS` | `#attr <= :val` | |
118
+ | `IN` | `#attr IN (:val0, :val1, ...)` | Pass an `Array` as value |
119
+ | `BETWEEN` | `#attr BETWEEN :val0 AND :val1` | Pass a 2-tuple as value |
120
+ | `BEGINS_WITH` | `begins_with(#attr, :val)` | String prefix match |
121
+ | `CONTAINS` | `contains(#attr, :val)` | Substring or set membership |
81
122
 
82
- - `create(item: T): Promise<T>` - Create a new item
83
- - `findById(id: string): Promise<T | null>` - Find item by partition key
84
- - `update(id: string, updates: Partial<T>): Promise<T>` - Update an existing item
85
- - `delete(id: string): Promise<void>` - Delete an item
86
- - `findAll(): Promise<T[]>` - Retrieve all items (use with caution on large tables)
87
- - `batchCreate(items: T[]): Promise<void>` - Create multiple items in batch
88
- - `query(options: QueryOptions): Promise<T[]>` - Query items with custom conditions
123
+ All operators support `negate: true` to wrap the expression in `NOT (...)`.
89
124
 
90
- ## Development
125
+ ### Batch Get
91
126
 
92
- ### Setup
93
- ```sh
94
- # Install dependencies
95
- npm install
127
+ ```typescript
128
+ const keys = [{ id: '1' }, { id: '2' }, { id: '3' }];
129
+ const items = await userRepository.batchGetItems(keys);
130
+ ```
96
131
 
97
- # Run linter
98
- npm run lint
132
+ Automatically deduplicates keys, pages requests in chunks of 100, and retries any `UnprocessedKeys` with exponential back-off.
99
133
 
100
- # Fix linting issues automatically
101
- npm run lint:fix
134
+ ### Composite Key Tables
102
135
 
103
- # Run tests
104
- npm test
136
+ ```typescript
137
+ interface OrderKey {
138
+ customerId: string;
139
+ orderId: string;
140
+ }
105
141
 
106
- # Run tests in watch mode
107
- npm run test:watch
142
+ interface Order {
143
+ customerId: string;
144
+ orderId: string;
145
+ total: number;
146
+ }
108
147
 
109
- # Run tests with coverage report
110
- npm run test:coverage
148
+ const orderRepository = new DynamoDbRepository<OrderKey, Order>({
149
+ client,
150
+ tableName: 'orders',
151
+ hashKey: 'customerId',
152
+ rangeKey: 'orderId',
153
+ });
111
154
 
112
- # Build the project
113
- npm run build
155
+ await orderRepository.putItem(
156
+ { customerId: 'c1', orderId: 'o1' },
157
+ { customerId: 'c1', orderId: 'o1', total: 99.99 },
158
+ );
114
159
  ```
115
- ### Testing
116
160
 
117
- The project uses Jest with Testcontainers for integration testing against a real DynamoDB instance:
118
- ```sh
119
- npm test
161
+ ### Consumed Capacity Middleware
162
+
163
+ ```typescript
164
+ import { consumedCapacityMiddleware } from '@eresearchqut/ddb-repository';
165
+
166
+ client.middlewareStack.add(
167
+ consumedCapacityMiddleware({
168
+ onConsumedCapacity: async (detail) => {
169
+ console.log('Consumed capacity:', detail.ConsumedCapacity);
170
+ },
171
+ }),
172
+ );
120
173
  ```
121
174
 
122
- ### Run tests with coverage
175
+ ## API Reference
123
176
 
124
- Generate coverage report
177
+ ### `DynamoDbRepository<K, T>`
125
178
 
126
- ```sh
127
- npm run test:coverage
179
+ #### Constructor options
180
+
181
+ | Option | Type | Required | Description |
182
+ |---|---|---|---|
183
+ | `client` | `DynamoDBClient` | โœ… | AWS SDK v3 DynamoDB client |
184
+ | `tableName` | `string` | โœ… | DynamoDB table name |
185
+ | `hashKey` | `string` | โœ… | Partition key attribute name |
186
+ | `rangeKey` | `string` | | Sort key attribute name |
187
+ | `returnConsumedCapacity` | `ReturnConsumedCapacity` | | Defaults to `TOTAL` |
188
+
189
+ #### Methods
190
+
191
+ | Method | Signature | Description |
192
+ |---|---|---|
193
+ | `getItem` | `(key: K) => Promise<T \| undefined>` | Read a single item by key |
194
+ | `putItem` | `(key: K, record: T) => Promise<T>` | Write an item (create or replace) |
195
+ | `updateItem` | `(key: K, updates: Partial<T>, remove?: string[]) => Promise<T \| undefined>` | Partial update with optional attribute removal |
196
+ | `deleteItem` | `(key: K) => Promise<Partial<T> \| undefined>` | Delete an item |
197
+ | `getItems` | `(query: Query) => Promise<T[] \| undefined>` | Query items (auto-paginates) |
198
+ | `batchGetItems` | `(keys: K[], projectedQuery?: ProjectedQuery) => Promise<Array<T \| undefined>>` | Fetch multiple items by key |
199
+
200
+ ## JsonPointerRepository
201
+
202
+ Stores JSON documents as individual per-pointer DynamoDB items using [RFC 6901 JSON Pointer](https://datatracker.ietf.org/doc/html/rfc6901) addressing.
203
+
204
+ See [JSON_POINTER_REPOSITORY.md](./JSON_POINTER_REPOSITORY.md) for full documentation.
205
+
206
+ ### Quick Example
207
+
208
+ ```typescript
209
+ import { JsonPointerRepository } from '@eresearchqut/ddb-repository';
210
+
211
+ const docRepo = new JsonPointerRepository({
212
+ client,
213
+ tableName: 'documents', // requires hash key "id", range key "pointer"
214
+ });
215
+
216
+ await docRepo.putDocument('doc-1', {
217
+ name: 'Alice',
218
+ address: { city: 'Melbourne' },
219
+ tags: ['admin', 'user'],
220
+ });
221
+
222
+ const doc = await docRepo.getDocument('doc-1');
223
+ // { name: 'Alice', address: { city: 'Melbourne' }, tags: ['admin', 'user'] }
224
+
225
+ const city = await docRepo.getAttribute('doc-1', '/address/city');
226
+ // 'Melbourne'
227
+
228
+ await docRepo.putAttribute('doc-1', '/address/postcode', '3000');
229
+ await docRepo.deleteAttribute('doc-1', '/tags/1');
230
+ await docRepo.deleteDocument('doc-1');
128
231
  ```
129
232
 
130
- Coverage reports are generated in the coverage/ directory:
131
- * coverage/lcov-report/index.html - Interactive HTML report
132
- * coverage/lcov.info - LCOV format for CI/CD integration
233
+ ## Development
133
234
 
134
- View HTML coverage report, open coverage/lcov-report/index.html
235
+ ```sh
236
+ npm install # install dependencies
237
+ npm run lint # lint
238
+ npm run lint:fix # lint with auto-fix
239
+ npm test # run integration tests (requires Docker)
240
+ npm run test:coverage # tests with coverage report
241
+ npm run build # compile to dist/
242
+ ```
135
243
 
136
244
  ## Configuration
137
245
 
138
246
  ### AWS Credentials
139
247
 
140
- Ensure your AWS credentials are configured via:
141
- - Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
142
- - AWS credentials file (`~/.aws/credentials`)
143
- - IAM role (when running on AWS infrastructure)
248
+ Configure via environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`), AWS credentials file (`~/.aws/credentials`), or IAM role.
144
249
 
145
250
  ### Required IAM Permissions
146
251
 
@@ -156,8 +261,7 @@ Ensure your AWS credentials are configured via:
156
261
  "dynamodb:UpdateItem",
157
262
  "dynamodb:DeleteItem",
158
263
  "dynamodb:Query",
159
- "dynamodb:Scan",
160
- "dynamodb:BatchWriteItem"
264
+ "dynamodb:BatchGetItem"
161
265
  ],
162
266
  "Resource": "arn:aws:dynamodb:*:*:table/your-table-name"
163
267
  }
@@ -167,37 +271,18 @@ Ensure your AWS credentials are configured via:
167
271
 
168
272
  ## Contributing
169
273
 
170
- Contributions are welcome! Please feel free to submit a Pull Request.
171
-
274
+ Contributions are welcome! Please submit a Pull Request.
172
275
 
173
276
  ### Commit message convention
174
- Semantic release uses conventional commits. Your commit messages should follow this format:
175
-
176
- * feat: new feature โ†’ triggers minor version bump (1.x.0)
177
- * fix: bug fix โ†’ triggers patch version bump (1.0.x)
178
- * perf: performance improvement โ†’ triggers patch version bump
179
- * docs: documentation change โ†’ no release
180
- * chore: maintenance task โ†’ no release
181
- * BREAKING CHANGE: in footer โ†’ triggers major version bump (x.0.0)
182
-
183
- Example:
184
-
185
- > feat: add batch write support
186
- >
187
- > Added support for batch write operations to improve performance
188
277
 
189
- Or with breaking change:
190
-
191
- > feat: change repository API
192
- >
193
- > BREAKING CHANGE: The query method now returns a Promise instead of an Observable
278
+ Semantic release uses [Conventional Commits](https://www.conventionalcommits.org/):
194
279
 
280
+ - `feat:` โ†’ minor version bump
281
+ - `fix:` / `perf:` โ†’ patch version bump
282
+ - `docs:` / `chore:` โ†’ no release
283
+ - `feat!:` or `BREAKING CHANGE:` footer โ†’ major version bump
195
284
 
196
285
  ## License
197
286
 
198
287
  MIT
199
288
 
200
- ## Support
201
-
202
- For issues and questions, please open an issue on the GitHub repository.
203
-