@eresearchqut/ddb-repository 1.0.2
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/.github/workflows/build.yml +61 -0
- package/.github/workflows/release.yml +42 -0
- package/.releaserc.json +17 -0
- package/CHANGELOG.md +23 -0
- package/README.md +202 -0
- package/dist/DynamoDbRepository.js +239 -0
- package/dist/index.js +17 -0
- package/eslint.config.mjs +13 -0
- package/jest.config.ts +28 -0
- package/package.json +45 -0
- package/src/DynamoDbRepository.ts +363 -0
- package/src/index.ts +3 -0
- package/test/repository.test.ts +1123 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
name: Build
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ main, develop ]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [ main, develop ]
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout code
|
|
14
|
+
uses: actions/checkout@v4
|
|
15
|
+
|
|
16
|
+
- name: Setup Node.js
|
|
17
|
+
uses: actions/setup-node@v4
|
|
18
|
+
with:
|
|
19
|
+
node-version: 'lts/*'
|
|
20
|
+
registry-url: 'https://registry.npmjs.org'
|
|
21
|
+
|
|
22
|
+
- name: Install dependencies
|
|
23
|
+
run: npm ci
|
|
24
|
+
|
|
25
|
+
- name: Run linter
|
|
26
|
+
run: npm run lint
|
|
27
|
+
|
|
28
|
+
- name: Run tests with coverage
|
|
29
|
+
run: npm run test:coverage
|
|
30
|
+
|
|
31
|
+
- name: Upload coverage reports to Codecov
|
|
32
|
+
uses: codecov/codecov-action@v4
|
|
33
|
+
with:
|
|
34
|
+
file: ./coverage/lcov.info
|
|
35
|
+
flags: unittests
|
|
36
|
+
name: codecov-umbrella
|
|
37
|
+
fail_ci_if_error: false
|
|
38
|
+
|
|
39
|
+
- name: Upload coverage to Coveralls
|
|
40
|
+
uses: coverallsapp/github-action@v2
|
|
41
|
+
with:
|
|
42
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
43
|
+
path-to-lcov: ./coverage/lcov.info
|
|
44
|
+
continue-on-error: true
|
|
45
|
+
|
|
46
|
+
- name: Build package
|
|
47
|
+
run: npm run build
|
|
48
|
+
|
|
49
|
+
- name: Upload build artifacts
|
|
50
|
+
uses: actions/upload-artifact@v4
|
|
51
|
+
with:
|
|
52
|
+
name: dist
|
|
53
|
+
path: dist/
|
|
54
|
+
retention-days: 7
|
|
55
|
+
|
|
56
|
+
- name: Upload coverage artifacts
|
|
57
|
+
uses: actions/upload-artifact@v4
|
|
58
|
+
with:
|
|
59
|
+
name: coverage-report
|
|
60
|
+
path: coverage/
|
|
61
|
+
retention-days: 7
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
name: Publish
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches:
|
|
6
|
+
- main
|
|
7
|
+
|
|
8
|
+
jobs:
|
|
9
|
+
release:
|
|
10
|
+
runs-on: ubuntu-latest
|
|
11
|
+
permissions:
|
|
12
|
+
contents: write
|
|
13
|
+
issues: write
|
|
14
|
+
pull-requests: write
|
|
15
|
+
id-token: write
|
|
16
|
+
steps:
|
|
17
|
+
- name: Checkout
|
|
18
|
+
uses: actions/checkout@v4
|
|
19
|
+
with:
|
|
20
|
+
fetch-depth: 0
|
|
21
|
+
persist-credentials: false
|
|
22
|
+
|
|
23
|
+
- name: Setup Node.js
|
|
24
|
+
uses: actions/setup-node@v4
|
|
25
|
+
with:
|
|
26
|
+
node-version: 'lts/*'
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: npm ci
|
|
30
|
+
|
|
31
|
+
- name: Run tests
|
|
32
|
+
run: npm test
|
|
33
|
+
|
|
34
|
+
- name: Build
|
|
35
|
+
run: npm run build
|
|
36
|
+
|
|
37
|
+
- name: Release
|
|
38
|
+
env:
|
|
39
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
40
|
+
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
41
|
+
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
42
|
+
run: npx semantic-release
|
package/.releaserc.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"branches": ["main"],
|
|
3
|
+
"plugins": [
|
|
4
|
+
"@semantic-release/commit-analyzer",
|
|
5
|
+
"@semantic-release/release-notes-generator",
|
|
6
|
+
"@semantic-release/changelog",
|
|
7
|
+
"@semantic-release/npm",
|
|
8
|
+
[
|
|
9
|
+
"@semantic-release/git",
|
|
10
|
+
{
|
|
11
|
+
"assets": ["package.json", "package-lock.json", "CHANGELOG.md"],
|
|
12
|
+
"message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}"
|
|
13
|
+
}
|
|
14
|
+
],
|
|
15
|
+
"@semantic-release/github"
|
|
16
|
+
]
|
|
17
|
+
}
|
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
## [1.0.2](https://github.com/eresearchqut/ddb-repository/compare/v1.0.1...v1.0.2) (2025-11-19)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* setting publish config access to public ([eed3a3b](https://github.com/eresearchqut/ddb-repository/commit/eed3a3b382153c457e3508d844e111b7f5b3123d))
|
|
7
|
+
|
|
8
|
+
## [1.0.1](https://github.com/eresearchqut/ddb-repository/compare/v1.0.0...v1.0.1) (2025-11-19)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug Fixes
|
|
12
|
+
|
|
13
|
+
* remove npm registry url from the node setup as per comments on https://github.com/semantic-release/semantic-release/issues/2313 ([291d61b](https://github.com/eresearchqut/ddb-repository/commit/291d61b55c3c5c665623c264306c40e1c21977a2))
|
|
14
|
+
* Repository and package are now public ([b07f151](https://github.com/eresearchqut/ddb-repository/commit/b07f151732f8a158a2b6f0a584e64ea9eb7f5825))
|
|
15
|
+
* updated secret name for GITHUB_TOKEN ([7e00208](https://github.com/eresearchqut/ddb-repository/commit/7e00208f487f5987e72646c15a50fe78808f6ba4))
|
|
16
|
+
* updated secret name for GITHUB_TOKEN ([6d3eff2](https://github.com/eresearchqut/ddb-repository/commit/6d3eff2fff43f0d0848cfc03b7e035d830a3ceb5))
|
|
17
|
+
|
|
18
|
+
# 1.0.0 (2025-11-19)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
### Features
|
|
22
|
+
|
|
23
|
+
* coverage badge ([0f3c9d1](https://github.com/eresearchqut/ddb-repository/commit/0f3c9d128b25466758062ea9e41d1c7d755c2191))
|
package/README.md
ADDED
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
# DynamoDB Repository
|
|
2
|
+
|
|
3
|
+
[](https://coveralls.io/github/eresearchqut/ddb-repository?branch=main)
|
|
4
|
+
|
|
5
|
+
A TypeScript library providing a generic repository pattern implementation for AWS DynamoDB, simplifying CRUD operations and common database interactions.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- 🚀 Generic repository pattern for type-safe DynamoDB operations
|
|
10
|
+
- 📦 Simple and intuitive API
|
|
11
|
+
- 🔍 Support for common CRUD operations (Create, Read, Update, Delete)
|
|
12
|
+
- 🎯 Batch operations support
|
|
13
|
+
- 🧪 Fully tested with Jest and Testcontainers
|
|
14
|
+
- 💪 Written in TypeScript with full type safety
|
|
15
|
+
- ⚡ Built on top of AWS SDK v3
|
|
16
|
+
|
|
17
|
+
## Installation
|
|
18
|
+
```sh
|
|
19
|
+
npm install ddb-repository
|
|
20
|
+
```
|
|
21
|
+
## Prerequisites
|
|
22
|
+
|
|
23
|
+
- Node.js
|
|
24
|
+
- AWS credentials configured (for production use)
|
|
25
|
+
- DynamoDB table with appropriate schema
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
### Basic Example
|
|
30
|
+
```typescript
|
|
31
|
+
import { DynamoDbRepository } from 'ddb-repository';
|
|
32
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
33
|
+
|
|
34
|
+
// Define your entity type
|
|
35
|
+
interface User {
|
|
36
|
+
id: string;
|
|
37
|
+
name: string;
|
|
38
|
+
email: string;
|
|
39
|
+
createdAt: string;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Initialize DynamoDB client
|
|
43
|
+
const client = new DynamoDBClient({ region: 'us-east-1' });
|
|
44
|
+
|
|
45
|
+
// Create repository instance
|
|
46
|
+
const userRepository = new DynamoDbRepository<User>(
|
|
47
|
+
client,
|
|
48
|
+
'users-table',
|
|
49
|
+
'id' // partition key
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Create a new user
|
|
53
|
+
await userRepository.create({
|
|
54
|
+
id: '123',
|
|
55
|
+
name: 'John Doe',
|
|
56
|
+
email: 'john@example.com',
|
|
57
|
+
createdAt: new Date().toISOString()
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// Find a user by ID
|
|
61
|
+
const user = await userRepository.findById('123');
|
|
62
|
+
|
|
63
|
+
// Update a user
|
|
64
|
+
await userRepository.update('123', {
|
|
65
|
+
email: 'newemail@example.com'
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Delete a user
|
|
69
|
+
await userRepository.delete('123');
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
## API Reference
|
|
73
|
+
|
|
74
|
+
### Constructor
|
|
75
|
+
```
|
|
76
|
+
typescript
|
|
77
|
+
new DynamoDbRepository<T>(client: DynamoDBClient, tableName: string, partitionKey: string, sortKey?: string)
|
|
78
|
+
```
|
|
79
|
+
### Methods
|
|
80
|
+
|
|
81
|
+
- `create(item: T): Promise<T>` - Create a new item
|
|
82
|
+
- `findById(id: string): Promise<T | null>` - Find item by partition key
|
|
83
|
+
- `update(id: string, updates: Partial<T>): Promise<T>` - Update an existing item
|
|
84
|
+
- `delete(id: string): Promise<void>` - Delete an item
|
|
85
|
+
- `findAll(): Promise<T[]>` - Retrieve all items (use with caution on large tables)
|
|
86
|
+
- `batchCreate(items: T[]): Promise<void>` - Create multiple items in batch
|
|
87
|
+
- `query(options: QueryOptions): Promise<T[]>` - Query items with custom conditions
|
|
88
|
+
|
|
89
|
+
## Development
|
|
90
|
+
|
|
91
|
+
### Setup
|
|
92
|
+
```sh
|
|
93
|
+
# Install dependencies
|
|
94
|
+
npm install
|
|
95
|
+
|
|
96
|
+
# Run linter
|
|
97
|
+
npm run lint
|
|
98
|
+
|
|
99
|
+
# Fix linting issues automatically
|
|
100
|
+
npm run lint:fix
|
|
101
|
+
|
|
102
|
+
# Run tests
|
|
103
|
+
npm test
|
|
104
|
+
|
|
105
|
+
# Run tests in watch mode
|
|
106
|
+
npm run test:watch
|
|
107
|
+
|
|
108
|
+
# Run tests with coverage report
|
|
109
|
+
npm run test:coverage
|
|
110
|
+
|
|
111
|
+
# Build the project
|
|
112
|
+
npm run build
|
|
113
|
+
```
|
|
114
|
+
### Testing
|
|
115
|
+
|
|
116
|
+
The project uses Jest with Testcontainers for integration testing against a real DynamoDB instance:
|
|
117
|
+
```sh
|
|
118
|
+
npm test
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
### Run tests with coverage
|
|
122
|
+
|
|
123
|
+
Generate coverage report
|
|
124
|
+
|
|
125
|
+
```sh
|
|
126
|
+
npm run test:coverage
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Coverage reports are generated in the coverage/ directory:
|
|
130
|
+
* coverage/lcov-report/index.html - Interactive HTML report
|
|
131
|
+
* coverage/lcov.info - LCOV format for CI/CD integration
|
|
132
|
+
|
|
133
|
+
View HTML coverage report, open coverage/lcov-report/index.html
|
|
134
|
+
|
|
135
|
+
## Configuration
|
|
136
|
+
|
|
137
|
+
### AWS Credentials
|
|
138
|
+
|
|
139
|
+
Ensure your AWS credentials are configured via:
|
|
140
|
+
- Environment variables (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`)
|
|
141
|
+
- AWS credentials file (`~/.aws/credentials`)
|
|
142
|
+
- IAM role (when running on AWS infrastructure)
|
|
143
|
+
|
|
144
|
+
### Required IAM Permissions
|
|
145
|
+
|
|
146
|
+
```json
|
|
147
|
+
{
|
|
148
|
+
"Version": "2012-10-17",
|
|
149
|
+
"Statement": [
|
|
150
|
+
{
|
|
151
|
+
"Effect": "Allow",
|
|
152
|
+
"Action": [
|
|
153
|
+
"dynamodb:PutItem",
|
|
154
|
+
"dynamodb:GetItem",
|
|
155
|
+
"dynamodb:UpdateItem",
|
|
156
|
+
"dynamodb:DeleteItem",
|
|
157
|
+
"dynamodb:Query",
|
|
158
|
+
"dynamodb:Scan",
|
|
159
|
+
"dynamodb:BatchWriteItem"
|
|
160
|
+
],
|
|
161
|
+
"Resource": "arn:aws:dynamodb:*:*:table/your-table-name"
|
|
162
|
+
}
|
|
163
|
+
]
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
## Contributing
|
|
168
|
+
|
|
169
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
### Commit message convention
|
|
173
|
+
Semantic release uses conventional commits. Your commit messages should follow this format:
|
|
174
|
+
|
|
175
|
+
* feat: new feature → triggers minor version bump (1.x.0)
|
|
176
|
+
* fix: bug fix → triggers patch version bump (1.0.x)
|
|
177
|
+
* perf: performance improvement → triggers patch version bump
|
|
178
|
+
* docs: documentation change → no release
|
|
179
|
+
* chore: maintenance task → no release
|
|
180
|
+
* BREAKING CHANGE: in footer → triggers major version bump (x.0.0)
|
|
181
|
+
|
|
182
|
+
Example:
|
|
183
|
+
|
|
184
|
+
> feat: add batch write support
|
|
185
|
+
>
|
|
186
|
+
> Added support for batch write operations to improve performance
|
|
187
|
+
|
|
188
|
+
Or with breaking change:
|
|
189
|
+
|
|
190
|
+
> feat: change repository API
|
|
191
|
+
>
|
|
192
|
+
> BREAKING CHANGE: The query method now returns a Promise instead of an Observable
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
## License
|
|
196
|
+
|
|
197
|
+
MIT
|
|
198
|
+
|
|
199
|
+
## Support
|
|
200
|
+
|
|
201
|
+
For issues and questions, please open an issue on the GitHub repository.
|
|
202
|
+
|
|
@@ -0,0 +1,239 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
|
|
3
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
4
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
5
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
6
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
7
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
8
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
9
|
+
});
|
|
10
|
+
};
|
|
11
|
+
var __rest = (this && this.__rest) || function (s, e) {
|
|
12
|
+
var t = {};
|
|
13
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
|
|
14
|
+
t[p] = s[p];
|
|
15
|
+
if (s != null && typeof Object.getOwnPropertySymbols === "function")
|
|
16
|
+
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
|
|
17
|
+
if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
|
|
18
|
+
t[p[i]] = s[p[i]];
|
|
19
|
+
}
|
|
20
|
+
return t;
|
|
21
|
+
};
|
|
22
|
+
var __asyncValues = (this && this.__asyncValues) || function (o) {
|
|
23
|
+
if (!Symbol.asyncIterator) throw new TypeError("Symbol.asyncIterator is not defined.");
|
|
24
|
+
var m = o[Symbol.asyncIterator], i;
|
|
25
|
+
return m ? m.call(o) : (o = typeof __values === "function" ? __values(o) : o[Symbol.iterator](), i = {}, verb("next"), verb("throw"), verb("return"), i[Symbol.asyncIterator] = function () { return this; }, i);
|
|
26
|
+
function verb(n) { i[n] = o[n] && function (v) { return new Promise(function (resolve, reject) { v = o[n](v), settle(resolve, reject, v.done, v.value); }); }; }
|
|
27
|
+
function settle(resolve, reject, d, v) { Promise.resolve(v).then(function(v) { resolve({ value: v, done: d }); }, reject); }
|
|
28
|
+
};
|
|
29
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
30
|
+
exports.DynamoDbRepository = exports.mapFilterExpressions = exports.mapFilterExpression = exports.FilterOperator = void 0;
|
|
31
|
+
const client_dynamodb_1 = require("@aws-sdk/client-dynamodb");
|
|
32
|
+
const util_dynamodb_1 = require("@aws-sdk/util-dynamodb");
|
|
33
|
+
const lodash_1 = require("lodash");
|
|
34
|
+
const expressionAttributeKey = (key) => (0, lodash_1.replace)(key, /-/g, "_");
|
|
35
|
+
var FilterOperator;
|
|
36
|
+
(function (FilterOperator) {
|
|
37
|
+
FilterOperator["EQUALS"] = "=";
|
|
38
|
+
FilterOperator["NOT_EQUALS"] = "<>";
|
|
39
|
+
FilterOperator["GREATER_THAN_OR_EQUALS"] = ">=";
|
|
40
|
+
FilterOperator["GREATER_THAN"] = ">";
|
|
41
|
+
FilterOperator["LESS_THAN"] = "<";
|
|
42
|
+
FilterOperator["LESS_THAN_OR_EQUALS"] = "<=";
|
|
43
|
+
FilterOperator["IN"] = "IN";
|
|
44
|
+
FilterOperator["BETWEEN"] = "BETWEEN";
|
|
45
|
+
})(FilterOperator || (exports.FilterOperator = FilterOperator = {}));
|
|
46
|
+
const mapInKeys = (filterExpression) => Array.isArray(filterExpression.value)
|
|
47
|
+
? filterExpression.value.map((_, index) => `:${filterExpression.attribute}${index}`)
|
|
48
|
+
: `:${filterExpression.attribute}`;
|
|
49
|
+
const mapFilterExpression = (filterExpression) => {
|
|
50
|
+
switch (filterExpression.operator) {
|
|
51
|
+
case FilterOperator.IN:
|
|
52
|
+
return (`#${expressionAttributeKey(filterExpression.attribute)} ${filterExpression.operator} ` +
|
|
53
|
+
`(${mapInKeys(filterExpression)})`);
|
|
54
|
+
case FilterOperator.BETWEEN:
|
|
55
|
+
return (`#${expressionAttributeKey(filterExpression.attribute)} ${filterExpression.operator} ` +
|
|
56
|
+
`:${expressionAttributeKey(filterExpression.attribute)}0 AND :${expressionAttributeKey(filterExpression.attribute)}1`);
|
|
57
|
+
default:
|
|
58
|
+
return (`#${expressionAttributeKey(filterExpression.attribute)} ${filterExpression.operator} ` +
|
|
59
|
+
`:${expressionAttributeKey(filterExpression.attribute)}`);
|
|
60
|
+
}
|
|
61
|
+
};
|
|
62
|
+
exports.mapFilterExpression = mapFilterExpression;
|
|
63
|
+
const mapFilterExpressions = (filterExpressions) => filterExpressions
|
|
64
|
+
.map((filterExpression) => filterExpression.negate
|
|
65
|
+
? `NOT ${(0, exports.mapFilterExpression)(filterExpression)}`
|
|
66
|
+
: (0, exports.mapFilterExpression)(filterExpression))
|
|
67
|
+
.join(" AND ");
|
|
68
|
+
exports.mapFilterExpressions = mapFilterExpressions;
|
|
69
|
+
const mapFilterExpressionValues = (filterExpression) => Array.isArray(filterExpression.value)
|
|
70
|
+
? filterExpression.value.reduce((reduction, value, index) => (Object.assign(Object.assign({}, reduction), { [`:${expressionAttributeKey(filterExpression.attribute)}${index}`]: value })), Object.assign({}))
|
|
71
|
+
: {
|
|
72
|
+
[`:${expressionAttributeKey(filterExpression.attribute)}`]: filterExpression.value,
|
|
73
|
+
};
|
|
74
|
+
const paginate = (array, pageSize) => {
|
|
75
|
+
return array.reduce((acc, val, i) => {
|
|
76
|
+
const idx = Math.floor(i / pageSize);
|
|
77
|
+
const page = acc[idx] || (acc[idx] = []);
|
|
78
|
+
page.push(val);
|
|
79
|
+
return acc;
|
|
80
|
+
}, []);
|
|
81
|
+
};
|
|
82
|
+
class DynamoDbRepository {
|
|
83
|
+
constructor(dynamoDBClient, tableName, hashKey, rangKey) {
|
|
84
|
+
this.dynamoDBClient = dynamoDBClient;
|
|
85
|
+
this.tableName = tableName;
|
|
86
|
+
this.hashKey = hashKey;
|
|
87
|
+
this.rangKey = rangKey;
|
|
88
|
+
this.getItem = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
89
|
+
return this.dynamoDBClient
|
|
90
|
+
.send(new client_dynamodb_1.GetItemCommand({
|
|
91
|
+
TableName: this.tableName,
|
|
92
|
+
Key: (0, util_dynamodb_1.marshall)(key, { removeUndefinedValues: true }),
|
|
93
|
+
}))
|
|
94
|
+
.then((result) => result.Item ? (0, util_dynamodb_1.unmarshall)(result.Item) : undefined);
|
|
95
|
+
});
|
|
96
|
+
this.putItem = (key, record) => __awaiter(this, void 0, void 0, function* () {
|
|
97
|
+
const Item = (0, util_dynamodb_1.marshall)(Object.assign(Object.assign({}, record), key), { removeUndefinedValues: true });
|
|
98
|
+
return this.dynamoDBClient
|
|
99
|
+
.send(new client_dynamodb_1.PutItemCommand({
|
|
100
|
+
TableName: this.tableName,
|
|
101
|
+
Item,
|
|
102
|
+
}))
|
|
103
|
+
.then(() => this.getItem(key));
|
|
104
|
+
});
|
|
105
|
+
this.deleteItem = (key) => __awaiter(this, void 0, void 0, function* () {
|
|
106
|
+
return this.dynamoDBClient.send(new client_dynamodb_1.DeleteItemCommand({
|
|
107
|
+
TableName: this.tableName,
|
|
108
|
+
Key: (0, util_dynamodb_1.marshall)(key),
|
|
109
|
+
})).then((result) => result.Attributes ?
|
|
110
|
+
(0, util_dynamodb_1.unmarshall)(result.Attributes) : undefined);
|
|
111
|
+
});
|
|
112
|
+
this.updateItem = (key, updates, remove) => __awaiter(this, void 0, void 0, function* () {
|
|
113
|
+
const hasUpdates = Object.keys(updates).length > 0;
|
|
114
|
+
const setAttributesExpression = hasUpdates ? `SET ${Object.entries(updates)
|
|
115
|
+
.filter(([, value]) => value !== undefined)
|
|
116
|
+
.map(([key]) => `#${expressionAttributeKey(key)} = :${expressionAttributeKey(key)}`)
|
|
117
|
+
.join(", ")}` : '';
|
|
118
|
+
const removeAttributesExpression = remove
|
|
119
|
+
? ` REMOVE ${remove.map((key) => `#${expressionAttributeKey(key)}`).join(", ")}`
|
|
120
|
+
: "";
|
|
121
|
+
const removeAttributeNames = remove
|
|
122
|
+
? remove.map(expressionAttributeKey).reduce((acc, key) => (Object.assign(Object.assign({}, acc), { [`#${expressionAttributeKey(key)}`]: key })), {})
|
|
123
|
+
: {};
|
|
124
|
+
const updateItemCommandInput = {
|
|
125
|
+
TableName: this.tableName,
|
|
126
|
+
Key: (0, util_dynamodb_1.marshall)(key),
|
|
127
|
+
UpdateExpression: `${setAttributesExpression}${removeAttributesExpression}`,
|
|
128
|
+
ExpressionAttributeNames: Object.entries(updates)
|
|
129
|
+
.filter(([, value]) => value !== undefined)
|
|
130
|
+
.reduce((acc, [key]) => (Object.assign(Object.assign({}, acc), { [`#${expressionAttributeKey(key)}`]: key })), Object.assign(removeAttributeNames)),
|
|
131
|
+
ExpressionAttributeValues: hasUpdates ? (0, util_dynamodb_1.marshall)(Object.entries(updates).reduce((acc, [key, value]) => (Object.assign(Object.assign({}, acc), { [`:${expressionAttributeKey(key)}`]: value })), Object.assign({})), { removeUndefinedValues: true }) : undefined,
|
|
132
|
+
};
|
|
133
|
+
return this.dynamoDBClient
|
|
134
|
+
.send(new client_dynamodb_1.UpdateItemCommand(updateItemCommandInput))
|
|
135
|
+
.then(() => this.getItem(key));
|
|
136
|
+
});
|
|
137
|
+
this.getItems = (query) => __awaiter(this, void 0, void 0, function* () {
|
|
138
|
+
var _a, e_1, _b, _c, _d, e_2, _e, _f;
|
|
139
|
+
var _g;
|
|
140
|
+
const { index, filterExpressions, projectedAttributes } = query, keys = __rest(query, ["index", "filterExpressions", "projectedAttributes"]);
|
|
141
|
+
const KeyConditionExpression = Object.keys(keys)
|
|
142
|
+
.map((key) => `#${expressionAttributeKey(key)} = :${expressionAttributeKey(key)}`).join(' AND ');
|
|
143
|
+
const keyExpressionAttributeNames = Object.keys(keys)
|
|
144
|
+
.reduce((acc, key) => (Object.assign(Object.assign({}, acc), { [`#${expressionAttributeKey(key)}`]: key })), Object.assign({}));
|
|
145
|
+
const keyExpressionAttributeValues = Object.entries(keys)
|
|
146
|
+
.reduce((acc, [key, value]) => (Object.assign(Object.assign({}, acc), { [`:${expressionAttributeKey(key)}`]: value })), Object.assign({}));
|
|
147
|
+
const ProjectionExpression = !index && projectedAttributes
|
|
148
|
+
? projectedAttributes.map((attribute) => `#${expressionAttributeKey(attribute)}`).join(',')
|
|
149
|
+
: undefined;
|
|
150
|
+
const projectionAttributeNames = !index && projectedAttributes ? projectedAttributes.reduce((reduction, attribute) => (Object.assign(Object.assign({}, reduction), { [`#${expressionAttributeKey(attribute)}`]: attribute })), Object.assign({})) : {};
|
|
151
|
+
const hasFilterExpressions = Array.isArray(filterExpressions) && filterExpressions.length > 0;
|
|
152
|
+
const FilterExpression = hasFilterExpressions
|
|
153
|
+
? (0, exports.mapFilterExpressions)(filterExpressions)
|
|
154
|
+
: undefined;
|
|
155
|
+
const filterAttributeNames = hasFilterExpressions
|
|
156
|
+
? filterExpressions.reduce((reduction, filterExpression) => (Object.assign(Object.assign({}, reduction), { [`#${expressionAttributeKey(filterExpression.attribute)}`]: filterExpression.attribute })), Object.assign({}))
|
|
157
|
+
: {};
|
|
158
|
+
const filterAttributeValues = filterExpressions
|
|
159
|
+
? filterExpressions.reduce((reduction, filterExpression) => (Object.assign(Object.assign({}, reduction), mapFilterExpressionValues(filterExpression))), Object.assign({}))
|
|
160
|
+
: {};
|
|
161
|
+
const queryCommandInput = {
|
|
162
|
+
TableName: this.tableName,
|
|
163
|
+
IndexName: index,
|
|
164
|
+
KeyConditionExpression,
|
|
165
|
+
FilterExpression,
|
|
166
|
+
ProjectionExpression,
|
|
167
|
+
ExpressionAttributeNames: Object.assign(Object.assign(Object.assign({}, keyExpressionAttributeNames), filterAttributeNames), projectionAttributeNames),
|
|
168
|
+
ExpressionAttributeValues: (0, util_dynamodb_1.marshall)(Object.assign(Object.assign({}, keyExpressionAttributeValues), filterAttributeValues), { removeUndefinedValues: true }),
|
|
169
|
+
};
|
|
170
|
+
const paginator = (0, client_dynamodb_1.paginateQuery)({ client: this.dynamoDBClient, pageSize: 100 }, queryCommandInput);
|
|
171
|
+
if (index) {
|
|
172
|
+
const keys = [];
|
|
173
|
+
try {
|
|
174
|
+
for (var _h = true, paginator_1 = __asyncValues(paginator), paginator_1_1; paginator_1_1 = yield paginator_1.next(), _a = paginator_1_1.done, !_a; _h = true) {
|
|
175
|
+
_c = paginator_1_1.value;
|
|
176
|
+
_h = false;
|
|
177
|
+
const page = _c;
|
|
178
|
+
if (page.Items) {
|
|
179
|
+
keys.push(...(page.Items.map((item) => (0, util_dynamodb_1.unmarshall)(item))
|
|
180
|
+
.map((item) => (0, lodash_1.pickBy)(item, (_, key) => (key === this.hashKey || key === this.rangKey)))));
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
catch (e_1_1) { e_1 = { error: e_1_1 }; }
|
|
185
|
+
finally {
|
|
186
|
+
try {
|
|
187
|
+
if (!_h && !_a && (_b = paginator_1.return)) yield _b.call(paginator_1);
|
|
188
|
+
}
|
|
189
|
+
finally { if (e_1) throw e_1.error; }
|
|
190
|
+
}
|
|
191
|
+
const items = yield this.batchGetItems(keys, query);
|
|
192
|
+
return items;
|
|
193
|
+
}
|
|
194
|
+
const items = [];
|
|
195
|
+
try {
|
|
196
|
+
for (var _j = true, paginator_2 = __asyncValues(paginator), paginator_2_1; paginator_2_1 = yield paginator_2.next(), _d = paginator_2_1.done, !_d; _j = true) {
|
|
197
|
+
_f = paginator_2_1.value;
|
|
198
|
+
_j = false;
|
|
199
|
+
const page = _f;
|
|
200
|
+
if (page.Items) {
|
|
201
|
+
items.push(...(((_g = page.Items) === null || _g === void 0 ? void 0 : _g.map((item) => (0, util_dynamodb_1.unmarshall)(item))) || []));
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
catch (e_2_1) { e_2 = { error: e_2_1 }; }
|
|
206
|
+
finally {
|
|
207
|
+
try {
|
|
208
|
+
if (!_j && !_d && (_e = paginator_2.return)) yield _e.call(paginator_2);
|
|
209
|
+
}
|
|
210
|
+
finally { if (e_2) throw e_2.error; }
|
|
211
|
+
}
|
|
212
|
+
return items;
|
|
213
|
+
});
|
|
214
|
+
this.batchGetItems = (keys, projectedQuery) => __awaiter(this, void 0, void 0, function* () {
|
|
215
|
+
const uniqueKeys = (0, lodash_1.uniqWith)(keys, lodash_1.isEqual);
|
|
216
|
+
const keyPages = paginate(uniqueKeys, 100);
|
|
217
|
+
const { projectedAttributes } = projectedQuery || {};
|
|
218
|
+
const ProjectionExpression = projectedAttributes
|
|
219
|
+
? projectedAttributes.map((attribute) => `#${expressionAttributeKey(attribute)}`).join(',')
|
|
220
|
+
: undefined;
|
|
221
|
+
const ExpressionAttributeNames = projectedAttributes ?
|
|
222
|
+
projectedAttributes.reduce((reduction, attribute) => (Object.assign(Object.assign({}, reduction), { [`#${expressionAttributeKey(attribute)}`]: attribute })), Object.assign({})) : undefined;
|
|
223
|
+
return Promise.all((keyPages.map((keyPage) => __awaiter(this, void 0, void 0, function* () {
|
|
224
|
+
const batchRequest = {
|
|
225
|
+
RequestItems: {
|
|
226
|
+
[this.tableName]: {
|
|
227
|
+
Keys: keyPage.map((key) => ((0, util_dynamodb_1.marshall)(key))),
|
|
228
|
+
ProjectionExpression,
|
|
229
|
+
ExpressionAttributeNames
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
return this.dynamoDBClient.send(new client_dynamodb_1.BatchGetItemCommand(batchRequest)).then(result => { var _a; return (_a = result.Responses) === null || _a === void 0 ? void 0 : _a[this.tableName].map((item) => (0, util_dynamodb_1.unmarshall)(item)); });
|
|
234
|
+
}))))
|
|
235
|
+
.then((itemSets) => itemSets.flat());
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
exports.DynamoDbRepository = DynamoDbRepository;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
14
|
+
for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
|
|
15
|
+
};
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
__exportStar(require("./DynamoDbRepository"), exports);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import eslint from '@eslint/js';
|
|
2
|
+
import { defineConfig, globalIgnores } from 'eslint/config';
|
|
3
|
+
|
|
4
|
+
import tseslint from 'typescript-eslint';
|
|
5
|
+
|
|
6
|
+
export default defineConfig([
|
|
7
|
+
globalIgnores([
|
|
8
|
+
"node_modules/*", // ignore its content
|
|
9
|
+
"dist/*",
|
|
10
|
+
])],
|
|
11
|
+
eslint.configs.recommended,
|
|
12
|
+
tseslint.configs.recommended,
|
|
13
|
+
);
|
package/jest.config.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import type { Config } from '@jest/types';
|
|
2
|
+
|
|
3
|
+
const config: Config.InitialOptions = {
|
|
4
|
+
preset: 'ts-jest',
|
|
5
|
+
testEnvironment: 'node',
|
|
6
|
+
roots: ['<rootDir>/src', '<rootDir>/test'],
|
|
7
|
+
testMatch: ['**/*.test.ts', '**/*.spec.ts'],
|
|
8
|
+
collectCoverage: false, // Set to true by default if you want coverage on every run
|
|
9
|
+
collectCoverageFrom: [
|
|
10
|
+
'src/**/*.ts',
|
|
11
|
+
'!src/**/*.d.ts',
|
|
12
|
+
'!src/**/*.interface.ts',
|
|
13
|
+
'!src/**/index.ts', // Typically just exports, can be excluded
|
|
14
|
+
],
|
|
15
|
+
coverageDirectory: 'coverage',
|
|
16
|
+
coverageReporters: [
|
|
17
|
+
'text', // Console output
|
|
18
|
+
'text-summary', // Summary in console
|
|
19
|
+
'html', // HTML report in coverage/
|
|
20
|
+
'lcov', // For CI/CD tools
|
|
21
|
+
'json', // JSON format
|
|
22
|
+
],
|
|
23
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'],
|
|
24
|
+
verbose: true,
|
|
25
|
+
testTimeout: 60000
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
export default config;
|