betterddb 0.3.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
@@ -90,7 +90,7 @@ class QueryBuilder {
90
90
  params.FilterExpression = this.filters.join(' AND ');
91
91
  }
92
92
  const result = await this.parent.getClient().query(params).promise();
93
- return (result.Items || []).map(item => this.parent.getSchema().parse(item));
93
+ return this.parent.getSchema().array().parse(result.Items);
94
94
  }
95
95
  // Thenable implementation.
96
96
  then(onfulfilled, onrejected) {
@@ -60,7 +60,7 @@ class ScanBuilder {
60
60
  params.FilterExpression = this.filters.join(' AND ');
61
61
  }
62
62
  const result = await this.parent.getClient().scan(params).promise();
63
- return (result.Items || []).map(item => this.parent.getSchema().parse(item));
63
+ return this.parent.getSchema().array().parse(result.Items);
64
64
  }
65
65
  // Thenable implementation.
66
66
  then(onfulfilled, onrejected) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betterddb",
3
- "version": "0.3.0",
3
+ "version": "0.4.0",
4
4
  "description": "A definition-based DynamoDB wrapper library that provides a schema-driven and fully typesafe DAL.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
package/src/betterddb.ts CHANGED
@@ -1,5 +1,4 @@
1
- // src/dynamo-dal.ts
2
- import { z, ZodSchema } from 'zod';
1
+ import { ZodSchema } from 'zod';
3
2
  import { DynamoDB } from 'aws-sdk';
4
3
  import { QueryBuilder } from './builders/query-builder';
5
4
  import { ScanBuilder } from './builders/scan-builder';
@@ -106,7 +106,7 @@ export class QueryBuilder<T> {
106
106
  }
107
107
 
108
108
  const result = await this.parent.getClient().query(params).promise();
109
- return (result.Items || []).map(item => this.parent.getSchema().parse(item));
109
+ return this.parent.getSchema().array().parse(result.Items);
110
110
  }
111
111
 
112
112
  // Thenable implementation.
@@ -69,7 +69,7 @@ export class ScanBuilder<T> {
69
69
  }
70
70
 
71
71
  const result = await this.parent.getClient().scan(params).promise();
72
- return (result.Items || []).map(item => this.parent.getSchema().parse(item));
72
+ return this.parent.getSchema().array().parse(result.Items);
73
73
  }
74
74
 
75
75
  // Thenable implementation.
@@ -0,0 +1,59 @@
1
+ import { z } from 'zod';
2
+ import { BetterDDB } from '../src/betterddb';
3
+ import { DynamoDB } from 'aws-sdk';
4
+ import { createTestTable, deleteTestTable } from './utils/table-setup';
5
+
6
+ const TEST_TABLE = "create-test-table";
7
+ const ENDPOINT = 'http://localhost:4566';
8
+ const REGION = 'us-east-1';
9
+ const ENTITY_NAME = 'USER';
10
+ const PRIMARY_KEY = 'id';
11
+ const PRIMARY_KEY_TYPE = 'S';
12
+ const SORT_KEY = 'email';
13
+ const SORT_KEY_TYPE = 'S';
14
+ const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }];
15
+ const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }];
16
+ const client = new DynamoDB.DocumentClient({
17
+ region: REGION,
18
+ endpoint: ENDPOINT,
19
+ });
20
+
21
+ const UserSchema = z.object({
22
+ id: z.string(),
23
+ name: z.string(),
24
+ email: z.string().email(),
25
+ createdAt: z.string(),
26
+ updatedAt: z.string(),
27
+ }).passthrough();
28
+
29
+ const userDdb = new BetterDDB({
30
+ schema: UserSchema,
31
+ tableName: TEST_TABLE,
32
+ entityName: ENTITY_NAME,
33
+ keys: {
34
+ primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
35
+ sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
36
+ },
37
+ client,
38
+ autoTimestamps: true,
39
+ });
40
+
41
+ beforeAll(async () => {
42
+ await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
43
+ });
44
+
45
+ afterAll(async () => {
46
+ await deleteTestTable(TEST_TABLE);
47
+ });
48
+
49
+ describe('BetterDDB - Create Operation', () => {
50
+ it('should insert an item using CreateBuilder', async () => {
51
+ const user = { id: 'user-123', name: 'John Doe', email: 'john@example.com' };
52
+ await userDdb.create(user as any).execute();
53
+ const createdUser = await userDdb.get({ id: 'user-123', email: 'john@example.com' }).execute();
54
+ expect(createdUser).not.toBeNull();
55
+ expect(createdUser?.id).toBe('user-123');
56
+ expect(createdUser).toHaveProperty('createdAt');
57
+ expect(createdUser).toHaveProperty('updatedAt');
58
+ });
59
+ });
@@ -0,0 +1,58 @@
1
+ // __tests__/delete.test.ts
2
+ import { z } from 'zod';
3
+ import { BetterDDB } from '../src/betterddb';
4
+ import { DynamoDB } from 'aws-sdk';
5
+ import { createTestTable, deleteTestTable } from './utils/table-setup';
6
+
7
+ const TEST_TABLE = "delete-test-table";
8
+ const ENDPOINT = 'http://localhost:4566';
9
+ const REGION = 'us-east-1';
10
+ const ENTITY_NAME = 'USER';
11
+ const PRIMARY_KEY = 'id';
12
+ const PRIMARY_KEY_TYPE = 'S';
13
+ const SORT_KEY = 'email';
14
+ const SORT_KEY_TYPE = 'S';
15
+ const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }];
16
+ const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }];
17
+ const client = new DynamoDB.DocumentClient({
18
+ region: REGION,
19
+ endpoint: ENDPOINT,
20
+ });
21
+
22
+ const UserSchema = z.object({
23
+ id: z.string(),
24
+ name: z.string(),
25
+ email: z.string().email(),
26
+ createdAt: z.string(),
27
+ updatedAt: z.string(),
28
+ }).passthrough();
29
+
30
+ const userDdb = new BetterDDB({
31
+ schema: UserSchema,
32
+ tableName: TEST_TABLE,
33
+ entityName: ENTITY_NAME,
34
+ keys: {
35
+ primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
36
+ sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
37
+ },
38
+ client,
39
+ autoTimestamps: true,
40
+ });
41
+
42
+ beforeAll(async () => {
43
+ await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
44
+ await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
45
+ });
46
+
47
+ afterAll(async () => {
48
+ await deleteTestTable(TEST_TABLE);
49
+ });
50
+
51
+ describe('BetterDDB - Delete Operation', () => {
52
+ it('should delete an item using DeleteBuilder', async () => {
53
+ await userDdb.delete({ id: 'user-123', email: 'john@example.com' })
54
+ .execute();
55
+ const deletedUser = await userDdb.get({ id: 'user-123', email: 'john@example.com' }).execute();
56
+ expect(deletedUser).toBeNull();
57
+ });
58
+ });
@@ -0,0 +1,58 @@
1
+ // __tests__/get.test.ts
2
+ import { z } from 'zod';
3
+ import { BetterDDB } from '../src/betterddb';
4
+ import { DynamoDB } from 'aws-sdk';
5
+ import { createTestTable, deleteTestTable } from './utils/table-setup';
6
+
7
+ const TEST_TABLE = "get-test-table";
8
+ const ENDPOINT = 'http://localhost:4566';
9
+ const REGION = 'us-east-1';
10
+ const ENTITY_NAME = 'USER';
11
+ const PRIMARY_KEY = 'id';
12
+ const PRIMARY_KEY_TYPE = 'S';
13
+ const SORT_KEY = 'email';
14
+ const SORT_KEY_TYPE = 'S';
15
+ const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }];
16
+ const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }];
17
+ const client = new DynamoDB.DocumentClient({
18
+ region: REGION,
19
+ endpoint: ENDPOINT,
20
+ });
21
+
22
+
23
+ const UserSchema = z.object({
24
+ id: z.string(),
25
+ name: z.string(),
26
+ email: z.string().email(),
27
+ createdAt: z.string(),
28
+ updatedAt: z.string(),
29
+ }).passthrough();
30
+
31
+ const userDdb = new BetterDDB({
32
+ schema: UserSchema,
33
+ tableName: TEST_TABLE,
34
+ entityName: ENTITY_NAME,
35
+ keys: {
36
+ primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
37
+ sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
38
+ },
39
+ client,
40
+ autoTimestamps: true,
41
+ });
42
+
43
+ beforeAll(async () => {
44
+ await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
45
+ await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await deleteTestTable(TEST_TABLE);
50
+ });
51
+
52
+ describe('BetterDDB - Get Operation', () => {
53
+ it('should retrieve an item using GetBuilder', async () => {
54
+ const user = await userDdb.get({ id: 'user-123', email: 'john@example.com' }).execute();
55
+ expect(user).not.toBeNull();
56
+ expect(user?.id).toBe('user-123');
57
+ });
58
+ });
@@ -0,0 +1,73 @@
1
+ import { z } from 'zod';
2
+ import { BetterDDB } from '../src/betterddb';
3
+ import { DynamoDB } from 'aws-sdk';
4
+ import { createTestTable, deleteTestTable } from './utils/table-setup';
5
+
6
+ const TEST_TABLE = "query-test-table";
7
+ const ENDPOINT = 'http://localhost:4566';
8
+ const REGION = 'us-east-1';
9
+ const ENTITY_NAME = 'USER';
10
+ const PRIMARY_KEY = 'id';
11
+ const PRIMARY_KEY_TYPE = 'S';
12
+ const SORT_KEY = 'email';
13
+ const SORT_KEY_TYPE = 'S';
14
+ const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }];
15
+ const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }];
16
+ const client = new DynamoDB.DocumentClient({
17
+ region: REGION,
18
+ endpoint: ENDPOINT,
19
+ });
20
+
21
+ const UserSchema = z.object({
22
+ id: z.string(),
23
+ name: z.string(),
24
+ email: z.string().email(),
25
+ createdAt: z.string(),
26
+ updatedAt: z.string(),
27
+ }).passthrough();
28
+
29
+ const userDdb = new BetterDDB({
30
+ schema: UserSchema,
31
+ tableName: TEST_TABLE,
32
+ entityName: ENTITY_NAME,
33
+ keys: {
34
+ primary: { name: PRIMARY_KEY, definition: PRIMARY_KEY },
35
+ gsis: {
36
+ EmailIndex: {
37
+ name: 'EmailIndex',
38
+ primary: { name: 'email', definition: 'email' }
39
+ }
40
+ }
41
+ },
42
+ client,
43
+ autoTimestamps: true,
44
+ });
45
+
46
+ beforeAll(async () => {
47
+ await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
48
+ // Insert multiple items.
49
+ const items = [
50
+ { id: 'user-1', name: 'Alice', email: 'alice@example.com' },
51
+ { id: 'user-2', name: 'Alice B', email: 'alice@example.com' },
52
+ { id: 'user-3', name: 'Bob', email: 'bob@example.com' }
53
+ ];
54
+ for (const item of items) {
55
+ await userDdb.create(item as any).execute();
56
+ }
57
+ });
58
+
59
+ afterAll(async () => {
60
+ await deleteTestTable(TEST_TABLE);
61
+ });
62
+
63
+ describe('BetterDDB - Query Operation', () => {
64
+ it('should query items using QueryBuilder', async () => {
65
+ const results = await userDdb.query({ id: 'user-1' })
66
+ .where('name', 'begins_with', 'Alice')
67
+ .limitResults(5);
68
+ expect(results.length).toBeGreaterThanOrEqual(1);
69
+ results.forEach(result => {
70
+ expect(result.name).toMatch(/^Alice/);
71
+ });
72
+ });
73
+ });
@@ -0,0 +1,66 @@
1
+ import { z } from 'zod';
2
+ import { BetterDDB } from '../src/betterddb';
3
+ import { DynamoDB } from 'aws-sdk';
4
+ import { createTestTable, deleteTestTable } from './utils/table-setup';
5
+
6
+ const TEST_TABLE = "scan-test-table";
7
+ const ENDPOINT = 'http://localhost:4566';
8
+ const REGION = 'us-east-1';
9
+ const ENTITY_NAME = 'USER';
10
+ const PRIMARY_KEY = 'id';
11
+ const PRIMARY_KEY_TYPE = 'S';
12
+ const SORT_KEY = 'email';
13
+ const SORT_KEY_TYPE = 'S';
14
+ const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }];
15
+ const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }];
16
+ const client = new DynamoDB.DocumentClient({
17
+ region: REGION,
18
+ endpoint: ENDPOINT,
19
+ });
20
+
21
+ const UserSchema = z.object({
22
+ id: z.string(),
23
+ name: z.string(),
24
+ email: z.string().email(),
25
+ createdAt: z.string(),
26
+ updatedAt: z.string(),
27
+ }).passthrough();
28
+
29
+ const userDdb = new BetterDDB({
30
+ schema: UserSchema,
31
+ tableName: TEST_TABLE,
32
+ entityName: ENTITY_NAME,
33
+ keys: {
34
+ primary: { name: PRIMARY_KEY, definition: PRIMARY_KEY },
35
+ },
36
+ client,
37
+ autoTimestamps: true,
38
+ });
39
+
40
+ beforeAll(async () => {
41
+ await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
42
+ // Insert multiple items.
43
+ const items = [
44
+ { id: 'user-4', name: 'Charlie', email: 'charlie@example.com' },
45
+ { id: 'user-5', name: 'Dave', email: 'dave@example.com' }
46
+ ];
47
+ for (const item of items) {
48
+ await userDdb.create(item as any).execute();
49
+ }
50
+ });
51
+
52
+ afterAll(async () => {
53
+ await deleteTestTable(TEST_TABLE);
54
+ });
55
+
56
+ describe('BetterDDB - Scan Operation', () => {
57
+ it('should scan items using ScanBuilder', async () => {
58
+ const results = await userDdb.scan()
59
+ .where('email', 'begins_with', 'char')
60
+ .limitResults(10);
61
+ expect(results.length).toBeGreaterThanOrEqual(1);
62
+ results.forEach(result => {
63
+ expect(result.email).toMatch(/^char/i);
64
+ });
65
+ });
66
+ });
@@ -0,0 +1,58 @@
1
+ import { z } from 'zod';
2
+ import { BetterDDB } from '../src/betterddb';
3
+ import { DynamoDB } from 'aws-sdk';
4
+ import { createTestTable, deleteTestTable } from './utils/table-setup';
5
+
6
+ const TEST_TABLE = "update-test-table";
7
+ const ENDPOINT = 'http://localhost:4566';
8
+ const REGION = 'us-east-1';
9
+ const ENTITY_NAME = 'USER';
10
+ const PRIMARY_KEY = 'id';
11
+ const PRIMARY_KEY_TYPE = 'S';
12
+ const SORT_KEY = 'email';
13
+ const SORT_KEY_TYPE = 'S';
14
+ const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }];
15
+ const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }];
16
+ const client = new DynamoDB.DocumentClient({
17
+ region: REGION,
18
+ endpoint: ENDPOINT,
19
+ });
20
+
21
+
22
+ const UserSchema = z.object({
23
+ id: z.string(),
24
+ name: z.string(),
25
+ email: z.string().email(),
26
+ createdAt: z.string(),
27
+ updatedAt: z.string(),
28
+ version: z.number().optional(),
29
+ }).passthrough();
30
+
31
+ const userDdb = new BetterDDB({
32
+ schema: UserSchema,
33
+ tableName: TEST_TABLE,
34
+ entityName: ENTITY_NAME,
35
+ keys: {
36
+ primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
37
+ sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
38
+ },
39
+ client,
40
+ autoTimestamps: true,
41
+ });
42
+
43
+ beforeAll(async () => {
44
+ await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
45
+ await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
46
+ });
47
+
48
+ afterAll(async () => {
49
+ await deleteTestTable(TEST_TABLE);
50
+ });
51
+
52
+ describe('BetterDDB - Update Operation', () => {
53
+ it('should update an existing item using UpdateBuilder', async () => {
54
+ const updatedUser = await userDdb.update({ id: 'user-123', email: 'john@example.com' }).set({ name: 'Jane Doe' }).execute();
55
+ expect(updatedUser.name).toBe('Jane Doe');
56
+ expect(updatedUser.email).toBe('john@example.com');
57
+ });
58
+ });
@@ -0,0 +1,55 @@
1
+ import { DynamoDB } from 'aws-sdk';
2
+
3
+ export const createTestTable = async (tableName: string, keySchema: DynamoDB.CreateTableInput['KeySchema'], attributeDefinitions: DynamoDB.CreateTableInput['AttributeDefinitions']) => {
4
+ const dynamoDB = new DynamoDB({
5
+ region: 'us-east-1',
6
+ endpoint: 'http://localhost:4566',
7
+ });
8
+
9
+ console.log('Creating DynamoDB table in LocalStack...');
10
+
11
+ try {
12
+ await dynamoDB.createTable({
13
+ TableName: tableName,
14
+ KeySchema: keySchema,
15
+ AttributeDefinitions: attributeDefinitions,
16
+ BillingMode: 'PAY_PER_REQUEST',
17
+ }).promise();
18
+ } catch (error: any) {
19
+ if (error.code === 'ResourceInUseException') {
20
+ console.log('Table already exists, skipping creation.');
21
+ } else {
22
+ throw error;
23
+ }
24
+ }
25
+
26
+ // Wait for the table to become active.
27
+ let attempts = 0;
28
+ while (attempts < 60) { // wait up to 60 seconds
29
+ const { Table } = await dynamoDB.describeTable({ TableName: tableName }).promise();
30
+ if (Table?.TableStatus === 'ACTIVE') {
31
+ console.log('DynamoDB table is ready.');
32
+ return;
33
+ }
34
+ console.log('Waiting for table to become ACTIVE...');
35
+ await new Promise((res) => setTimeout(res, 1000));
36
+ attempts++;
37
+ }
38
+ throw new Error('Table did not become active in time.');
39
+ };
40
+
41
+ export const deleteTestTable = async (tableName: string) => {
42
+ const dynamoDB = new DynamoDB({
43
+ region: 'us-east-1',
44
+ endpoint: 'http://localhost:4566',
45
+ });
46
+ try {
47
+ await dynamoDB.deleteTable({ TableName: tableName }).promise();
48
+ } catch (error: any) {
49
+ if (error.code === 'ResourceNotFoundException') {
50
+ console.log('Table not found during deletion.');
51
+ } else {
52
+ throw error;
53
+ }
54
+ }
55
+ };
@@ -1,99 +0,0 @@
1
- import { z } from 'zod';
2
- import { BetterDDB } from '../src/betterddb';
3
- import { DynamoDB } from 'aws-sdk';
4
-
5
- const TEST_TABLE = 'TestTable';
6
-
7
- // LocalStack Configuration
8
- const client = new DynamoDB.DocumentClient({
9
- region: 'us-east-1',
10
- endpoint: 'http://localhost:4566'
11
- });
12
-
13
- // Table Schema
14
- const UserSchema = z.object({
15
- id: z.string(),
16
- name: z.string(),
17
- email: z.string().email(),
18
- createdAt: z.string(),
19
- updatedAt: z.string()
20
- });
21
-
22
- const userDal = new BetterDDB({
23
- schema: UserSchema,
24
- tableName: TEST_TABLE,
25
- keys: {
26
- primary: { name: 'pk', definition: 'id' }
27
- },
28
- entityName: 'USER',
29
- client,
30
- autoTimestamps: true
31
- });
32
-
33
- beforeAll(async () => {
34
- const dynamoDB = new DynamoDB({
35
- region: 'us-east-1',
36
- endpoint: 'http://localhost:4566'
37
- });
38
-
39
- console.log('Creating DynamoDB table in LocalStack...');
40
-
41
- await dynamoDB.createTable({
42
- TableName: TEST_TABLE,
43
- KeySchema: [{ AttributeName: 'pk', KeyType: 'HASH' }],
44
- AttributeDefinitions: [{ AttributeName: 'pk', AttributeType: 'S' }],
45
- BillingMode: 'PAY_PER_REQUEST'
46
- }).promise();
47
-
48
- // Wait for the table to become active
49
- while (true) {
50
- const { Table } = await dynamoDB.describeTable({ TableName: TEST_TABLE }).promise();
51
- if (Table?.TableStatus === 'ACTIVE') {
52
- console.log('DynamoDB table is ready.');
53
- break;
54
- }
55
- console.log('Waiting for table to become ACTIVE...');
56
- await new Promise(res => setTimeout(res, 1000)); // Wait 1 sec before retrying
57
- }
58
- });
59
-
60
- afterAll(async () => {
61
- // Cleanup: delete the table
62
- const dynamoDB = new DynamoDB({
63
- region: 'us-east-1',
64
- endpoint: 'http://localhost:4566'
65
- });
66
-
67
- await dynamoDB.deleteTable({ TableName: TEST_TABLE }).promise();
68
- });
69
-
70
- describe('BetterDDB - Integration Tests', () => {
71
- it('should insert an item into DynamoDB', async () => {
72
- const user = {
73
- id: 'user-123',
74
- name: 'John Doe',
75
- email: 'john@example.com'
76
- };
77
-
78
- const createdUser = await userDal.create(user as any).execute();
79
- expect(createdUser).toHaveProperty('createdAt');
80
- expect(createdUser).toHaveProperty('updatedAt');
81
- });
82
-
83
- it('should retrieve an item by ID', async () => {
84
- const user = await userDal.get({ id: 'user-123' }).execute();
85
- expect(user).not.toBeNull();
86
- expect(user?.id).toBe('user-123');
87
- });
88
-
89
- it('should update an existing item', async () => {
90
- const updatedUser = await userDal.update({ id: 'user-123' }).set({ name: 'Jane Doe' }).execute();
91
- expect(updatedUser.name).toBe('Jane Doe');
92
- });
93
-
94
- it('should delete an item', async () => {
95
- await userDal.delete({ id: 'user-123' }).execute();
96
- const deletedUser = await userDal.get({ id: 'user-123' }).execute();
97
- expect(deletedUser).toBeNull();
98
- });
99
- });