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