betterddb 0.2.0 → 0.4.0
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/npm-publish.yml +33 -0
- package/README.md +152 -46
- package/lib/betterddb.d.ts +34 -41
- package/lib/betterddb.js +47 -363
- package/lib/builders/create-builder.d.ts +14 -0
- package/lib/builders/create-builder.js +74 -0
- package/lib/builders/delete-builder.d.ts +19 -0
- package/lib/builders/delete-builder.js +80 -0
- package/lib/builders/get-builder.d.ts +20 -0
- package/lib/builders/get-builder.js +79 -0
- package/lib/builders/query-builder.d.ts +27 -0
- package/lib/builders/query-builder.js +106 -0
- package/lib/builders/scan-builder.d.ts +20 -0
- package/lib/builders/scan-builder.js +76 -0
- package/lib/builders/update-builder.d.ts +35 -0
- package/lib/builders/update-builder.js +205 -0
- package/package.json +1 -1
- package/src/betterddb.ts +62 -424
- package/src/builders/create-builder.ts +82 -0
- package/src/builders/delete-builder.ts +84 -0
- package/src/builders/get-builder.ts +83 -0
- package/src/builders/query-builder.ts +127 -0
- package/src/builders/scan-builder.ts +90 -0
- package/src/builders/update-builder.ts +230 -0
- package/test/create.test.ts +59 -0
- package/test/delete.test.ts +58 -0
- package/test/get.test.ts +58 -0
- package/test/query.test.ts +73 -0
- package/test/scan.test.ts +66 -0
- package/test/update.test.ts +58 -0
- package/test/utils/table-setup.ts +55 -0
- package/test/placeholder.test.ts +0 -98
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# This workflow will run tests using node and then publish a package to GitHub Packages when a release is created
|
|
2
|
+
# For more information see: https://docs.github.com/en/actions/publishing-packages/publishing-nodejs-packages
|
|
3
|
+
|
|
4
|
+
name: Node.js Package
|
|
5
|
+
|
|
6
|
+
on:
|
|
7
|
+
release:
|
|
8
|
+
types: [created]
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
build:
|
|
12
|
+
runs-on: ubuntu-latest
|
|
13
|
+
steps:
|
|
14
|
+
- uses: actions/checkout@v4
|
|
15
|
+
- uses: actions/setup-node@v4
|
|
16
|
+
with:
|
|
17
|
+
node-version: 20
|
|
18
|
+
- run: npm ci
|
|
19
|
+
- run: npm test
|
|
20
|
+
|
|
21
|
+
publish-npm:
|
|
22
|
+
needs: build
|
|
23
|
+
runs-on: ubuntu-latest
|
|
24
|
+
steps:
|
|
25
|
+
- uses: actions/checkout@v4
|
|
26
|
+
- uses: actions/setup-node@v4
|
|
27
|
+
with:
|
|
28
|
+
node-version: 20
|
|
29
|
+
registry-url: https://registry.npmjs.org/
|
|
30
|
+
- run: npm ci
|
|
31
|
+
- run: npm publish
|
|
32
|
+
env:
|
|
33
|
+
NODE_AUTH_TOKEN: ${{secrets.npm_token}}
|
package/README.md
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
|
-
# betterddb
|
|
1
|
+
# betterddb [IN DEVELOPMENT - NOT READY FOR PRODUCTION - BREAKING CHANGES - PLEASE FOR THE LOVE OF GOD DO NOT USE]
|
|
2
2
|
|
|
3
|
-
**betterddb** is a definition-based DynamoDB wrapper library written in TypeScript. It provides a generic, schema-driven Data Access Layer (DAL) using [Zod](https://github.com/colinhacks/zod) for runtime validation and the AWS SDK for DynamoDB operations. With built-in support for compound keys, computed indexes, automatic timestamp injection, transactional and batch operations, and
|
|
3
|
+
**betterddb** is a definition-based DynamoDB wrapper library written in TypeScript. It provides a generic, schema-driven Data Access Layer (DAL) using [Zod](https://github.com/colinhacks/zod) for runtime validation and the AWS SDK for DynamoDB operations. With built-in support for compound keys, computed indexes, automatic timestamp injection, transactional and batch operations, and a fluent builder API for all CRUD operations (create, get, update, delete) as well as queries and scans, **betterddb** lets you work with DynamoDB using definitions instead of ad hoc query code.
|
|
4
|
+
|
|
5
|
+
---
|
|
4
6
|
|
|
5
7
|
## Installation
|
|
6
8
|
|
|
@@ -8,15 +10,18 @@
|
|
|
8
10
|
npm install betterddb
|
|
9
11
|
```
|
|
10
12
|
|
|
13
|
+
---
|
|
14
|
+
|
|
11
15
|
## Usage Example
|
|
12
|
-
|
|
16
|
+
|
|
17
|
+
Below is an example of using **betterddb** for a User entity with a compound key, and using the new fluent builder APIs for create, get, update, and delete, as well as for query and scan operations.
|
|
13
18
|
|
|
14
19
|
```ts
|
|
15
20
|
import { BetterDDB } from 'betterddb';
|
|
16
21
|
import { z } from 'zod';
|
|
17
22
|
import { DynamoDB } from 'aws-sdk';
|
|
18
23
|
|
|
19
|
-
// Define the User schema. Use .passthrough()
|
|
24
|
+
// Define the User schema. Use .passthrough() to allow computed keys.
|
|
20
25
|
const UserSchema = z.object({
|
|
21
26
|
tenantId: z.string(),
|
|
22
27
|
userId: z.string(),
|
|
@@ -25,9 +30,9 @@ const UserSchema = z.object({
|
|
|
25
30
|
createdAt: z.string(),
|
|
26
31
|
updatedAt: z.string(),
|
|
27
32
|
version: z.number().optional()
|
|
28
|
-
});
|
|
33
|
+
}).passthrough();
|
|
29
34
|
|
|
30
|
-
// Configure the DynamoDB DocumentClient (
|
|
35
|
+
// Configure the DynamoDB DocumentClient (example using LocalStack)
|
|
31
36
|
const client = new DynamoDB.DocumentClient({
|
|
32
37
|
region: 'us-east-1',
|
|
33
38
|
endpoint: 'http://localhost:4566'
|
|
@@ -40,72 +45,173 @@ const userDdb = new BetterDDB({
|
|
|
40
45
|
keys: {
|
|
41
46
|
primary: {
|
|
42
47
|
name: 'pk',
|
|
43
|
-
// Compute the partition key from tenantId
|
|
48
|
+
// Compute the partition key from tenantId.
|
|
44
49
|
definition: { build: (raw) => `TENANT#${raw.tenantId}` }
|
|
45
50
|
},
|
|
46
51
|
sort: {
|
|
47
52
|
name: 'sk',
|
|
48
|
-
// Compute the sort key from userId
|
|
53
|
+
// Compute the sort key from userId.
|
|
49
54
|
definition: { build: (raw) => `USER#${raw.userId}` }
|
|
50
55
|
},
|
|
51
56
|
gsis: {
|
|
52
57
|
// Example: a Global Secondary Index on email.
|
|
53
58
|
EmailIndex: {
|
|
54
|
-
primary: {
|
|
55
|
-
name: 'email',
|
|
56
|
-
definition: 'email'
|
|
57
|
-
}
|
|
59
|
+
primary: { name: 'email', definition: 'email' }
|
|
58
60
|
}
|
|
59
61
|
}
|
|
60
62
|
},
|
|
61
63
|
client,
|
|
62
|
-
autoTimestamps: true
|
|
64
|
+
autoTimestamps: true,
|
|
65
|
+
entityName: 'USER'
|
|
63
66
|
});
|
|
64
67
|
|
|
65
|
-
// Use the BetterDDB instance to create and query items.
|
|
66
68
|
(async () => {
|
|
67
|
-
// Create
|
|
68
|
-
|
|
69
|
+
// ### Create Operation ###
|
|
70
|
+
// Use the CreateBuilder to build and execute a create operation.
|
|
71
|
+
const newUser = await userDdb.createBuilder({
|
|
69
72
|
tenantId: 'tenant1',
|
|
70
73
|
userId: 'user123',
|
|
71
74
|
email: 'user@example.com',
|
|
72
75
|
name: 'Alice'
|
|
73
|
-
});
|
|
76
|
+
}).execute();
|
|
74
77
|
console.log('Created User:', newUser);
|
|
75
78
|
|
|
76
|
-
//
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
);
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
79
|
+
// ### Get Operation ###
|
|
80
|
+
// Use the GetBuilder to retrieve an item. Optionally, use a projection.
|
|
81
|
+
const user = await userDdb.getBuilder({ id: 'user123' })
|
|
82
|
+
.withProjection(['name', 'email'])
|
|
83
|
+
.execute();
|
|
84
|
+
console.log('Retrieved User:', user);
|
|
85
|
+
|
|
86
|
+
// ### Update Operation ###
|
|
87
|
+
// Use the UpdateBuilder to perform a fluent update.
|
|
88
|
+
const updatedUser = await userDdb.update({ tenantId: 'tenant1', userId: 'user123' }, 1)
|
|
89
|
+
.set({ name: 'Jane Doe' })
|
|
90
|
+
.remove(['obsoleteAttribute'])
|
|
91
|
+
.execute();
|
|
92
|
+
console.log('Updated User (immediate):', updatedUser);
|
|
93
|
+
|
|
94
|
+
// Or build a transaction update item and include it in a transaction:
|
|
95
|
+
const transactionUpdateItem = userDdb.update({ tenantId: 'tenant1', userId: 'user123' }, 1)
|
|
96
|
+
.set({ name: 'Jane Doe' })
|
|
97
|
+
.remove(['obsoleteAttribute'])
|
|
98
|
+
.toTransactUpdate();
|
|
99
|
+
// Assume transactWrite is available on BetterDDB for executing a transaction.
|
|
100
|
+
await userDdb.transactWrite([transactionUpdateItem]);
|
|
101
|
+
console.log('Updated User (transaction) executed.');
|
|
102
|
+
|
|
103
|
+
// ### Delete Operation ###
|
|
104
|
+
// Use the DeleteBuilder to delete an item with an optional condition.
|
|
105
|
+
await userDdb.deleteBuilder({ id: 'user123' })
|
|
106
|
+
.withCondition('#status = :expected', { ':expected': 'inactive' })
|
|
107
|
+
.execute();
|
|
108
|
+
console.log('User deleted');
|
|
109
|
+
|
|
110
|
+
// ### Query Operation ###
|
|
111
|
+
// Use the fluent QueryBuilder to query items.
|
|
112
|
+
const queryResults = await userDdb.query({ tenantId: 'tenant1' })
|
|
113
|
+
.where('name', 'begins_with', 'John')
|
|
114
|
+
.limitResults(10);
|
|
115
|
+
console.log('Query Results:', queryResults);
|
|
116
|
+
|
|
117
|
+
// ### Scan Operation ###
|
|
118
|
+
// Use the fluent ScanBuilder to scan the table with a filter.
|
|
119
|
+
const scanResults = await userDdb.scan()
|
|
120
|
+
.where('tenantId', 'eq', 'tenant1')
|
|
121
|
+
.limitResults(50);
|
|
122
|
+
console.log('Scan Results:', scanResults);
|
|
86
123
|
})();
|
|
87
124
|
```
|
|
88
125
|
|
|
89
|
-
|
|
90
|
-
|
|
126
|
+
---
|
|
127
|
+
|
|
128
|
+
## API Overview
|
|
129
|
+
|
|
130
|
+
**betterddb** exposes a generic class `BetterDDB<T>` with the following methods:
|
|
131
|
+
|
|
132
|
+
### Fluent CRUD Builders
|
|
133
|
+
|
|
134
|
+
- **CreateBuilder**
|
|
135
|
+
- `createBuilder(item: T): CreateBuilder<T>`
|
|
136
|
+
- Builds a put request with automatic timestamp and key computation.
|
|
137
|
+
- Usage:
|
|
138
|
+
```ts
|
|
139
|
+
await betterDdb.createBuilder(item).execute();
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
- **GetBuilder**
|
|
143
|
+
- `getBuilder(key: Partial<T>): GetBuilder<T>`
|
|
144
|
+
- Builds a get request. Supports projections via `.withProjection()`.
|
|
145
|
+
- Usage:
|
|
146
|
+
```ts
|
|
147
|
+
const result = await betterDdb.getBuilder({ id: 'user123' })
|
|
148
|
+
.withProjection(['name', 'email'])
|
|
149
|
+
.execute();
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
- **DeleteBuilder**
|
|
153
|
+
- `deleteBuilder(key: Partial<T>): DeleteBuilder<T>`
|
|
154
|
+
- Builds a delete request. Supports condition expressions via `.withCondition()`.
|
|
155
|
+
- Usage:
|
|
156
|
+
```ts
|
|
157
|
+
await betterDdb.deleteBuilder({ id: 'user123' })
|
|
158
|
+
.withCondition('#status = :expected', { ':expected': 'inactive' })
|
|
159
|
+
.execute();
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Fluent Update Builder
|
|
163
|
+
|
|
164
|
+
- `update(key: Partial<T>, expectedVersion?: number): UpdateBuilder<T>`
|
|
165
|
+
- Provides chainable methods such as `.set()`, `.remove()`, `.add()`, and `.delete()`.
|
|
166
|
+
- Also supports transaction mode:
|
|
167
|
+
- `.toTransactUpdate()` returns a transaction item.
|
|
168
|
+
- `.transactWrite([...])` allows you to combine update items in a transaction.
|
|
169
|
+
- Usage:
|
|
170
|
+
```ts
|
|
171
|
+
await betterDdb.update({ id: 'user123' }, 1)
|
|
172
|
+
.set({ name: 'Jane Doe' })
|
|
173
|
+
.remove(['obsoleteAttribute'])
|
|
174
|
+
.execute();
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Fluent Query & Scan Builders
|
|
178
|
+
|
|
179
|
+
- **QueryBuilder**
|
|
180
|
+
- `query(key: Partial<T>): QueryBuilder<T>`
|
|
181
|
+
- Allows you to chain conditions (via `.where()`), sort direction, limits, and pagination.
|
|
182
|
+
- Usage:
|
|
183
|
+
```ts
|
|
184
|
+
const results = await betterDdb.query({ tenantId: 'tenant1' })
|
|
185
|
+
.where('name', 'begins_with', 'John')
|
|
186
|
+
.limitResults(10);
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
- **ScanBuilder**
|
|
190
|
+
- `scan(): ScanBuilder<T>`
|
|
191
|
+
- Provides a fluent API to filter and paginate scan operations.
|
|
192
|
+
- Usage:
|
|
193
|
+
```ts
|
|
194
|
+
const results = await betterDdb.scan()
|
|
195
|
+
.where('tenantId', 'eq', 'tenant1')
|
|
196
|
+
.limitResults(50);
|
|
197
|
+
```
|
|
198
|
+
|
|
199
|
+
### Batch and Transaction Operations
|
|
200
|
+
|
|
201
|
+
- **Batch Operations:**
|
|
202
|
+
- `batchWrite(ops: { puts?: T[]; deletes?: Partial<T>[] }): Promise<void>`
|
|
203
|
+
- `batchGet(rawKeys: Partial<T>[]): Promise<T[]>`
|
|
204
|
+
|
|
205
|
+
- **Transaction Helpers:**
|
|
206
|
+
- `buildTransactPut(item: T)`
|
|
207
|
+
- `buildTransactUpdate(rawKey: Partial<T>, update: Partial<T>, options?: { expectedVersion?: number })`
|
|
208
|
+
- `buildTransactDelete(rawKey: Partial<T>)`
|
|
209
|
+
- `transactWrite(...)` and `transactGetByKeys(...)`
|
|
91
210
|
|
|
92
|
-
```ts
|
|
93
|
-
create(item: T): Promise<T>
|
|
94
|
-
get(rawKey: Partial<T>): Promise<T | null>
|
|
95
|
-
update(rawKey: Partial<T>, update: Partial<T>, options?: { expectedVersion?: number }): Promise<T>
|
|
96
|
-
delete(rawKey: Partial<T>): Promise<void>
|
|
97
|
-
queryByGsi(gsiName: string, key: Partial<T>, sortKeyCondition?: { operator: "eq" | "begins_with" | "between"; values: any | [any, any] }): Promise<T[]>
|
|
98
|
-
queryByPrimaryKey(rawKey: Partial<T>, sortKeyCondition?: { operator: "eq" | "begins_with" | "between"; values: any | [any, any] }, options?: { limit?: number; lastKey?: Record<string, any> }): Promise<{ items: T[]; lastKey?: Record<string, any> }>
|
|
99
|
-
Batch operations:
|
|
100
|
-
batchWrite(ops: { puts?: T[]; deletes?: Partial<T>[] }): Promise<void>
|
|
101
|
-
batchGet(rawKeys: Partial<T>[]): Promise<T[]>
|
|
102
|
-
Transaction helper methods:
|
|
103
|
-
buildTransactPut(item: T)
|
|
104
|
-
buildTransactUpdate(rawKey: Partial<T>, update: Partial<T>, options?: { expectedVersion?: number })
|
|
105
|
-
buildTransactDelete(rawKey: Partial<T>)
|
|
106
|
-
transactWrite(...) and transactGetByKeys(...)
|
|
107
211
|
For complete details, please refer to the API documentation.
|
|
108
|
-
```
|
|
109
212
|
|
|
110
|
-
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
111
217
|
MIT
|
package/lib/betterddb.d.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import { ZodSchema } from 'zod';
|
|
2
2
|
import { DynamoDB } from 'aws-sdk';
|
|
3
|
+
import { QueryBuilder } from './builders/query-builder';
|
|
4
|
+
import { ScanBuilder } from './builders/scan-builder';
|
|
5
|
+
import { UpdateBuilder } from './builders/update-builder';
|
|
6
|
+
import { CreateBuilder } from './builders/create-builder';
|
|
7
|
+
import { GetBuilder } from './builders/get-builder';
|
|
8
|
+
import { DeleteBuilder } from './builders/delete-builder';
|
|
3
9
|
export type PrimaryKeyValue = string | number;
|
|
4
10
|
/**
|
|
5
11
|
* A key definition can be either a simple key (a property name)
|
|
@@ -56,6 +62,7 @@ export interface KeysConfig<T> {
|
|
|
56
62
|
export interface BetterDDBOptions<T> {
|
|
57
63
|
schema: ZodSchema<T>;
|
|
58
64
|
tableName: string;
|
|
65
|
+
entityName: string;
|
|
59
66
|
keys: KeysConfig<T>;
|
|
60
67
|
client: DynamoDB.DocumentClient;
|
|
61
68
|
/**
|
|
@@ -73,64 +80,50 @@ export interface BetterDDBOptions<T> {
|
|
|
73
80
|
export declare class BetterDDB<T> {
|
|
74
81
|
protected schema: ZodSchema<T>;
|
|
75
82
|
protected tableName: string;
|
|
83
|
+
protected entityName: string;
|
|
76
84
|
protected client: DynamoDB.DocumentClient;
|
|
77
85
|
protected keys: KeysConfig<T>;
|
|
78
86
|
protected autoTimestamps: boolean;
|
|
79
87
|
constructor(options: BetterDDBOptions<T>);
|
|
88
|
+
getKeys(): KeysConfig<T>;
|
|
89
|
+
getTableName(): string;
|
|
90
|
+
getClient(): DynamoDB.DocumentClient;
|
|
91
|
+
getSchema(): ZodSchema<T>;
|
|
92
|
+
getAutoTimestamps(): boolean;
|
|
80
93
|
protected getKeyValue(def: KeyDefinition<T>, rawKey: Partial<T>): string;
|
|
81
94
|
/**
|
|
82
95
|
* Build the primary key from a raw key object.
|
|
83
96
|
*/
|
|
84
|
-
|
|
97
|
+
buildKey(rawKey: Partial<T>): Record<string, any>;
|
|
85
98
|
/**
|
|
86
99
|
* Build index attributes for each defined GSI.
|
|
87
100
|
*/
|
|
88
|
-
|
|
101
|
+
buildIndexes(rawItem: Partial<T>): Record<string, any>;
|
|
89
102
|
/**
|
|
90
103
|
* Create an item:
|
|
91
104
|
* - Computes primary key and index attributes,
|
|
92
105
|
* - Optionally injects timestamps,
|
|
93
106
|
* - Validates the item and writes it to DynamoDB.
|
|
94
107
|
*/
|
|
95
|
-
create(item: T):
|
|
96
|
-
get(rawKey: Partial<T>): Promise<T | null>;
|
|
97
|
-
update(rawKey: Partial<T>, update: Partial<T>, options?: {
|
|
98
|
-
expectedVersion?: number;
|
|
99
|
-
}): Promise<T>;
|
|
100
|
-
delete(rawKey: Partial<T>): Promise<void>;
|
|
101
|
-
queryByGsi(gsiName: string, key: Partial<T>, sortKeyCondition?: {
|
|
102
|
-
operator: 'eq' | 'begins_with' | 'between';
|
|
103
|
-
values: any | [any, any];
|
|
104
|
-
}): Promise<T[]>;
|
|
108
|
+
create(item: T): CreateBuilder<T>;
|
|
105
109
|
/**
|
|
106
|
-
*
|
|
110
|
+
* Get an item by its primary key.
|
|
107
111
|
*/
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
transactGet(getItems: {
|
|
126
|
-
TableName: string;
|
|
127
|
-
Key: any;
|
|
128
|
-
}[]): Promise<T[]>;
|
|
129
|
-
batchWrite(ops: {
|
|
130
|
-
puts?: T[];
|
|
131
|
-
deletes?: Partial<T>[];
|
|
132
|
-
}): Promise<void>;
|
|
133
|
-
private batchWriteChunk;
|
|
134
|
-
private retryBatchWrite;
|
|
135
|
-
batchGet(rawKeys: Partial<T>[]): Promise<T[]>;
|
|
112
|
+
get(rawKey: Partial<T>): GetBuilder<T>;
|
|
113
|
+
/**
|
|
114
|
+
* Update an item.
|
|
115
|
+
*/
|
|
116
|
+
update(key: Partial<T>, expectedVersion?: number): UpdateBuilder<T>;
|
|
117
|
+
/**
|
|
118
|
+
* Delete an item.
|
|
119
|
+
*/
|
|
120
|
+
delete(rawKey: Partial<T>): DeleteBuilder<T>;
|
|
121
|
+
/**
|
|
122
|
+
* Query items.
|
|
123
|
+
*/
|
|
124
|
+
query(key: Partial<T>): QueryBuilder<T>;
|
|
125
|
+
/**
|
|
126
|
+
* Scan for items.
|
|
127
|
+
*/
|
|
128
|
+
scan(): ScanBuilder<T>;
|
|
136
129
|
}
|