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,84 @@
1
+ import { DynamoDB } from 'aws-sdk';
2
+ import { BetterDDB } from '../betterddb';
3
+
4
+ export class DeleteBuilder<T> {
5
+ private condition?: { expression: string; attributeValues: Record<string, any> };
6
+ private extraTransactItems: DynamoDB.DocumentClient.TransactWriteItemList = [];
7
+ constructor(private parent: BetterDDB<T>, private key: Partial<T>) {}
8
+
9
+ /**
10
+ * Specify a condition expression for the delete operation.
11
+ */
12
+ public withCondition(expression: string, attributeValues: Record<string, any>): this {
13
+ if (this.condition) {
14
+ this.condition.expression += ` AND ${expression}`;
15
+ Object.assign(this.condition.attributeValues, attributeValues);
16
+ } else {
17
+ this.condition = { expression, attributeValues };
18
+ }
19
+ return this;
20
+ }
21
+
22
+ public async execute(): Promise<void> {
23
+ if (this.extraTransactItems.length > 0) {
24
+ // Build our update transaction item.
25
+ const myTransactItem = this.toTransactDelete();
26
+ // Combine with extra transaction items.
27
+ const allItems = [...this.extraTransactItems, myTransactItem];
28
+ await this.parent.getClient().transactWrite({
29
+ TransactItems: allItems
30
+ }).promise();
31
+ // After transaction, retrieve the updated item.
32
+ const result = await this.parent.get(this.key).execute();
33
+ if (result === null) {
34
+ throw new Error('Item not found after transaction delete');
35
+ }
36
+ } else {
37
+ const params: DynamoDB.DocumentClient.DeleteItemInput = {
38
+ TableName: this.parent.getTableName(),
39
+ Key: this.parent.buildKey(this.key)
40
+ };
41
+ if (this.condition) {
42
+ params.ConditionExpression = this.condition.expression;
43
+ params.ExpressionAttributeValues = this.condition.attributeValues;
44
+ }
45
+ await this.parent.getClient().delete(params).promise();
46
+ }
47
+ }
48
+
49
+ public transactWrite(ops: DynamoDB.DocumentClient.TransactWriteItemList | DynamoDB.DocumentClient.TransactWriteItem): this {
50
+ if (Array.isArray(ops)) {
51
+ this.extraTransactItems.push(...ops);
52
+ } else {
53
+ this.extraTransactItems.push(ops);
54
+ }
55
+ return this;
56
+ }
57
+
58
+ public toTransactDelete(): DynamoDB.DocumentClient.TransactWriteItem {
59
+ const deleteItem: DynamoDB.DocumentClient.Delete = {
60
+ TableName: this.parent.getTableName(),
61
+ Key: this.parent.buildKey(this.key)
62
+ };
63
+ if (this.condition) {
64
+ deleteItem.ConditionExpression = this.condition.expression;
65
+ deleteItem.ExpressionAttributeValues = this.condition.attributeValues;
66
+ }
67
+ return { Delete: deleteItem };
68
+ }
69
+
70
+ public then<TResult1 = void, TResult2 = never>(
71
+ onfulfilled?: ((value: void) => TResult1 | PromiseLike<TResult1>) | null,
72
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
73
+ ): Promise<TResult1 | TResult2> {
74
+ return this.execute().then(onfulfilled, onrejected);
75
+ }
76
+ public catch<TResult = never>(
77
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
78
+ ): Promise<void | TResult> {
79
+ return this.execute().catch(onrejected);
80
+ }
81
+ public finally(onfinally?: (() => void) | null): Promise<void> {
82
+ return this.execute().finally(onfinally);
83
+ }
84
+ }
@@ -0,0 +1,83 @@
1
+ import { DynamoDB } from 'aws-sdk';
2
+ import { BetterDDB } from '../betterddb';
3
+
4
+ export class GetBuilder<T> {
5
+ private projectionExpression?: string;
6
+ private expressionAttributeNames: Record<string, string> = {};
7
+ private extraTransactItems: DynamoDB.DocumentClient.TransactGetItemList = [];
8
+ constructor(private parent: BetterDDB<T>, private key: Partial<T>) {}
9
+
10
+ /**
11
+ * Specify a projection by providing an array of attribute names.
12
+ */
13
+ public withProjection(attributes: (keyof T)[]): this {
14
+ this.projectionExpression = attributes.map(attr => `#${String(attr)}`).join(', ');
15
+ for (const attr of attributes) {
16
+ this.expressionAttributeNames[`#${String(attr)}`] = String(attr);
17
+ }
18
+ return this;
19
+ }
20
+
21
+ public async execute(): Promise<T | null> {
22
+ if (this.extraTransactItems.length > 0) {
23
+ // Build our update transaction item.
24
+ const myTransactItem = this.toTransactGet();
25
+ // Combine with extra transaction items.
26
+ const allItems = [...this.extraTransactItems, myTransactItem];
27
+ await this.parent.getClient().transactGet({
28
+ TransactItems: allItems
29
+ }).promise();
30
+ // After transaction, retrieve the updated item.
31
+ const result = await this.parent.get(this.key).execute();
32
+ return result;
33
+ } else {
34
+ const params: DynamoDB.DocumentClient.GetItemInput = {
35
+ TableName: this.parent.getTableName(),
36
+ Key: this.parent.buildKey(this.key)
37
+ };
38
+ if (this.projectionExpression) {
39
+ params.ProjectionExpression = this.projectionExpression;
40
+ params.ExpressionAttributeNames = this.expressionAttributeNames;
41
+ }
42
+ const result = await this.parent.getClient().get(params).promise();
43
+ if (!result.Item) return null;
44
+ return this.parent.getSchema().parse(result.Item);
45
+ }
46
+ }
47
+
48
+ public transactGet(ops: DynamoDB.DocumentClient.TransactGetItemList | DynamoDB.DocumentClient.TransactGetItem): this {
49
+ if (Array.isArray(ops)) {
50
+ this.extraTransactItems.push(...ops);
51
+ } else {
52
+ this.extraTransactItems.push(ops);
53
+ }
54
+ return this;
55
+ }
56
+
57
+ public toTransactGet(): DynamoDB.DocumentClient.TransactGetItem {
58
+ const getItem: DynamoDB.DocumentClient.Get = {
59
+ TableName: this.parent.getTableName(),
60
+ Key: this.parent.buildKey(this.key)
61
+ };
62
+ if (this.projectionExpression) {
63
+ getItem.ProjectionExpression = this.projectionExpression;
64
+ getItem.ExpressionAttributeNames = this.expressionAttributeNames;
65
+ }
66
+ return { Get: getItem };
67
+ }
68
+
69
+ public then<TResult1 = T | null, TResult2 = never>(
70
+ onfulfilled?: ((value: T | null) => TResult1 | PromiseLike<TResult1>) | null,
71
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
72
+ ): Promise<TResult1 | TResult2> {
73
+ return this.execute().then(onfulfilled, onrejected);
74
+ }
75
+ public catch<TResult = never>(
76
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
77
+ ): Promise<T | null | TResult> {
78
+ return this.execute().catch(onrejected);
79
+ }
80
+ public finally(onfinally?: (() => void) | null): Promise<T | null> {
81
+ return this.execute().finally(onfinally);
82
+ }
83
+ }
@@ -0,0 +1,127 @@
1
+ import { DynamoDB } from 'aws-sdk';
2
+ import { BetterDDB } from '../betterddb';
3
+
4
+ export class QueryBuilder<T> {
5
+ private filters: string[] = [];
6
+ private expressionAttributeNames: Record<string, string> = {};
7
+ private expressionAttributeValues: Record<string, any> = {};
8
+ private indexName?: string;
9
+ private sortKeyCondition?: string; // You can extend this to support a fluent sort builder.
10
+ private limit?: number;
11
+ private lastKey?: Record<string, any>;
12
+ private ascending: boolean = true;
13
+
14
+ constructor(private parent: BetterDDB<T>, private key: Partial<T>) {}
15
+
16
+ public usingIndex(indexName: string): this {
17
+ this.indexName = indexName;
18
+ return this;
19
+ }
20
+
21
+ public sortAscending(): this {
22
+ this.ascending = true;
23
+ return this;
24
+ }
25
+
26
+ public sortDescending(): this {
27
+ this.ascending = false;
28
+ return this;
29
+ }
30
+
31
+ public where(
32
+ attribute: keyof T,
33
+ operator: 'eq' | 'begins_with' | 'between',
34
+ values: any | [any, any]
35
+ ): this {
36
+ const attrStr = String(attribute);
37
+ const nameKey = `#attr_${attrStr}`;
38
+ this.expressionAttributeNames[nameKey] = attrStr;
39
+
40
+ if (operator === 'eq') {
41
+ const valueKey = `:val_${attrStr}`;
42
+ this.expressionAttributeValues[valueKey] = values;
43
+ this.filters.push(`${nameKey} = ${valueKey}`);
44
+ } else if (operator === 'begins_with') {
45
+ const valueKey = `:val_${attrStr}`;
46
+ this.expressionAttributeValues[valueKey] = values;
47
+ this.filters.push(`begins_with(${nameKey}, ${valueKey})`);
48
+ } else if (operator === 'between') {
49
+ if (!Array.isArray(values) || values.length !== 2) {
50
+ throw new Error(`For 'between' operator, values must be a tuple of two items`);
51
+ }
52
+ const valueKeyStart = `:val_start_${attrStr}`;
53
+ const valueKeyEnd = `:val_end_${attrStr}`;
54
+ this.expressionAttributeValues[valueKeyStart] = values[0];
55
+ this.expressionAttributeValues[valueKeyEnd] = values[1];
56
+ this.filters.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
57
+ } else {
58
+ throw new Error(`Unsupported operator: ${operator}`);
59
+ }
60
+ return this;
61
+ }
62
+
63
+ public limitResults(limit: number): this {
64
+ this.limit = limit;
65
+ return this;
66
+ }
67
+
68
+ public startFrom(lastKey: Record<string, any>): this {
69
+ this.lastKey = lastKey;
70
+ return this;
71
+ }
72
+
73
+ /**
74
+ * Executes the query and returns a Promise that resolves with an array of items.
75
+ */
76
+ public async execute(): Promise<T[]> {
77
+ // Build a simple key condition for the partition key.
78
+ const keys = this.parent.getKeys();
79
+ const pkName = keys.primary.name;
80
+ this.expressionAttributeNames['#pk'] = pkName;
81
+
82
+ // Cast the built key to a record so that we can index by string.
83
+ const builtKey = this.parent.buildKey(this.key) as Record<string, any>;
84
+ this.expressionAttributeValues[':pk_value'] = builtKey[pkName];
85
+
86
+ let keyConditionExpression = `#pk = :pk_value`;
87
+ // If a sortKeyCondition was set via another fluent method, append it.
88
+ if (this.sortKeyCondition) {
89
+ keyConditionExpression += ` AND ${this.sortKeyCondition}`;
90
+ }
91
+
92
+ // If any filters were added, set them as FilterExpression.
93
+ const params: DynamoDB.DocumentClient.QueryInput = {
94
+ TableName: this.parent.getTableName(),
95
+ KeyConditionExpression: keyConditionExpression,
96
+ ExpressionAttributeNames: this.expressionAttributeNames,
97
+ ExpressionAttributeValues: this.expressionAttributeValues,
98
+ ScanIndexForward: this.ascending,
99
+ Limit: this.limit,
100
+ ExclusiveStartKey: this.lastKey,
101
+ IndexName: this.indexName
102
+ };
103
+
104
+ if (this.filters.length > 0) {
105
+ params.FilterExpression = this.filters.join(' AND ');
106
+ }
107
+
108
+ const result = await this.parent.getClient().query(params).promise();
109
+ return this.parent.getSchema().array().parse(result.Items);
110
+ }
111
+
112
+ // Thenable implementation.
113
+ public then<TResult1 = T[], TResult2 = never>(
114
+ onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
115
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
116
+ ): Promise<TResult1 | TResult2> {
117
+ return this.execute().then(onfulfilled, onrejected);
118
+ }
119
+ public catch<TResult = never>(
120
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
121
+ ): Promise<T[] | TResult> {
122
+ return this.execute().catch(onrejected);
123
+ }
124
+ public finally(onfinally?: (() => void) | null): Promise<T[]> {
125
+ return this.execute().finally(onfinally);
126
+ }
127
+ }
@@ -0,0 +1,90 @@
1
+ import { DynamoDB } from 'aws-sdk';
2
+ import { BetterDDB } from '../betterddb';
3
+
4
+ export class ScanBuilder<T> {
5
+ private filters: string[] = [];
6
+ private expressionAttributeNames: Record<string, string> = {};
7
+ private expressionAttributeValues: Record<string, any> = {};
8
+ private limit?: number;
9
+ private lastKey?: Record<string, any>;
10
+
11
+ constructor(private parent: BetterDDB<T>) {}
12
+
13
+ public where(
14
+ attribute: keyof T,
15
+ operator: 'eq' | 'begins_with' | 'between',
16
+ values: any | [any, any]
17
+ ): this {
18
+ const attrStr = String(attribute);
19
+ const nameKey = `#attr_${attrStr}`;
20
+ this.expressionAttributeNames[nameKey] = attrStr;
21
+
22
+ if (operator === 'eq') {
23
+ const valueKey = `:val_${attrStr}`;
24
+ this.expressionAttributeValues[valueKey] = values;
25
+ this.filters.push(`${nameKey} = ${valueKey}`);
26
+ } else if (operator === 'begins_with') {
27
+ const valueKey = `:val_${attrStr}`;
28
+ this.expressionAttributeValues[valueKey] = values;
29
+ this.filters.push(`begins_with(${nameKey}, ${valueKey})`);
30
+ } else if (operator === 'between') {
31
+ if (!Array.isArray(values) || values.length !== 2) {
32
+ throw new Error(`For 'between' operator, values must be a tuple of two items`);
33
+ }
34
+ const valueKeyStart = `:val_start_${attrStr}`;
35
+ const valueKeyEnd = `:val_end_${attrStr}`;
36
+ this.expressionAttributeValues[valueKeyStart] = values[0];
37
+ this.expressionAttributeValues[valueKeyEnd] = values[1];
38
+ this.filters.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
39
+ } else {
40
+ throw new Error(`Unsupported operator: ${operator}`);
41
+ }
42
+ return this;
43
+ }
44
+
45
+ public limitResults(limit: number): this {
46
+ this.limit = limit;
47
+ return this;
48
+ }
49
+
50
+ public startFrom(lastKey: Record<string, any>): this {
51
+ this.lastKey = lastKey;
52
+ return this;
53
+ }
54
+
55
+ /**
56
+ * Executes the scan and returns a Promise that resolves with an array of items.
57
+ */
58
+ public async execute(): Promise<T[]> {
59
+ const params: DynamoDB.DocumentClient.ScanInput = {
60
+ TableName: this.parent.getTableName(),
61
+ ExpressionAttributeNames: this.expressionAttributeNames,
62
+ ExpressionAttributeValues: this.expressionAttributeValues,
63
+ Limit: this.limit,
64
+ ExclusiveStartKey: this.lastKey
65
+ };
66
+
67
+ if (this.filters.length > 0) {
68
+ params.FilterExpression = this.filters.join(' AND ');
69
+ }
70
+
71
+ const result = await this.parent.getClient().scan(params).promise();
72
+ return this.parent.getSchema().array().parse(result.Items);
73
+ }
74
+
75
+ // Thenable implementation.
76
+ public then<TResult1 = T[], TResult2 = never>(
77
+ onfulfilled?: ((value: T[]) => TResult1 | PromiseLike<TResult1>) | null,
78
+ onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | null
79
+ ): Promise<TResult1 | TResult2> {
80
+ return this.execute().then(onfulfilled, onrejected);
81
+ }
82
+ public catch<TResult = never>(
83
+ onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | null
84
+ ): Promise<T[] | TResult> {
85
+ return this.execute().catch(onrejected);
86
+ }
87
+ public finally(onfinally?: (() => void) | null): Promise<T[]> {
88
+ return this.execute().finally(onfinally);
89
+ }
90
+ }
@@ -0,0 +1,230 @@
1
+
2
+ import { DynamoDB } from 'aws-sdk';
3
+ import { BetterDDB } from '../betterddb';
4
+
5
+ interface UpdateActions<T> {
6
+ set?: Partial<T>;
7
+ remove?: (keyof T)[];
8
+ add?: Partial<Record<keyof T, number | Set<any>>>;
9
+ delete?: Partial<Record<keyof T, Set<any>>>;
10
+ }
11
+
12
+ export class UpdateBuilder<T> {
13
+ private actions: UpdateActions<T> = {};
14
+ private condition?: { expression: string; attributeValues: Record<string, any> };
15
+ private expectedVersion?: number;
16
+ // When using transaction mode, we store extra transaction items.
17
+ private extraTransactItems: DynamoDB.DocumentClient.TransactWriteItemList = [];
18
+
19
+ // Reference to the parent BetterDDB instance and key.
20
+ constructor(private parent: BetterDDB<T>, private key: Partial<T>, expectedVersion?: number) {
21
+ this.expectedVersion = expectedVersion;
22
+ }
23
+
24
+ // Chainable methods:
25
+ public set(attrs: Partial<T>): this {
26
+ this.actions.set = { ...this.actions.set, ...attrs };
27
+ return this;
28
+ }
29
+
30
+ public remove(attrs: (keyof T)[]): this {
31
+ this.actions.remove = [...(this.actions.remove || []), ...attrs];
32
+ return this;
33
+ }
34
+
35
+ public add(attrs: Partial<Record<keyof T, number | Set<any>>>): this {
36
+ this.actions.add = { ...this.actions.add, ...attrs };
37
+ return this;
38
+ }
39
+
40
+ public delete(attrs: Partial<Record<keyof T, Set<any>>>): this {
41
+ this.actions.delete = { ...this.actions.delete, ...attrs };
42
+ return this;
43
+ }
44
+
45
+ /**
46
+ * Adds a condition expression to the update.
47
+ */
48
+ public setCondition(expression: string, attributeValues: Record<string, any>): this {
49
+ if (this.condition) {
50
+ // Merge conditions with AND.
51
+ this.condition.expression += ` AND ${expression}`;
52
+ Object.assign(this.condition.attributeValues, attributeValues);
53
+ } else {
54
+ this.condition = { expression, attributeValues };
55
+ }
56
+ return this;
57
+ }
58
+
59
+ /**
60
+ * Specifies additional transaction items to include when executing this update as a transaction.
61
+ */
62
+ public transactWrite(ops: DynamoDB.DocumentClient.TransactWriteItemList | DynamoDB.DocumentClient.TransactWriteItem): this {
63
+ if (Array.isArray(ops)) {
64
+ this.extraTransactItems.push(...ops);
65
+ } else {
66
+ this.extraTransactItems.push(ops);
67
+ }
68
+ return this;
69
+ }
70
+
71
+ /**
72
+ * Builds the update expression and associated maps.
73
+ */
74
+ private buildExpression(): {
75
+ updateExpression: string;
76
+ attributeNames: Record<string, string>;
77
+ attributeValues: Record<string, any>;
78
+ } {
79
+ const ExpressionAttributeNames: Record<string, string> = {};
80
+ const ExpressionAttributeValues: Record<string, any> = {};
81
+ const clauses: string[] = [];
82
+
83
+ // Build SET clause.
84
+ if (this.actions.set) {
85
+ const setParts: string[] = [];
86
+ for (const [attr, value] of Object.entries(this.actions.set)) {
87
+ const nameKey = `#set_${attr}`;
88
+ const valueKey = `:set_${attr}`;
89
+ ExpressionAttributeNames[nameKey] = attr;
90
+ ExpressionAttributeValues[valueKey] = value;
91
+ setParts.push(`${nameKey} = ${valueKey}`);
92
+ }
93
+ if (setParts.length > 0) {
94
+ clauses.push(`SET ${setParts.join(', ')}`);
95
+ }
96
+ }
97
+
98
+ // Build REMOVE clause.
99
+ if (this.actions.remove && this.actions.remove.length > 0) {
100
+ const removeParts = this.actions.remove.map(attr => {
101
+ const nameKey = `#remove_${String(attr)}`;
102
+ ExpressionAttributeNames[nameKey] = String(attr);
103
+ return nameKey;
104
+ });
105
+ clauses.push(`REMOVE ${removeParts.join(', ')}`);
106
+ }
107
+
108
+ // Build ADD clause.
109
+ if (this.actions.add) {
110
+ const addParts: string[] = [];
111
+ for (const [attr, value] of Object.entries(this.actions.add)) {
112
+ const nameKey = `#add_${attr}`;
113
+ const valueKey = `:add_${attr}`;
114
+ ExpressionAttributeNames[nameKey] = attr;
115
+ ExpressionAttributeValues[valueKey] = value;
116
+ addParts.push(`${nameKey} ${valueKey}`);
117
+ }
118
+ if (addParts.length > 0) {
119
+ clauses.push(`ADD ${addParts.join(', ')}`);
120
+ }
121
+ }
122
+
123
+ // Build DELETE clause.
124
+ if (this.actions.delete) {
125
+ const deleteParts: string[] = [];
126
+ for (const [attr, value] of Object.entries(this.actions.delete)) {
127
+ const nameKey = `#delete_${attr}`;
128
+ const valueKey = `:delete_${attr}`;
129
+ ExpressionAttributeNames[nameKey] = attr;
130
+ ExpressionAttributeValues[valueKey] = value;
131
+ deleteParts.push(`${nameKey} ${valueKey}`);
132
+ }
133
+ if (deleteParts.length > 0) {
134
+ clauses.push(`DELETE ${deleteParts.join(', ')}`);
135
+ }
136
+ }
137
+
138
+ // Incorporate expectedVersion if provided.
139
+ if (this.expectedVersion !== undefined) {
140
+ ExpressionAttributeNames['#version'] = 'version';
141
+ ExpressionAttributeValues[':expectedVersion'] = this.expectedVersion;
142
+ ExpressionAttributeValues[':newVersion'] = this.expectedVersion + 1;
143
+
144
+ // Append version update in SET clause.
145
+ const versionClause = '#version = :newVersion';
146
+ const setIndex = clauses.findIndex(clause => clause.startsWith('SET '));
147
+ if (setIndex >= 0) {
148
+ clauses[setIndex] += `, ${versionClause}`;
149
+ } else {
150
+ clauses.push(`SET ${versionClause}`);
151
+ }
152
+
153
+ // Ensure condition expression includes version check.
154
+ if (this.condition && this.condition.expression) {
155
+ this.condition.expression += ` AND #version = :expectedVersion`;
156
+ } else {
157
+ this.condition = { expression: '#version = :expectedVersion', attributeValues: {} };
158
+ }
159
+ }
160
+
161
+ // Merge any provided condition attribute values.
162
+ if (this.condition) {
163
+ Object.assign(ExpressionAttributeValues, this.condition.attributeValues);
164
+ }
165
+
166
+ return {
167
+ updateExpression: clauses.join(' '),
168
+ attributeNames: ExpressionAttributeNames,
169
+ attributeValues: ExpressionAttributeValues
170
+ };
171
+ }
172
+
173
+ /**
174
+ * Returns a transaction update item that can be included in a transactWrite call.
175
+ */
176
+ public toTransactUpdate(): DynamoDB.DocumentClient.TransactWriteItem {
177
+ const { updateExpression, attributeNames, attributeValues } = this.buildExpression();
178
+ const updateItem: DynamoDB.DocumentClient.Update = {
179
+ TableName: this.parent.getTableName(),
180
+ Key: this.parent.buildKey(this.key),
181
+ UpdateExpression: updateExpression,
182
+ ExpressionAttributeNames: attributeNames,
183
+ ExpressionAttributeValues: attributeValues
184
+ };
185
+ if (this.condition && this.condition.expression) {
186
+ updateItem.ConditionExpression = this.condition.expression;
187
+ }
188
+ return { Update: updateItem };
189
+ }
190
+
191
+ /**
192
+ * Commits the update immediately by calling the parent's update method.
193
+ */
194
+ public async execute(): Promise<T> {
195
+ if (this.extraTransactItems.length > 0) {
196
+ // Build our update transaction item.
197
+ const myTransactItem = this.toTransactUpdate();
198
+ // Combine with extra transaction items.
199
+ const allItems = [...this.extraTransactItems, myTransactItem];
200
+ await this.parent.getClient().transactWrite({
201
+ TransactItems: allItems
202
+ }).promise();
203
+ // After transaction, retrieve the updated item.
204
+ const result = await this.parent.get(this.key).execute();
205
+ if (result === null) {
206
+ throw new Error('Item not found after transaction update');
207
+ }
208
+ return result;
209
+ } else {
210
+ // Normal update flow.
211
+ const { updateExpression, attributeNames, attributeValues } = this.buildExpression();
212
+ const params: DynamoDB.DocumentClient.UpdateItemInput = {
213
+ TableName: this.parent.getTableName(),
214
+ Key: this.parent.buildKey(this.key),
215
+ UpdateExpression: updateExpression,
216
+ ExpressionAttributeNames: attributeNames,
217
+ ExpressionAttributeValues: attributeValues,
218
+ ReturnValues: 'ALL_NEW'
219
+ };
220
+ if (this.condition && this.condition.expression) {
221
+ params.ConditionExpression = this.condition.expression;
222
+ }
223
+ const result = await this.parent.getClient().update(params).promise();
224
+ if (!result.Attributes) {
225
+ throw new Error('No attributes returned after update');
226
+ }
227
+ return this.parent.getSchema().parse(result.Attributes);
228
+ }
229
+ }
230
+ }
@@ -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
+ });