betterddb 0.1.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/LICENCSE +21 -0
- package/README.md +111 -0
- package/docker-compose.yml +16 -0
- package/jest.config.js +16 -0
- package/lib/betterddb.d.ts +132 -0
- package/lib/betterddb.js +458 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +17 -0
- package/package.json +41 -0
- package/src/betterddb.ts +600 -0
- package/src/index.ts +1 -0
- package/test/placeholder.test.ts +98 -0
- package/tsconfig.json +12 -0
package/LICENCSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Ryan Rawlings Wang
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# betterddb
|
|
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.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install betterddb
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage Example
|
|
12
|
+
Below is an example of using betterddb for a User entity with a compound key.
|
|
13
|
+
|
|
14
|
+
```ts
|
|
15
|
+
import { BetterDDB } from 'betterddb';
|
|
16
|
+
import { z } from 'zod';
|
|
17
|
+
import { DynamoDB } from 'aws-sdk';
|
|
18
|
+
|
|
19
|
+
// Define the User schema. Use .passthrough() if you want to allow extra keys (e.g. computed keys).
|
|
20
|
+
const UserSchema = z.object({
|
|
21
|
+
tenantId: z.string(),
|
|
22
|
+
userId: z.string(),
|
|
23
|
+
email: z.string().email(),
|
|
24
|
+
name: z.string(),
|
|
25
|
+
createdAt: z.string(),
|
|
26
|
+
updatedAt: z.string(),
|
|
27
|
+
version: z.number().optional()
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Configure the DynamoDB DocumentClient (for example, using LocalStack)
|
|
31
|
+
const client = new DynamoDB.DocumentClient({
|
|
32
|
+
region: 'us-east-1',
|
|
33
|
+
endpoint: 'http://localhost:4566'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Initialize BetterDDB with compound key definitions.
|
|
37
|
+
const userDdb = new BetterDDB({
|
|
38
|
+
schema: UserSchema,
|
|
39
|
+
tableName: 'Users',
|
|
40
|
+
keys: {
|
|
41
|
+
primary: {
|
|
42
|
+
name: 'pk',
|
|
43
|
+
// Compute the partition key from tenantId
|
|
44
|
+
definition: { build: (raw) => `TENANT#${raw.tenantId}` }
|
|
45
|
+
},
|
|
46
|
+
sort: {
|
|
47
|
+
name: 'sk',
|
|
48
|
+
// Compute the sort key from userId
|
|
49
|
+
definition: { build: (raw) => `USER#${raw.userId}` }
|
|
50
|
+
},
|
|
51
|
+
gsis: {
|
|
52
|
+
// Example: a Global Secondary Index on email.
|
|
53
|
+
EmailIndex: {
|
|
54
|
+
primary: {
|
|
55
|
+
name: 'email',
|
|
56
|
+
definition: 'email'
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
client,
|
|
62
|
+
autoTimestamps: true
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
// Use the BetterDDB instance to create and query items.
|
|
66
|
+
(async () => {
|
|
67
|
+
// Create a new user.
|
|
68
|
+
const newUser = await userDdb.create({
|
|
69
|
+
tenantId: 'tenant1',
|
|
70
|
+
userId: 'user123',
|
|
71
|
+
email: 'user@example.com',
|
|
72
|
+
name: 'Alice'
|
|
73
|
+
});
|
|
74
|
+
console.log('Created User:', newUser);
|
|
75
|
+
|
|
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
|
+
}
|
|
86
|
+
})();
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
## API
|
|
90
|
+
betterddb exposes a generic class BetterDDB<T> with methods for:
|
|
91
|
+
|
|
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
|
+
For complete details, please refer to the API documentation.
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
License
|
|
111
|
+
MIT
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
version: '3.8'
|
|
2
|
+
services:
|
|
3
|
+
betterddb-localstack:
|
|
4
|
+
image: localstack/localstack:latest
|
|
5
|
+
container_name: betterddb-localstack
|
|
6
|
+
ports:
|
|
7
|
+
- "4566:4566" # Main edge port
|
|
8
|
+
- "4571:4571"
|
|
9
|
+
environment:
|
|
10
|
+
- SERVICES=dynamodb
|
|
11
|
+
- DEFAULT_REGION=us-east-1
|
|
12
|
+
- DATA_DIR=/tmp/localstack_data
|
|
13
|
+
- HOST_TMP_FOLDER=${TMPDIR:-/tmp}/localstack
|
|
14
|
+
- LOCALSTACK_UI=1
|
|
15
|
+
volumes:
|
|
16
|
+
- "./localstack-tmp:/tmp/localstack_data"
|
package/jest.config.js
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const config = {
|
|
2
|
+
testEnvironment: 'node',
|
|
3
|
+
roots: ['<rootDir>/test/'],
|
|
4
|
+
testMatch: ['**/*.test.ts'],
|
|
5
|
+
transform: {
|
|
6
|
+
'^.+\\.(ts|tsx|js|jsx)$': 'ts-jest'
|
|
7
|
+
},
|
|
8
|
+
moduleNameMapper: {
|
|
9
|
+
'^@/(.*)$': '<rootDir>/src/$1',
|
|
10
|
+
'^~/(.*)$': '<rootDir>/src/$1'
|
|
11
|
+
},
|
|
12
|
+
extensionsToTreatAsEsm: ['.ts', '.tsx'],
|
|
13
|
+
moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node']
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export default config;
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
import { ZodSchema } from 'zod';
|
|
2
|
+
import { DynamoDB } from 'aws-sdk';
|
|
3
|
+
export type PrimaryKeyValue = string | number;
|
|
4
|
+
/**
|
|
5
|
+
* A key definition can be either a simple key (a property name)
|
|
6
|
+
* or an object containing a build function that computes the value.
|
|
7
|
+
* (In this design, the attribute name is provided separately.)
|
|
8
|
+
*/
|
|
9
|
+
export type KeyDefinition<T> = keyof T | {
|
|
10
|
+
build: (rawKey: Partial<T>) => string;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Configuration for a primary (partition) key.
|
|
14
|
+
*/
|
|
15
|
+
export interface PrimaryKeyConfig<T> {
|
|
16
|
+
/** The attribute name for the primary key in DynamoDB */
|
|
17
|
+
name: string;
|
|
18
|
+
/** How to compute the key value; if a keyof T, then the raw value is used;
|
|
19
|
+
* if an object, the build function is used.
|
|
20
|
+
*/
|
|
21
|
+
definition: KeyDefinition<T>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Configuration for a sort key.
|
|
25
|
+
*/
|
|
26
|
+
export interface SortKeyConfig<T> {
|
|
27
|
+
/** The attribute name for the sort key in DynamoDB */
|
|
28
|
+
name: string;
|
|
29
|
+
/** How to compute the sort key value */
|
|
30
|
+
definition: KeyDefinition<T>;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Configuration for a Global Secondary Index (GSI).
|
|
34
|
+
*/
|
|
35
|
+
export interface GSIConfig<T> {
|
|
36
|
+
primary: PrimaryKeyConfig<T>;
|
|
37
|
+
sort?: SortKeyConfig<T>;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Keys configuration for the table.
|
|
41
|
+
*/
|
|
42
|
+
export interface KeysConfig<T> {
|
|
43
|
+
primary: PrimaryKeyConfig<T>;
|
|
44
|
+
sort?: SortKeyConfig<T>;
|
|
45
|
+
gsis?: {
|
|
46
|
+
[gsiName: string]: GSIConfig<T>;
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Options for initializing BetterDDB.
|
|
51
|
+
*/
|
|
52
|
+
export interface BetterDDBOptions<T> {
|
|
53
|
+
schema: ZodSchema<T>;
|
|
54
|
+
tableName: string;
|
|
55
|
+
keys: KeysConfig<T>;
|
|
56
|
+
client: DynamoDB.DocumentClient;
|
|
57
|
+
/**
|
|
58
|
+
* If true, automatically inject timestamp fields:
|
|
59
|
+
* - On create, sets both `createdAt` and `updatedAt`
|
|
60
|
+
* - On update, sets `updatedAt`
|
|
61
|
+
*
|
|
62
|
+
* (T should include these fields if enabled.)
|
|
63
|
+
*/
|
|
64
|
+
autoTimestamps?: boolean;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* BetterDDB is a definition-based DynamoDB wrapper library.
|
|
68
|
+
*/
|
|
69
|
+
export declare class BetterDDB<T> {
|
|
70
|
+
protected schema: ZodSchema<T>;
|
|
71
|
+
protected tableName: string;
|
|
72
|
+
protected client: DynamoDB.DocumentClient;
|
|
73
|
+
protected keys: KeysConfig<T>;
|
|
74
|
+
protected autoTimestamps: boolean;
|
|
75
|
+
constructor(options: BetterDDBOptions<T>);
|
|
76
|
+
protected getKeyValue(def: KeyDefinition<T>, rawKey: Partial<T>): string;
|
|
77
|
+
/**
|
|
78
|
+
* Build the primary key from a raw key object.
|
|
79
|
+
*/
|
|
80
|
+
protected buildKey(rawKey: Partial<T>): Record<string, any>;
|
|
81
|
+
/**
|
|
82
|
+
* Build index attributes for each defined GSI.
|
|
83
|
+
*/
|
|
84
|
+
protected buildIndexes(rawItem: Partial<T>): Record<string, any>;
|
|
85
|
+
/**
|
|
86
|
+
* Create an item:
|
|
87
|
+
* - Computes primary key and index attributes,
|
|
88
|
+
* - Optionally injects timestamps,
|
|
89
|
+
* - Validates the item and writes it to DynamoDB.
|
|
90
|
+
*/
|
|
91
|
+
create(item: T): Promise<T>;
|
|
92
|
+
get(rawKey: Partial<T>): Promise<T | null>;
|
|
93
|
+
update(rawKey: Partial<T>, update: Partial<T>, options?: {
|
|
94
|
+
expectedVersion?: number;
|
|
95
|
+
}): Promise<T>;
|
|
96
|
+
delete(rawKey: Partial<T>): Promise<void>;
|
|
97
|
+
queryByGsi(gsiName: string, key: Partial<T>, sortKeyCondition?: {
|
|
98
|
+
operator: 'eq' | 'begins_with' | 'between';
|
|
99
|
+
values: any | [any, any];
|
|
100
|
+
}): Promise<T[]>;
|
|
101
|
+
/**
|
|
102
|
+
* Query by primary key (using the computed primary key) and an optional sort key condition.
|
|
103
|
+
*/
|
|
104
|
+
queryByPrimaryKey(rawKey: Partial<T>, sortKeyCondition?: {
|
|
105
|
+
operator: 'eq' | 'begins_with' | 'between';
|
|
106
|
+
values: any | [any, any];
|
|
107
|
+
}, options?: {
|
|
108
|
+
limit?: number;
|
|
109
|
+
lastKey?: Record<string, any>;
|
|
110
|
+
}): Promise<{
|
|
111
|
+
items: T[];
|
|
112
|
+
lastKey?: Record<string, any>;
|
|
113
|
+
}>;
|
|
114
|
+
buildTransactPut(item: T): DynamoDB.DocumentClient.TransactWriteItem;
|
|
115
|
+
buildTransactUpdate(rawKey: Partial<T>, update: Partial<T>, options?: {
|
|
116
|
+
expectedVersion?: number;
|
|
117
|
+
}): DynamoDB.DocumentClient.TransactWriteItem;
|
|
118
|
+
buildTransactDelete(rawKey: Partial<T>): DynamoDB.DocumentClient.TransactWriteItem;
|
|
119
|
+
transactWrite(operations: DynamoDB.DocumentClient.TransactWriteItemList): Promise<void>;
|
|
120
|
+
transactGetByKeys(rawKeys: Partial<T>[]): Promise<T[]>;
|
|
121
|
+
transactGet(getItems: {
|
|
122
|
+
TableName: string;
|
|
123
|
+
Key: any;
|
|
124
|
+
}[]): Promise<T[]>;
|
|
125
|
+
batchWrite(ops: {
|
|
126
|
+
puts?: T[];
|
|
127
|
+
deletes?: Partial<T>[];
|
|
128
|
+
}): Promise<void>;
|
|
129
|
+
private batchWriteChunk;
|
|
130
|
+
private retryBatchWrite;
|
|
131
|
+
batchGet(rawKeys: Partial<T>[]): Promise<T[]>;
|
|
132
|
+
}
|