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.
@@ -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 pagination for queries, **betterddb** lets you work with DynamoDB using definitions instead of ad hoc query code.
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
- Below is an example of using betterddb for a User entity with a compound key.
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() if you want to allow extra keys (e.g. computed keys).
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 (for example, using LocalStack)
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 a new user.
68
- const newUser = await userDdb.create({
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
- // Query by primary key with an optional sort key condition.
77
- const { items, lastKey } = await userDdb.queryByPrimaryKey(
78
- { tenantId: 'tenant1' },
79
- { operator: 'begins_with', values: 'USER#user' },
80
- { limit: 10 }
81
- );
82
- console.log('Queried Items:', items);
83
- if (lastKey) {
84
- console.log('More items available. Use lastKey for pagination:', lastKey);
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
- ## API
90
- betterddb exposes a generic class BetterDDB<T> with methods for:
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
- License
213
+ ---
214
+
215
+ ## License
216
+
111
217
  MIT
@@ -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
- protected buildKey(rawKey: Partial<T>): Record<string, any>;
97
+ buildKey(rawKey: Partial<T>): Record<string, any>;
85
98
  /**
86
99
  * Build index attributes for each defined GSI.
87
100
  */
88
- protected buildIndexes(rawItem: Partial<T>): Record<string, any>;
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): Promise<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
- * Query by primary key (using the computed primary key) and an optional sort key condition.
110
+ * Get an item by its primary key.
107
111
  */
108
- queryByPrimaryKey(rawKey: Partial<T>, sortKeyCondition?: {
109
- operator: 'eq' | 'begins_with' | 'between';
110
- values: any | [any, any];
111
- }, options?: {
112
- limit?: number;
113
- lastKey?: Record<string, any>;
114
- }): Promise<{
115
- items: T[];
116
- lastKey?: Record<string, any>;
117
- }>;
118
- buildTransactPut(item: T): DynamoDB.DocumentClient.TransactWriteItem;
119
- buildTransactUpdate(rawKey: Partial<T>, update: Partial<T>, options?: {
120
- expectedVersion?: number;
121
- }): DynamoDB.DocumentClient.TransactWriteItem;
122
- buildTransactDelete(rawKey: Partial<T>): DynamoDB.DocumentClient.TransactWriteItem;
123
- transactWrite(operations: DynamoDB.DocumentClient.TransactWriteItemList): Promise<void>;
124
- transactGetByKeys(rawKeys: Partial<T>[]): Promise<T[]>;
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
  }