betterddb 0.4.7 → 0.4.9
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/lib/builders/batch-get-builder.d.ts +0 -6
- package/lib/builders/batch-get-builder.js +0 -15
- package/lib/builders/query-builder.d.ts +5 -3
- package/lib/builders/query-builder.js +77 -28
- package/lib/builders/scan-builder.d.ts +2 -1
- package/lib/builders/scan-builder.js +11 -12
- package/lib/operator.d.ts +2 -0
- package/lib/operator.js +31 -0
- package/package.json +1 -1
- package/src/builders/batch-get-builder.ts +0 -18
- package/src/builders/query-builder.ts +86 -34
- package/src/builders/scan-builder.ts +17 -14
- package/src/operator.ts +43 -0
- package/test/batch-get.test.ts +83 -0
- package/test/create.test.ts +27 -7
- package/test/delete.test.ts +26 -6
- package/test/get.test.ts +27 -6
- package/test/query.test.ts +111 -14
- package/test/scan.test.ts +59 -14
- package/test/update.test.ts +26 -6
- package/test/utils/table-setup.ts +2 -10
|
@@ -2,17 +2,11 @@ import { BetterDDB } from '../betterddb';
|
|
|
2
2
|
export declare class BatchGetBuilder<T> {
|
|
3
3
|
private parent;
|
|
4
4
|
private keys;
|
|
5
|
-
private projectionExpression?;
|
|
6
|
-
private expressionAttributeNames;
|
|
7
5
|
/**
|
|
8
6
|
* @param parent - The BetterDDB instance for the table.
|
|
9
7
|
* @param keys - An array of partial keys for the items you wish to retrieve.
|
|
10
8
|
*/
|
|
11
9
|
constructor(parent: BetterDDB<T>, keys: Partial<T>[]);
|
|
12
|
-
/**
|
|
13
|
-
* Specify a projection by providing an array of attribute names.
|
|
14
|
-
*/
|
|
15
|
-
withProjection(attributes: (keyof T)[]): this;
|
|
16
10
|
/**
|
|
17
11
|
* Executes the batch get operation.
|
|
18
12
|
* Returns an array of parsed items of type T.
|
|
@@ -10,17 +10,6 @@ class BatchGetBuilder {
|
|
|
10
10
|
constructor(parent, keys) {
|
|
11
11
|
this.parent = parent;
|
|
12
12
|
this.keys = keys;
|
|
13
|
-
this.expressionAttributeNames = {};
|
|
14
|
-
}
|
|
15
|
-
/**
|
|
16
|
-
* Specify a projection by providing an array of attribute names.
|
|
17
|
-
*/
|
|
18
|
-
withProjection(attributes) {
|
|
19
|
-
this.projectionExpression = attributes.map(attr => `#${String(attr)}`).join(', ');
|
|
20
|
-
for (const attr of attributes) {
|
|
21
|
-
this.expressionAttributeNames[`#${String(attr)}`] = String(attr);
|
|
22
|
-
}
|
|
23
|
-
return this;
|
|
24
13
|
}
|
|
25
14
|
/**
|
|
26
15
|
* Executes the batch get operation.
|
|
@@ -35,10 +24,6 @@ class BatchGetBuilder {
|
|
|
35
24
|
RequestItems: {
|
|
36
25
|
[tableName]: {
|
|
37
26
|
Keys: keysArray,
|
|
38
|
-
...(this.projectionExpression && {
|
|
39
|
-
ProjectionExpression: this.projectionExpression,
|
|
40
|
-
ExpressionAttributeNames: this.expressionAttributeNames,
|
|
41
|
-
}),
|
|
42
27
|
},
|
|
43
28
|
},
|
|
44
29
|
};
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
import { BetterDDB } from '../betterddb';
|
|
2
|
+
import { Operator } from '../operator';
|
|
2
3
|
export declare class QueryBuilder<T> {
|
|
3
4
|
private parent;
|
|
4
5
|
private key;
|
|
5
|
-
private
|
|
6
|
+
private keyConditions;
|
|
7
|
+
private filterConditions;
|
|
6
8
|
private expressionAttributeNames;
|
|
7
9
|
private expressionAttributeValues;
|
|
8
10
|
private index?;
|
|
9
|
-
private sortKeyCondition?;
|
|
10
11
|
private limit?;
|
|
11
12
|
private lastKey?;
|
|
12
13
|
private ascending;
|
|
@@ -14,7 +15,8 @@ export declare class QueryBuilder<T> {
|
|
|
14
15
|
usingIndex(indexName: string): this;
|
|
15
16
|
sortAscending(): this;
|
|
16
17
|
sortDescending(): this;
|
|
17
|
-
where(
|
|
18
|
+
where(operator: Operator, values: Partial<T> | [Partial<T>, Partial<T>]): this;
|
|
19
|
+
filter(attribute: keyof T, operator: Operator, values: any | [any, any]): this;
|
|
18
20
|
limitResults(limit: number): this;
|
|
19
21
|
startFrom(lastKey: Record<string, any>): this;
|
|
20
22
|
/**
|
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.QueryBuilder = void 0;
|
|
4
4
|
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
5
|
+
const operator_1 = require("../operator");
|
|
5
6
|
class QueryBuilder {
|
|
6
7
|
constructor(parent, key) {
|
|
7
8
|
this.parent = parent;
|
|
8
9
|
this.key = key;
|
|
9
|
-
this.
|
|
10
|
+
this.keyConditions = [];
|
|
11
|
+
this.filterConditions = [];
|
|
10
12
|
this.expressionAttributeNames = {};
|
|
11
13
|
this.expressionAttributeValues = {};
|
|
12
14
|
this.ascending = true;
|
|
@@ -29,32 +31,78 @@ class QueryBuilder {
|
|
|
29
31
|
this.ascending = false;
|
|
30
32
|
return this;
|
|
31
33
|
}
|
|
32
|
-
where(
|
|
33
|
-
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
this.
|
|
39
|
-
|
|
34
|
+
where(operator, values) {
|
|
35
|
+
var _a, _b;
|
|
36
|
+
const keys = this.parent.getKeys();
|
|
37
|
+
// Determine the sort key name from either the index or the primary keys.
|
|
38
|
+
const sortKeyName = this.index ? (_a = this.index.sort) === null || _a === void 0 ? void 0 : _a.name : (_b = keys.sort) === null || _b === void 0 ? void 0 : _b.name;
|
|
39
|
+
if (!sortKeyName) {
|
|
40
|
+
throw new Error('Sort key is not defined for this table/index.');
|
|
41
|
+
}
|
|
42
|
+
const nameKey = '#sk';
|
|
43
|
+
this.expressionAttributeNames[nameKey] = sortKeyName;
|
|
44
|
+
// Enforce that a complex sort key requires an object input.
|
|
45
|
+
if (typeof values !== 'object' || values === null) {
|
|
46
|
+
throw new Error(`For complex sort keys, please provide an object with all necessary properties.`);
|
|
47
|
+
}
|
|
48
|
+
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 objects`);
|
|
51
|
+
}
|
|
52
|
+
const valueKeyStart = ':sk_start';
|
|
53
|
+
const valueKeyEnd = ':sk_end';
|
|
54
|
+
// Use the key definition's build function to build the key from the full object.
|
|
55
|
+
this.expressionAttributeValues[valueKeyStart] = this.index
|
|
56
|
+
? this.parent.buildIndexes(values[0])[sortKeyName]
|
|
57
|
+
: this.parent.buildKey(values[0])[sortKeyName];
|
|
58
|
+
this.expressionAttributeValues[valueKeyEnd] = this.index
|
|
59
|
+
? this.parent.buildIndexes(values[1])[sortKeyName]
|
|
60
|
+
: this.parent.buildKey(values[1])[sortKeyName];
|
|
61
|
+
this.keyConditions.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
|
|
40
62
|
}
|
|
41
63
|
else if (operator === 'begins_with') {
|
|
42
|
-
const valueKey =
|
|
43
|
-
this.expressionAttributeValues[valueKey] =
|
|
44
|
-
|
|
64
|
+
const valueKey = ':sk_value';
|
|
65
|
+
this.expressionAttributeValues[valueKey] = this.index
|
|
66
|
+
? this.parent.buildIndexes(values)[sortKeyName]
|
|
67
|
+
: this.parent.buildKey(values)[sortKeyName];
|
|
68
|
+
this.keyConditions.push(`begins_with(${nameKey}, ${valueKey})`);
|
|
45
69
|
}
|
|
46
|
-
else
|
|
70
|
+
else {
|
|
71
|
+
// For eq, lt, lte, gt, gte:
|
|
72
|
+
const valueKey = ':sk_value';
|
|
73
|
+
this.expressionAttributeValues[valueKey] = this.index
|
|
74
|
+
? this.parent.buildIndexes(values)[sortKeyName]
|
|
75
|
+
: this.parent.buildKey(values)[sortKeyName];
|
|
76
|
+
const condition = (0, operator_1.getOperatorExpression)(operator, nameKey, valueKey);
|
|
77
|
+
this.keyConditions.push(condition);
|
|
78
|
+
}
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
filter(attribute, operator, values) {
|
|
82
|
+
const attrStr = String(attribute);
|
|
83
|
+
const randomString = Math.random().toString(36).substring(2, 15);
|
|
84
|
+
const placeholderName = `#attr_${attrStr}_${randomString}`;
|
|
85
|
+
this.expressionAttributeNames[placeholderName] = attrStr;
|
|
86
|
+
if (operator === 'between') {
|
|
47
87
|
if (!Array.isArray(values) || values.length !== 2) {
|
|
48
|
-
throw new Error(
|
|
88
|
+
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
49
89
|
}
|
|
50
|
-
const
|
|
51
|
-
const
|
|
52
|
-
this.expressionAttributeValues[
|
|
53
|
-
this.expressionAttributeValues[
|
|
54
|
-
this.
|
|
90
|
+
const placeholderValueStart = `:val_start_${attrStr}_${randomString}`;
|
|
91
|
+
const placeholderValueEnd = `:val_end_${attrStr}_${randomString}`;
|
|
92
|
+
this.expressionAttributeValues[placeholderValueStart] = values[0];
|
|
93
|
+
this.expressionAttributeValues[placeholderValueEnd] = values[1];
|
|
94
|
+
this.filterConditions.push(`${placeholderName} BETWEEN ${placeholderValueStart} AND ${placeholderValueEnd}`);
|
|
95
|
+
}
|
|
96
|
+
else if (operator === 'begins_with' || operator === 'contains') {
|
|
97
|
+
const placeholderValue = `:val_${attrStr}_${randomString}`;
|
|
98
|
+
this.expressionAttributeValues[placeholderValue] = values;
|
|
99
|
+
this.filterConditions.push(`${operator}(${placeholderName}, ${placeholderValue})`);
|
|
55
100
|
}
|
|
56
101
|
else {
|
|
57
|
-
|
|
102
|
+
const placeholderValue = `:val_${attrStr}_${randomString}`;
|
|
103
|
+
this.expressionAttributeValues[placeholderValue] = values;
|
|
104
|
+
const condition = (0, operator_1.getOperatorExpression)(operator, placeholderName, placeholderValue);
|
|
105
|
+
this.filterConditions.push(condition);
|
|
58
106
|
}
|
|
59
107
|
return this;
|
|
60
108
|
}
|
|
@@ -78,12 +126,12 @@ class QueryBuilder {
|
|
|
78
126
|
pkName = this.index.primary.name;
|
|
79
127
|
builtKey = this.parent.buildIndexes(this.key);
|
|
80
128
|
}
|
|
81
|
-
this.expressionAttributeNames['#pk']
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
129
|
+
if (!this.expressionAttributeNames['#pk']) {
|
|
130
|
+
this.expressionAttributeNames['#pk'] = pkName;
|
|
131
|
+
this.expressionAttributeValues[':pk_value'] = builtKey[pkName];
|
|
132
|
+
this.keyConditions.unshift(`#pk = :pk_value`);
|
|
85
133
|
}
|
|
86
|
-
this.
|
|
134
|
+
const keyConditionExpression = this.keyConditions.join(' AND ');
|
|
87
135
|
const params = {
|
|
88
136
|
TableName: this.parent.getTableName(),
|
|
89
137
|
KeyConditionExpression: keyConditionExpression,
|
|
@@ -92,11 +140,12 @@ class QueryBuilder {
|
|
|
92
140
|
ScanIndexForward: this.ascending,
|
|
93
141
|
Limit: this.limit,
|
|
94
142
|
ExclusiveStartKey: this.lastKey,
|
|
95
|
-
IndexName: (_b = (_a = this.index) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : undefined
|
|
143
|
+
IndexName: (_b = (_a = this.index) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : undefined,
|
|
96
144
|
};
|
|
97
|
-
if (this.
|
|
98
|
-
params.FilterExpression = this.
|
|
145
|
+
if (this.filterConditions.length > 0) {
|
|
146
|
+
params.FilterExpression = this.filterConditions.join(' AND ');
|
|
99
147
|
}
|
|
148
|
+
console.log(params);
|
|
100
149
|
const result = await this.parent.getClient().send(new lib_dynamodb_1.QueryCommand(params));
|
|
101
150
|
return this.parent.getSchema().array().parse(result.Items);
|
|
102
151
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BetterDDB } from '../betterddb';
|
|
2
|
+
import { Operator } from '../operator';
|
|
2
3
|
export declare class ScanBuilder<T> {
|
|
3
4
|
private parent;
|
|
4
5
|
private filters;
|
|
@@ -7,7 +8,7 @@ export declare class ScanBuilder<T> {
|
|
|
7
8
|
private limit?;
|
|
8
9
|
private lastKey?;
|
|
9
10
|
constructor(parent: BetterDDB<T>);
|
|
10
|
-
where(attribute: keyof T, operator:
|
|
11
|
+
where(attribute: keyof T, operator: Operator, values: any | [any, any]): this;
|
|
11
12
|
limitResults(limit: number): this;
|
|
12
13
|
startFrom(lastKey: Record<string, any>): this;
|
|
13
14
|
/**
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.ScanBuilder = void 0;
|
|
4
4
|
const lib_dynamodb_1 = require("@aws-sdk/lib-dynamodb");
|
|
5
|
+
const operator_1 = require("../operator");
|
|
5
6
|
class ScanBuilder {
|
|
6
7
|
constructor(parent) {
|
|
7
8
|
this.parent = parent;
|
|
@@ -13,17 +14,7 @@ class ScanBuilder {
|
|
|
13
14
|
const attrStr = String(attribute);
|
|
14
15
|
const nameKey = `#attr_${attrStr}`;
|
|
15
16
|
this.expressionAttributeNames[nameKey] = attrStr;
|
|
16
|
-
if (operator === '
|
|
17
|
-
const valueKey = `:val_${attrStr}`;
|
|
18
|
-
this.expressionAttributeValues[valueKey] = values;
|
|
19
|
-
this.filters.push(`${nameKey} = ${valueKey}`);
|
|
20
|
-
}
|
|
21
|
-
else if (operator === 'begins_with') {
|
|
22
|
-
const valueKey = `:val_${attrStr}`;
|
|
23
|
-
this.expressionAttributeValues[valueKey] = values;
|
|
24
|
-
this.filters.push(`begins_with(${nameKey}, ${valueKey})`);
|
|
25
|
-
}
|
|
26
|
-
else if (operator === 'between') {
|
|
17
|
+
if (operator === 'between') {
|
|
27
18
|
if (!Array.isArray(values) || values.length !== 2) {
|
|
28
19
|
throw new Error(`For 'between' operator, values must be a tuple of two items`);
|
|
29
20
|
}
|
|
@@ -33,8 +24,16 @@ class ScanBuilder {
|
|
|
33
24
|
this.expressionAttributeValues[valueKeyEnd] = values[1];
|
|
34
25
|
this.filters.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
|
|
35
26
|
}
|
|
27
|
+
else if (operator === 'begins_with' || operator === 'contains') {
|
|
28
|
+
const valueKey = `:val_${attrStr}`;
|
|
29
|
+
this.expressionAttributeValues[valueKey] = values;
|
|
30
|
+
this.filters.push(`${operator}(${nameKey}, ${valueKey})`);
|
|
31
|
+
}
|
|
36
32
|
else {
|
|
37
|
-
|
|
33
|
+
const valueKey = `:val_${attrStr}`;
|
|
34
|
+
this.expressionAttributeValues[valueKey] = values;
|
|
35
|
+
const condition = (0, operator_1.getOperatorExpression)(operator, nameKey, valueKey);
|
|
36
|
+
this.filters.push(condition);
|
|
38
37
|
}
|
|
39
38
|
return this;
|
|
40
39
|
}
|
package/lib/operator.js
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getOperatorExpression = void 0;
|
|
4
|
+
function getOperatorExpression(operator, nameKey, valueKey, secondValueKey) {
|
|
5
|
+
switch (operator) {
|
|
6
|
+
case '==':
|
|
7
|
+
return `${nameKey} = ${valueKey}`;
|
|
8
|
+
case '!=':
|
|
9
|
+
return `${nameKey} <> ${valueKey}`;
|
|
10
|
+
case '<':
|
|
11
|
+
return `${nameKey} < ${valueKey}`;
|
|
12
|
+
case '<=':
|
|
13
|
+
return `${nameKey} <= ${valueKey}`;
|
|
14
|
+
case '>':
|
|
15
|
+
return `${nameKey} > ${valueKey}`;
|
|
16
|
+
case '>=':
|
|
17
|
+
return `${nameKey} >= ${valueKey}`;
|
|
18
|
+
case 'begins_with':
|
|
19
|
+
return `begins_with(${nameKey}, ${valueKey})`;
|
|
20
|
+
case 'between':
|
|
21
|
+
if (!secondValueKey) {
|
|
22
|
+
throw new Error("The 'between' operator requires two value keys");
|
|
23
|
+
}
|
|
24
|
+
return `${nameKey} BETWEEN ${valueKey} AND ${secondValueKey}`;
|
|
25
|
+
case 'contains':
|
|
26
|
+
return `contains(${nameKey}, ${valueKey})`;
|
|
27
|
+
default:
|
|
28
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
exports.getOperatorExpression = getOperatorExpression;
|
package/package.json
CHANGED
|
@@ -3,26 +3,12 @@ import { BatchGetCommand } from '@aws-sdk/lib-dynamodb';
|
|
|
3
3
|
import { BatchGetItemInput } from '@aws-sdk/client-dynamodb';
|
|
4
4
|
|
|
5
5
|
export class BatchGetBuilder<T> {
|
|
6
|
-
private projectionExpression?: string;
|
|
7
|
-
private expressionAttributeNames: Record<string, string> = {};
|
|
8
|
-
|
|
9
6
|
/**
|
|
10
7
|
* @param parent - The BetterDDB instance for the table.
|
|
11
8
|
* @param keys - An array of partial keys for the items you wish to retrieve.
|
|
12
9
|
*/
|
|
13
10
|
constructor(private parent: BetterDDB<T>, private keys: Partial<T>[]) {}
|
|
14
11
|
|
|
15
|
-
/**
|
|
16
|
-
* Specify a projection by providing an array of attribute names.
|
|
17
|
-
*/
|
|
18
|
-
public withProjection(attributes: (keyof T)[]): this {
|
|
19
|
-
this.projectionExpression = attributes.map(attr => `#${String(attr)}`).join(', ');
|
|
20
|
-
for (const attr of attributes) {
|
|
21
|
-
this.expressionAttributeNames[`#${String(attr)}`] = String(attr);
|
|
22
|
-
}
|
|
23
|
-
return this;
|
|
24
|
-
}
|
|
25
|
-
|
|
26
12
|
/**
|
|
27
13
|
* Executes the batch get operation.
|
|
28
14
|
* Returns an array of parsed items of type T.
|
|
@@ -37,10 +23,6 @@ export class BatchGetBuilder<T> {
|
|
|
37
23
|
RequestItems: {
|
|
38
24
|
[tableName]: {
|
|
39
25
|
Keys: keysArray,
|
|
40
|
-
...(this.projectionExpression && {
|
|
41
|
-
ProjectionExpression: this.projectionExpression,
|
|
42
|
-
ExpressionAttributeNames: this.expressionAttributeNames,
|
|
43
|
-
}),
|
|
44
26
|
},
|
|
45
27
|
},
|
|
46
28
|
};
|
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
import { QueryCommand, QueryCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
2
|
import { BetterDDB, GSIConfig } from '../betterddb';
|
|
3
|
-
|
|
3
|
+
import { getOperatorExpression, Operator } from '../operator';
|
|
4
4
|
export class QueryBuilder<T> {
|
|
5
|
-
private
|
|
5
|
+
private keyConditions: string[] = [];
|
|
6
|
+
private filterConditions: string[] = [];
|
|
6
7
|
private expressionAttributeNames: Record<string, string> = {};
|
|
7
8
|
private expressionAttributeValues: Record<string, any> = {};
|
|
8
9
|
private index?: GSIConfig<T>;
|
|
9
|
-
private sortKeyCondition?: string;
|
|
10
10
|
private limit?: number;
|
|
11
11
|
private lastKey?: Record<string, any>;
|
|
12
12
|
private ascending: boolean = true;
|
|
13
13
|
|
|
14
|
-
constructor(private parent: BetterDDB<T>, private key: Partial<T
|
|
14
|
+
constructor(private parent: BetterDDB<T>, private key: Partial<T>, ) {}
|
|
15
15
|
|
|
16
16
|
public usingIndex(indexName: string): this {
|
|
17
17
|
if (!this.parent.getKeys().gsis) {
|
|
@@ -36,34 +36,87 @@ export class QueryBuilder<T> {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
public where(
|
|
39
|
+
operator: Operator,
|
|
40
|
+
values: Partial<T> | [Partial<T>, Partial<T>]
|
|
41
|
+
): this {
|
|
42
|
+
const keys = this.parent.getKeys();
|
|
43
|
+
// Determine the sort key name from either the index or the primary keys.
|
|
44
|
+
const sortKeyName = this.index ? this.index.sort?.name : keys.sort?.name;
|
|
45
|
+
if (!sortKeyName) {
|
|
46
|
+
throw new Error('Sort key is not defined for this table/index.');
|
|
47
|
+
}
|
|
48
|
+
const nameKey = '#sk';
|
|
49
|
+
this.expressionAttributeNames[nameKey] = sortKeyName;
|
|
50
|
+
|
|
51
|
+
// Enforce that a complex sort key requires an object input.
|
|
52
|
+
if (typeof values !== 'object' || values === null) {
|
|
53
|
+
throw new Error(`For complex sort keys, please provide an object with all necessary properties.`);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
if (operator === 'between') {
|
|
57
|
+
if (!Array.isArray(values) || values.length !== 2) {
|
|
58
|
+
throw new Error(`For 'between' operator, values must be a tuple of two objects`);
|
|
59
|
+
}
|
|
60
|
+
const valueKeyStart = ':sk_start';
|
|
61
|
+
const valueKeyEnd = ':sk_end';
|
|
62
|
+
// Use the key definition's build function to build the key from the full object.
|
|
63
|
+
this.expressionAttributeValues[valueKeyStart] = this.index
|
|
64
|
+
? this.parent.buildIndexes(values[0])[sortKeyName]
|
|
65
|
+
: this.parent.buildKey(values[0])[sortKeyName];
|
|
66
|
+
this.expressionAttributeValues[valueKeyEnd] = this.index
|
|
67
|
+
? this.parent.buildIndexes(values[1])[sortKeyName]
|
|
68
|
+
: this.parent.buildKey(values[1])[sortKeyName];
|
|
69
|
+
this.keyConditions.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
|
|
70
|
+
} else if (operator === 'begins_with') {
|
|
71
|
+
const valueKey = ':sk_value';
|
|
72
|
+
this.expressionAttributeValues[valueKey] = this.index
|
|
73
|
+
? this.parent.buildIndexes(values as Partial<T>)[sortKeyName]
|
|
74
|
+
: this.parent.buildKey(values as Partial<T>)[sortKeyName];
|
|
75
|
+
this.keyConditions.push(`begins_with(${nameKey}, ${valueKey})`);
|
|
76
|
+
} else {
|
|
77
|
+
// For eq, lt, lte, gt, gte:
|
|
78
|
+
const valueKey = ':sk_value';
|
|
79
|
+
this.expressionAttributeValues[valueKey] = this.index
|
|
80
|
+
? this.parent.buildIndexes(values as Partial<T>)[sortKeyName]
|
|
81
|
+
: this.parent.buildKey(values as Partial<T>)[sortKeyName];
|
|
82
|
+
const condition = getOperatorExpression(operator, nameKey, valueKey);
|
|
83
|
+
this.keyConditions.push(condition);
|
|
84
|
+
}
|
|
85
|
+
return this;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
public filter(
|
|
39
92
|
attribute: keyof T,
|
|
40
|
-
operator:
|
|
93
|
+
operator: Operator,
|
|
41
94
|
values: any | [any, any]
|
|
42
95
|
): this {
|
|
43
96
|
const attrStr = String(attribute);
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
if (operator === '
|
|
48
|
-
const valueKey = `:val_${attrStr}`;
|
|
49
|
-
this.expressionAttributeValues[valueKey] = values;
|
|
50
|
-
this.filters.push(`${nameKey} = ${valueKey}`);
|
|
51
|
-
} else if (operator === 'begins_with') {
|
|
52
|
-
const valueKey = `:val_${attrStr}`;
|
|
53
|
-
this.expressionAttributeValues[valueKey] = values;
|
|
54
|
-
this.filters.push(`begins_with(${nameKey}, ${valueKey})`);
|
|
55
|
-
} else if (operator === 'between') {
|
|
97
|
+
const randomString = Math.random().toString(36).substring(2, 15);
|
|
98
|
+
const placeholderName = `#attr_${attrStr}_${randomString}`;
|
|
99
|
+
this.expressionAttributeNames[placeholderName] = attrStr;
|
|
100
|
+
if (operator === 'between') {
|
|
56
101
|
if (!Array.isArray(values) || values.length !== 2) {
|
|
57
|
-
throw new Error(
|
|
102
|
+
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
58
103
|
}
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
this.expressionAttributeValues[
|
|
62
|
-
this.expressionAttributeValues[
|
|
63
|
-
this.
|
|
104
|
+
const placeholderValueStart = `:val_start_${attrStr}_${randomString}`;
|
|
105
|
+
const placeholderValueEnd = `:val_end_${attrStr}_${randomString}`;
|
|
106
|
+
this.expressionAttributeValues[placeholderValueStart] = values[0];
|
|
107
|
+
this.expressionAttributeValues[placeholderValueEnd] = values[1];
|
|
108
|
+
this.filterConditions.push(`${placeholderName} BETWEEN ${placeholderValueStart} AND ${placeholderValueEnd}`);
|
|
109
|
+
} else if (operator === 'begins_with' || operator === 'contains') {
|
|
110
|
+
const placeholderValue = `:val_${attrStr}_${randomString}`;
|
|
111
|
+
this.expressionAttributeValues[placeholderValue] = values;
|
|
112
|
+
this.filterConditions.push(`${operator}(${placeholderName}, ${placeholderValue})`);
|
|
64
113
|
} else {
|
|
65
|
-
|
|
114
|
+
const placeholderValue = `:val_${attrStr}_${randomString}`;
|
|
115
|
+
this.expressionAttributeValues[placeholderValue] = values;
|
|
116
|
+
const condition = getOperatorExpression(operator, placeholderName, placeholderValue);
|
|
117
|
+
this.filterConditions.push(condition);
|
|
66
118
|
}
|
|
119
|
+
|
|
67
120
|
return this;
|
|
68
121
|
}
|
|
69
122
|
|
|
@@ -88,14 +141,13 @@ export class QueryBuilder<T> {
|
|
|
88
141
|
pkName = this.index.primary.name;
|
|
89
142
|
builtKey = this.parent.buildIndexes(this.key);
|
|
90
143
|
}
|
|
91
|
-
this.expressionAttributeNames['#pk']
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
keyConditionExpression += ` AND ${this.sortKeyCondition}`;
|
|
144
|
+
if (!this.expressionAttributeNames['#pk']) {
|
|
145
|
+
this.expressionAttributeNames['#pk'] = pkName;
|
|
146
|
+
this.expressionAttributeValues[':pk_value'] = builtKey[pkName];
|
|
147
|
+
this.keyConditions.unshift(`#pk = :pk_value`);
|
|
96
148
|
}
|
|
97
149
|
|
|
98
|
-
this.
|
|
150
|
+
const keyConditionExpression = this.keyConditions.join(' AND ');
|
|
99
151
|
|
|
100
152
|
const params: QueryCommandInput = {
|
|
101
153
|
TableName: this.parent.getTableName(),
|
|
@@ -105,13 +157,13 @@ export class QueryBuilder<T> {
|
|
|
105
157
|
ScanIndexForward: this.ascending,
|
|
106
158
|
Limit: this.limit,
|
|
107
159
|
ExclusiveStartKey: this.lastKey,
|
|
108
|
-
IndexName: this.index?.name ?? undefined
|
|
160
|
+
IndexName: this.index?.name ?? undefined,
|
|
109
161
|
};
|
|
110
162
|
|
|
111
|
-
if (this.
|
|
112
|
-
params.FilterExpression = this.
|
|
163
|
+
if (this.filterConditions.length > 0) {
|
|
164
|
+
params.FilterExpression = this.filterConditions.join(' AND ');
|
|
113
165
|
}
|
|
114
|
-
|
|
166
|
+
console.log(params);
|
|
115
167
|
const result = await this.parent.getClient().send(new QueryCommand(params));
|
|
116
168
|
return this.parent.getSchema().array().parse(result.Items) as T[];
|
|
117
169
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { ScanCommand, ScanCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
2
|
import { BetterDDB } from '../betterddb';
|
|
3
|
-
|
|
3
|
+
import { getOperatorExpression, Operator } from '../operator';
|
|
4
4
|
export class ScanBuilder<T> {
|
|
5
5
|
private filters: string[] = [];
|
|
6
6
|
private expressionAttributeNames: Record<string, string> = {};
|
|
@@ -12,32 +12,35 @@ export class ScanBuilder<T> {
|
|
|
12
12
|
|
|
13
13
|
public where(
|
|
14
14
|
attribute: keyof T,
|
|
15
|
-
operator:
|
|
15
|
+
operator: Operator,
|
|
16
16
|
values: any | [any, any]
|
|
17
17
|
): this {
|
|
18
18
|
const attrStr = String(attribute);
|
|
19
19
|
const nameKey = `#attr_${attrStr}`;
|
|
20
20
|
this.expressionAttributeNames[nameKey] = attrStr;
|
|
21
21
|
|
|
22
|
-
if (operator === '
|
|
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') {
|
|
22
|
+
if (operator === 'between') {
|
|
31
23
|
if (!Array.isArray(values) || values.length !== 2) {
|
|
32
|
-
throw new Error(
|
|
24
|
+
throw new Error(
|
|
25
|
+
`For 'between' operator, values must be a tuple of two items`
|
|
26
|
+
);
|
|
33
27
|
}
|
|
34
28
|
const valueKeyStart = `:val_start_${attrStr}`;
|
|
35
29
|
const valueKeyEnd = `:val_end_${attrStr}`;
|
|
36
30
|
this.expressionAttributeValues[valueKeyStart] = values[0];
|
|
37
31
|
this.expressionAttributeValues[valueKeyEnd] = values[1];
|
|
38
|
-
this.filters.push(
|
|
32
|
+
this.filters.push(
|
|
33
|
+
`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`
|
|
34
|
+
);
|
|
35
|
+
} else if (operator === 'begins_with' || operator === 'contains') {
|
|
36
|
+
const valueKey = `:val_${attrStr}`;
|
|
37
|
+
this.expressionAttributeValues[valueKey] = values;
|
|
38
|
+
this.filters.push(`${operator}(${nameKey}, ${valueKey})`);
|
|
39
39
|
} else {
|
|
40
|
-
|
|
40
|
+
const valueKey = `:val_${attrStr}`;
|
|
41
|
+
this.expressionAttributeValues[valueKey] = values;
|
|
42
|
+
const condition = getOperatorExpression(operator, nameKey, valueKey);
|
|
43
|
+
this.filters.push(condition);
|
|
41
44
|
}
|
|
42
45
|
return this;
|
|
43
46
|
}
|
package/src/operator.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
export type Operator =
|
|
2
|
+
| '=='
|
|
3
|
+
| '!='
|
|
4
|
+
| '<'
|
|
5
|
+
| '<='
|
|
6
|
+
| '>'
|
|
7
|
+
| '>='
|
|
8
|
+
| 'begins_with'
|
|
9
|
+
| 'between'
|
|
10
|
+
| 'contains';
|
|
11
|
+
|
|
12
|
+
export function getOperatorExpression(
|
|
13
|
+
operator: Operator,
|
|
14
|
+
nameKey: string,
|
|
15
|
+
valueKey: string,
|
|
16
|
+
secondValueKey?: string
|
|
17
|
+
): string {
|
|
18
|
+
switch (operator) {
|
|
19
|
+
case '==':
|
|
20
|
+
return `${nameKey} = ${valueKey}`;
|
|
21
|
+
case '!=':
|
|
22
|
+
return `${nameKey} <> ${valueKey}`;
|
|
23
|
+
case '<':
|
|
24
|
+
return `${nameKey} < ${valueKey}`;
|
|
25
|
+
case '<=':
|
|
26
|
+
return `${nameKey} <= ${valueKey}`;
|
|
27
|
+
case '>':
|
|
28
|
+
return `${nameKey} > ${valueKey}`;
|
|
29
|
+
case '>=':
|
|
30
|
+
return `${nameKey} >= ${valueKey}`;
|
|
31
|
+
case 'begins_with':
|
|
32
|
+
return `begins_with(${nameKey}, ${valueKey})`;
|
|
33
|
+
case 'between':
|
|
34
|
+
if (!secondValueKey) {
|
|
35
|
+
throw new Error("The 'between' operator requires two value keys");
|
|
36
|
+
}
|
|
37
|
+
return `${nameKey} BETWEEN ${valueKey} AND ${secondValueKey}`;
|
|
38
|
+
case 'contains':
|
|
39
|
+
return `contains(${nameKey}, ${valueKey})`;
|
|
40
|
+
default:
|
|
41
|
+
throw new Error(`Unsupported operator: ${operator}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { BetterDDB } from '../src/betterddb';
|
|
3
|
+
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
4
|
+
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
5
|
+
import { DynamoDB, GlobalSecondaryIndex } from '@aws-sdk/client-dynamodb';
|
|
6
|
+
import { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
7
|
+
const TEST_TABLE = "batch-get-test-table";
|
|
8
|
+
const ENDPOINT = 'http://localhost:4566';
|
|
9
|
+
const REGION = 'us-east-1';
|
|
10
|
+
const ENTITY_NAME = 'USER';
|
|
11
|
+
const PRIMARY_KEY = 'pk';
|
|
12
|
+
const PRIMARY_KEY_TYPE = 'S';
|
|
13
|
+
const SORT_KEY = 'sk';
|
|
14
|
+
const SORT_KEY_TYPE = 'S';
|
|
15
|
+
const GSI_NAME = 'EmailIndex';
|
|
16
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
17
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
18
|
+
const KEY_SCHEMA = [
|
|
19
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
20
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
21
|
+
] as KeySchemaElement[];
|
|
22
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
23
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
24
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
25
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
27
|
+
] as AttributeDefinition[];
|
|
28
|
+
const GSIS = [
|
|
29
|
+
{
|
|
30
|
+
IndexName: GSI_NAME,
|
|
31
|
+
KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
|
|
32
|
+
Projection: {
|
|
33
|
+
ProjectionType: 'ALL',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
] as GlobalSecondaryIndex[];
|
|
37
|
+
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
38
|
+
region: REGION,
|
|
39
|
+
endpoint: ENDPOINT,
|
|
40
|
+
}));
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
const UserSchema = z.object({
|
|
44
|
+
id: z.string(),
|
|
45
|
+
name: z.string(),
|
|
46
|
+
email: z.string().email(),
|
|
47
|
+
createdAt: z.string(),
|
|
48
|
+
updatedAt: z.string(),
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
type User = z.infer<typeof UserSchema>;
|
|
52
|
+
|
|
53
|
+
const userDdb = new BetterDDB({
|
|
54
|
+
schema: UserSchema,
|
|
55
|
+
tableName: TEST_TABLE,
|
|
56
|
+
entityName: ENTITY_NAME,
|
|
57
|
+
keys: {
|
|
58
|
+
primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
|
|
59
|
+
sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
|
|
60
|
+
},
|
|
61
|
+
client,
|
|
62
|
+
autoTimestamps: true,
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
beforeAll(async () => {
|
|
66
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
67
|
+
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
68
|
+
await userDdb.create({ id: 'user-124', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
69
|
+
await userDdb.create({ id: 'user-125', name: 'Bob Doe', email: 'bob@example.com' } as any).execute();
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
afterAll(async () => {
|
|
73
|
+
await deleteTestTable(TEST_TABLE);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
describe('BetterDDB - Get Operation', () => {
|
|
77
|
+
it('should retrieve an item using GetBuilder', async () => {
|
|
78
|
+
const users = await userDdb.batchGet([{ id: 'user-123', email: 'john@example.com' }, { id: 'user-124', email: 'john@example.com' }]).execute();
|
|
79
|
+
expect(users.length).toEqual(2);
|
|
80
|
+
expect(users.some(user => user.id === 'user-123')).toBe(true);
|
|
81
|
+
expect(users.some(user => user.id === 'user-124')).toBe(true);
|
|
82
|
+
});
|
|
83
|
+
});
|
package/test/create.test.ts
CHANGED
|
@@ -2,19 +2,39 @@ import { z } from 'zod';
|
|
|
2
2
|
import { BetterDDB } from '../src/betterddb';
|
|
3
3
|
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
4
4
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
5
|
-
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
5
|
+
import { DynamoDB, GlobalSecondaryIndex } from '@aws-sdk/client-dynamodb';
|
|
6
6
|
import { GetCommand } from '@aws-sdk/lib-dynamodb';
|
|
7
7
|
import { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
8
8
|
const TEST_TABLE = "create-test-table";
|
|
9
9
|
const ENDPOINT = 'http://localhost:4566';
|
|
10
10
|
const REGION = 'us-east-1';
|
|
11
11
|
const ENTITY_NAME = 'USER';
|
|
12
|
-
const PRIMARY_KEY = '
|
|
12
|
+
const PRIMARY_KEY = 'pk';
|
|
13
13
|
const PRIMARY_KEY_TYPE = 'S';
|
|
14
|
-
const SORT_KEY = '
|
|
14
|
+
const SORT_KEY = 'sk';
|
|
15
15
|
const SORT_KEY_TYPE = 'S';
|
|
16
|
-
const
|
|
17
|
-
const
|
|
16
|
+
const GSI_NAME = 'EmailIndex';
|
|
17
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
18
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
19
|
+
const KEY_SCHEMA = [
|
|
20
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
21
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
22
|
+
] as KeySchemaElement[];
|
|
23
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
24
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
25
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
27
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
28
|
+
] as AttributeDefinition[];
|
|
29
|
+
const GSIS = [
|
|
30
|
+
{
|
|
31
|
+
IndexName: GSI_NAME,
|
|
32
|
+
KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
|
|
33
|
+
Projection: {
|
|
34
|
+
ProjectionType: 'ALL',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
] as GlobalSecondaryIndex[];
|
|
18
38
|
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
19
39
|
region: REGION,
|
|
20
40
|
endpoint: ENDPOINT,
|
|
@@ -44,7 +64,7 @@ const userDdb = new BetterDDB({
|
|
|
44
64
|
});
|
|
45
65
|
|
|
46
66
|
beforeAll(async () => {
|
|
47
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
67
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
48
68
|
});
|
|
49
69
|
|
|
50
70
|
afterAll(async () => {
|
|
@@ -57,7 +77,7 @@ describe('BetterDDB - Create Operation', () => {
|
|
|
57
77
|
|
|
58
78
|
await userDdb.create(user as any).execute();
|
|
59
79
|
|
|
60
|
-
const result = await client.send(new GetCommand({ TableName: TEST_TABLE, Key: {
|
|
80
|
+
const result = await client.send(new GetCommand({ TableName: TEST_TABLE, Key: { pk: 'USER#user-123', sk: 'EMAIL#john@example.com' } }));
|
|
61
81
|
|
|
62
82
|
expect(result).not.toBeNull();
|
|
63
83
|
expect(result.Item).not.toBeNull();
|
package/test/delete.test.ts
CHANGED
|
@@ -4,17 +4,37 @@ import { BetterDDB } from '../src/betterddb';
|
|
|
4
4
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
5
5
|
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
6
6
|
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
7
|
-
import { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
7
|
+
import { KeySchemaElement, AttributeDefinition, GlobalSecondaryIndex } from '@aws-sdk/client-dynamodb';
|
|
8
8
|
const TEST_TABLE = "delete-test-table";
|
|
9
9
|
const ENDPOINT = 'http://localhost:4566';
|
|
10
10
|
const REGION = 'us-east-1';
|
|
11
11
|
const ENTITY_NAME = 'USER';
|
|
12
|
-
const PRIMARY_KEY = '
|
|
12
|
+
const PRIMARY_KEY = 'pk';
|
|
13
13
|
const PRIMARY_KEY_TYPE = 'S';
|
|
14
|
-
const SORT_KEY = '
|
|
14
|
+
const SORT_KEY = 'sk';
|
|
15
15
|
const SORT_KEY_TYPE = 'S';
|
|
16
|
-
const
|
|
17
|
-
const
|
|
16
|
+
const GSI_NAME = 'EmailIndex';
|
|
17
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
18
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
19
|
+
const KEY_SCHEMA = [
|
|
20
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
21
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
22
|
+
] as KeySchemaElement[];
|
|
23
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
24
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
25
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
27
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
28
|
+
] as AttributeDefinition[];
|
|
29
|
+
const GSIS = [
|
|
30
|
+
{
|
|
31
|
+
IndexName: GSI_NAME,
|
|
32
|
+
KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
|
|
33
|
+
Projection: {
|
|
34
|
+
ProjectionType: 'ALL',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
] as GlobalSecondaryIndex[];
|
|
18
38
|
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
19
39
|
region: REGION,
|
|
20
40
|
endpoint: ENDPOINT,
|
|
@@ -43,7 +63,7 @@ const userDdb = new BetterDDB({
|
|
|
43
63
|
});
|
|
44
64
|
|
|
45
65
|
beforeAll(async () => {
|
|
46
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
66
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
47
67
|
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
48
68
|
});
|
|
49
69
|
|
package/test/get.test.ts
CHANGED
|
@@ -3,18 +3,39 @@ import { z } from 'zod';
|
|
|
3
3
|
import { BetterDDB } from '../src/betterddb';
|
|
4
4
|
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
5
5
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
6
|
-
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
6
|
+
import { DynamoDB, GlobalSecondaryIndex } from '@aws-sdk/client-dynamodb';
|
|
7
7
|
import { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
8
8
|
const TEST_TABLE = "get-test-table";
|
|
9
9
|
const ENDPOINT = 'http://localhost:4566';
|
|
10
10
|
const REGION = 'us-east-1';
|
|
11
11
|
const ENTITY_NAME = 'USER';
|
|
12
|
-
const PRIMARY_KEY = '
|
|
12
|
+
const PRIMARY_KEY = 'pk';
|
|
13
13
|
const PRIMARY_KEY_TYPE = 'S';
|
|
14
|
-
const SORT_KEY = '
|
|
14
|
+
const SORT_KEY = 'sk';
|
|
15
15
|
const SORT_KEY_TYPE = 'S';
|
|
16
|
-
const
|
|
17
|
-
const
|
|
16
|
+
const GSI_NAME = 'EmailIndex';
|
|
17
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
18
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
19
|
+
const KEY_SCHEMA = [
|
|
20
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
21
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
22
|
+
] as KeySchemaElement[];
|
|
23
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
24
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
25
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
27
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
28
|
+
] as AttributeDefinition[];
|
|
29
|
+
const GSIS = [
|
|
30
|
+
{
|
|
31
|
+
IndexName: GSI_NAME,
|
|
32
|
+
KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
|
|
33
|
+
Projection: {
|
|
34
|
+
ProjectionType: 'ALL',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
] as GlobalSecondaryIndex[];
|
|
38
|
+
|
|
18
39
|
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
19
40
|
region: REGION,
|
|
20
41
|
endpoint: ENDPOINT,
|
|
@@ -44,7 +65,7 @@ const userDdb = new BetterDDB({
|
|
|
44
65
|
});
|
|
45
66
|
|
|
46
67
|
beforeAll(async () => {
|
|
47
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
68
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
48
69
|
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
49
70
|
});
|
|
50
71
|
|
package/test/query.test.ts
CHANGED
|
@@ -1,19 +1,41 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { BetterDDB } from '../src/betterddb';
|
|
3
3
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
import { AttributeDefinition, DynamoDB, KeySchemaElement } from '@aws-sdk/client-dynamodb';
|
|
4
|
+
import { AttributeDefinition, DynamoDB, GlobalSecondaryIndex, KeySchemaElement } from '@aws-sdk/client-dynamodb';
|
|
5
5
|
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
6
6
|
|
|
7
7
|
const TEST_TABLE = "query-test-table";
|
|
8
8
|
const ENDPOINT = 'http://localhost:4566';
|
|
9
9
|
const REGION = 'us-east-1';
|
|
10
10
|
const ENTITY_NAME = 'USER';
|
|
11
|
-
const PRIMARY_KEY = '
|
|
11
|
+
const PRIMARY_KEY = 'pk';
|
|
12
12
|
const PRIMARY_KEY_TYPE = 'S';
|
|
13
|
-
const SORT_KEY = '
|
|
13
|
+
const SORT_KEY = 'sk';
|
|
14
14
|
const SORT_KEY_TYPE = 'S';
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const GSI_NAME = 'EmailIndex';
|
|
16
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
17
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
18
|
+
const KEY_SCHEMA = [
|
|
19
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
20
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
21
|
+
] as KeySchemaElement[];
|
|
22
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
23
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
24
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
25
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
27
|
+
] as AttributeDefinition[];
|
|
28
|
+
const GSIS = [
|
|
29
|
+
{
|
|
30
|
+
IndexName: GSI_NAME,
|
|
31
|
+
KeySchema: [
|
|
32
|
+
{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' },
|
|
33
|
+
{ AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }
|
|
34
|
+
],
|
|
35
|
+
Projection: { ProjectionType: 'ALL' },
|
|
36
|
+
},
|
|
37
|
+
] as GlobalSecondaryIndex[];
|
|
38
|
+
|
|
17
39
|
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
18
40
|
region: REGION,
|
|
19
41
|
endpoint: ENDPOINT,
|
|
@@ -34,11 +56,25 @@ const userDdb = new BetterDDB({
|
|
|
34
56
|
tableName: TEST_TABLE,
|
|
35
57
|
entityName: ENTITY_NAME,
|
|
36
58
|
keys: {
|
|
37
|
-
primary: {
|
|
59
|
+
primary: {
|
|
60
|
+
name: PRIMARY_KEY,
|
|
61
|
+
definition: { build: (raw) => `USER#${raw.id}` }
|
|
62
|
+
},
|
|
63
|
+
sort: {
|
|
64
|
+
name: SORT_KEY,
|
|
65
|
+
definition: { build: (raw) => `USER#${raw.email}` }
|
|
66
|
+
},
|
|
38
67
|
gsis: {
|
|
39
68
|
EmailIndex: {
|
|
40
69
|
name: 'EmailIndex',
|
|
41
|
-
primary: {
|
|
70
|
+
primary: {
|
|
71
|
+
name: GSI_PRIMARY_KEY,
|
|
72
|
+
definition: { build: (raw) => `USER#${raw.email}` }
|
|
73
|
+
},
|
|
74
|
+
sort: {
|
|
75
|
+
name: GSI_SORT_KEY,
|
|
76
|
+
definition: { build: (raw) => `USER#${raw.email}` }
|
|
77
|
+
}
|
|
42
78
|
}
|
|
43
79
|
}
|
|
44
80
|
},
|
|
@@ -47,16 +83,14 @@ const userDdb = new BetterDDB({
|
|
|
47
83
|
});
|
|
48
84
|
|
|
49
85
|
beforeAll(async () => {
|
|
50
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
86
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
51
87
|
|
|
52
88
|
const items = [
|
|
53
89
|
{ id: 'user-1', name: 'Alice', email: 'alice@example.com' },
|
|
54
90
|
{ id: 'user-2', name: 'Alice B', email: 'alice@example.com' },
|
|
55
91
|
{ id: 'user-3', name: 'Bob', email: 'bob@example.com' }
|
|
56
92
|
];
|
|
57
|
-
|
|
58
|
-
await userDdb.create(item as any).execute();
|
|
59
|
-
}
|
|
93
|
+
await Promise.all(items.map(item => userDdb.create(item as any).execute()));
|
|
60
94
|
});
|
|
61
95
|
|
|
62
96
|
afterAll(async () => {
|
|
@@ -64,10 +98,11 @@ afterAll(async () => {
|
|
|
64
98
|
});
|
|
65
99
|
|
|
66
100
|
describe('BetterDDB - Query Operation', () => {
|
|
67
|
-
it('should query items using QueryBuilder', async () => {
|
|
101
|
+
it('should query items using QueryBuilder with filter condition', async () => {
|
|
68
102
|
const results = await userDdb.query({ id: 'user-1' })
|
|
69
|
-
.
|
|
70
|
-
.limitResults(5)
|
|
103
|
+
.filter('name', 'begins_with', 'Alice')
|
|
104
|
+
.limitResults(5)
|
|
105
|
+
.execute();
|
|
71
106
|
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
72
107
|
results.forEach(result => {
|
|
73
108
|
expect(result.name).toMatch(/^Alice/);
|
|
@@ -84,4 +119,66 @@ describe('BetterDDB - Query Operation', () => {
|
|
|
84
119
|
expect(result.email).toEqual('alice@example.com');
|
|
85
120
|
});
|
|
86
121
|
});
|
|
122
|
+
|
|
123
|
+
it('should query items using QueryBuilder with a sort key condition', async () => {
|
|
124
|
+
// For a complex sort key, users must supply an object.
|
|
125
|
+
const results = await userDdb.query({ id: 'user-1' })
|
|
126
|
+
.where('begins_with', { email: 'alice' })
|
|
127
|
+
.execute();
|
|
128
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
129
|
+
results.forEach(result => {
|
|
130
|
+
expect(result.email).toMatch(/^alice/i);
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it('should return no results if the sort key condition does not match', async () => {
|
|
135
|
+
const results = await userDdb.query({ id: 'user-1' })
|
|
136
|
+
.where('begins_with', { email: 'bob' })
|
|
137
|
+
.execute();
|
|
138
|
+
expect(results.length).toEqual(0);
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should query items using QueryBuilder with index and additional filter', async () => {
|
|
142
|
+
const results = await userDdb.query({ email: 'alice@example.com' })
|
|
143
|
+
.usingIndex('EmailIndex')
|
|
144
|
+
.filter('name', 'begins_with', 'Alice')
|
|
145
|
+
.execute();
|
|
146
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
147
|
+
results.forEach(result => {
|
|
148
|
+
expect(result.email).toEqual('alice@example.com');
|
|
149
|
+
expect(result.name).toMatch(/^Alice/);
|
|
150
|
+
});
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
it('should query items using QueryBuilder with a sort key condition using "between"', async () => {
|
|
155
|
+
// Here we use the "between" operator. The sort key build function produces a value like "USER#alice@example.com"
|
|
156
|
+
// We provide lower and upper bounds as objects.
|
|
157
|
+
const results = await userDdb.query({ id: 'user-1' })
|
|
158
|
+
.where('between', [
|
|
159
|
+
{ email: 'alice' }, // Lower bound -> built to "USER#alice"
|
|
160
|
+
{ email: 'alice@example.com' } // Upper bound -> built to "USER#alice@example.com"
|
|
161
|
+
])
|
|
162
|
+
.execute();
|
|
163
|
+
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
164
|
+
results.forEach(result => {
|
|
165
|
+
// The built sort key for user-1 is "USER#alice@example.com"
|
|
166
|
+
expect(result.email).toMatch(/alice@example\.com/i);
|
|
167
|
+
});
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
it('should query items using QueryBuilder with multiple filter conditions on an index', async () => {
|
|
171
|
+
// Query the GSI for email "alice@example.com". Two items match.
|
|
172
|
+
// Then apply two filter conditions: name begins_with "Alice" and name contains "B" should only match one.
|
|
173
|
+
const results = await userDdb.query({ email: 'alice@example.com' })
|
|
174
|
+
.usingIndex('EmailIndex')
|
|
175
|
+
.filter('name', 'begins_with', 'Alice')
|
|
176
|
+
.filter('name', 'contains', 'B')
|
|
177
|
+
.execute();
|
|
178
|
+
expect(results.length).toEqual(1);
|
|
179
|
+
results.forEach(result => {
|
|
180
|
+
expect(result.name).toMatch(/^Alice/);
|
|
181
|
+
expect(result.name).toContain('B');
|
|
182
|
+
});
|
|
183
|
+
});
|
|
87
184
|
});
|
package/test/scan.test.ts
CHANGED
|
@@ -1,19 +1,39 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
import { BetterDDB } from '../src/betterddb';
|
|
3
3
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
4
|
+
import { DynamoDB, GlobalSecondaryIndex } from '@aws-sdk/client-dynamodb';
|
|
5
5
|
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
6
6
|
import { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
7
7
|
const TEST_TABLE = "scan-test-table";
|
|
8
8
|
const ENDPOINT = 'http://localhost:4566';
|
|
9
9
|
const REGION = 'us-east-1';
|
|
10
10
|
const ENTITY_NAME = 'USER';
|
|
11
|
-
const PRIMARY_KEY = '
|
|
11
|
+
const PRIMARY_KEY = 'pk';
|
|
12
12
|
const PRIMARY_KEY_TYPE = 'S';
|
|
13
|
-
const SORT_KEY = '
|
|
13
|
+
const SORT_KEY = 'sk';
|
|
14
14
|
const SORT_KEY_TYPE = 'S';
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const GSI_NAME = 'EmailIndex';
|
|
16
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
17
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
18
|
+
const KEY_SCHEMA = [
|
|
19
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
20
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
21
|
+
] as KeySchemaElement[];
|
|
22
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
23
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
24
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
25
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
27
|
+
] as AttributeDefinition[];
|
|
28
|
+
const GSIS = [
|
|
29
|
+
{
|
|
30
|
+
IndexName: GSI_NAME,
|
|
31
|
+
KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
|
|
32
|
+
Projection: {
|
|
33
|
+
ProjectionType: 'ALL',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
] as GlobalSecondaryIndex[];
|
|
17
37
|
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
18
38
|
region: REGION,
|
|
19
39
|
endpoint: ENDPOINT,
|
|
@@ -35,22 +55,22 @@ const userDdb = new BetterDDB({
|
|
|
35
55
|
tableName: TEST_TABLE,
|
|
36
56
|
entityName: ENTITY_NAME,
|
|
37
57
|
keys: {
|
|
38
|
-
primary: { name: PRIMARY_KEY, definition:
|
|
58
|
+
primary: { name: PRIMARY_KEY, definition: { build: (raw) => `USER#${raw.id}` } },
|
|
59
|
+
sort: { name: SORT_KEY, definition: { build: (raw) => `EMAIL#${raw.email}` } },
|
|
39
60
|
},
|
|
40
61
|
client,
|
|
41
62
|
autoTimestamps: true,
|
|
42
63
|
});
|
|
43
64
|
|
|
44
65
|
beforeAll(async () => {
|
|
45
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
66
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
46
67
|
// Insert multiple items.
|
|
47
68
|
const items = [
|
|
48
|
-
{ id: 'user-
|
|
49
|
-
{ id: 'user-
|
|
69
|
+
{ id: 'user-1', name: 'Alice', email: 'alice@example.com' },
|
|
70
|
+
{ id: 'user-2', name: 'Alice B', email: 'alice@example.com' },
|
|
71
|
+
{ id: 'user-3', name: 'Bob', email: 'bob@example.com' }
|
|
50
72
|
];
|
|
51
|
-
|
|
52
|
-
await userDdb.create(item as any).execute();
|
|
53
|
-
}
|
|
73
|
+
await Promise.all(items.map(item => userDdb.create(item as any).execute()));
|
|
54
74
|
});
|
|
55
75
|
|
|
56
76
|
afterAll(async () => {
|
|
@@ -60,11 +80,36 @@ afterAll(async () => {
|
|
|
60
80
|
describe('BetterDDB - Scan Operation', () => {
|
|
61
81
|
it('should scan items using ScanBuilder', async () => {
|
|
62
82
|
const results = await userDdb.scan()
|
|
63
|
-
.where('email', 'begins_with', '
|
|
83
|
+
.where('email', 'begins_with', 'a')
|
|
64
84
|
.limitResults(10).execute();
|
|
65
85
|
expect(results.length).toBeGreaterThanOrEqual(1);
|
|
66
86
|
results.forEach(result => {
|
|
67
|
-
expect(result.email).toMatch(/^
|
|
87
|
+
expect(result.email).toMatch(/^alice/i);
|
|
88
|
+
});
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should scan items using ScanBuilder with a contains filter', async () => {
|
|
92
|
+
// Scan for users whose name contains "Alice"
|
|
93
|
+
const results = await userDdb.scan()
|
|
94
|
+
.where('name', 'contains', 'Alice')
|
|
95
|
+
.limitResults(10)
|
|
96
|
+
.execute();
|
|
97
|
+
expect(results.length).toBeGreaterThan(0);
|
|
98
|
+
results.forEach(result => {
|
|
99
|
+
expect(result.name).toContain('Alice');
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('should scan items using ScanBuilder with a between filter on email', async () => {
|
|
104
|
+
// Using lexicographical order on the email address:
|
|
105
|
+
// 'alice@example.com' should be between "a" and "c".
|
|
106
|
+
const results = await userDdb.scan()
|
|
107
|
+
.where('email', 'between', ['a', 'c'])
|
|
108
|
+
.execute();
|
|
109
|
+
expect(results.length).toBeGreaterThan(0);
|
|
110
|
+
results.forEach(result => {
|
|
111
|
+
// A simple lexicographical check
|
|
112
|
+
expect(result.email >= 'a' && result.email <= 'c').toBeTruthy();
|
|
68
113
|
});
|
|
69
114
|
});
|
|
70
115
|
});
|
package/test/update.test.ts
CHANGED
|
@@ -2,18 +2,38 @@ import { z } from 'zod';
|
|
|
2
2
|
import { BetterDDB } from '../src/betterddb';
|
|
3
3
|
import { DynamoDBDocumentClient } from '@aws-sdk/lib-dynamodb';
|
|
4
4
|
import { createTestTable, deleteTestTable } from './utils/table-setup';
|
|
5
|
-
import { DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
5
|
+
import { DynamoDB, GlobalSecondaryIndex } from '@aws-sdk/client-dynamodb';
|
|
6
6
|
import { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
7
7
|
const TEST_TABLE = "update-test-table";
|
|
8
8
|
const ENDPOINT = 'http://localhost:4566';
|
|
9
9
|
const REGION = 'us-east-1';
|
|
10
10
|
const ENTITY_NAME = 'USER';
|
|
11
|
-
const PRIMARY_KEY = '
|
|
11
|
+
const PRIMARY_KEY = 'pk';
|
|
12
12
|
const PRIMARY_KEY_TYPE = 'S';
|
|
13
|
-
const SORT_KEY = '
|
|
13
|
+
const SORT_KEY = 'sk';
|
|
14
14
|
const SORT_KEY_TYPE = 'S';
|
|
15
|
-
const
|
|
16
|
-
const
|
|
15
|
+
const GSI_NAME = 'EmailIndex';
|
|
16
|
+
const GSI_PRIMARY_KEY = 'gsi1pk';
|
|
17
|
+
const GSI_SORT_KEY = 'gsi1sk';
|
|
18
|
+
const KEY_SCHEMA = [
|
|
19
|
+
{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' },
|
|
20
|
+
{ AttributeName: SORT_KEY, KeyType: 'RANGE' }
|
|
21
|
+
] as KeySchemaElement[];
|
|
22
|
+
const ATTRIBUTE_DEFINITIONS = [
|
|
23
|
+
{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
24
|
+
{ AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
25
|
+
{ AttributeName: GSI_PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE },
|
|
26
|
+
{ AttributeName: GSI_SORT_KEY, AttributeType: SORT_KEY_TYPE },
|
|
27
|
+
] as AttributeDefinition[];
|
|
28
|
+
const GSIS = [
|
|
29
|
+
{
|
|
30
|
+
IndexName: GSI_NAME,
|
|
31
|
+
KeySchema: [{ AttributeName: GSI_PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: GSI_SORT_KEY, KeyType: 'RANGE' }],
|
|
32
|
+
Projection: {
|
|
33
|
+
ProjectionType: 'ALL',
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
] as GlobalSecondaryIndex[];
|
|
17
37
|
const client = DynamoDBDocumentClient.from(new DynamoDB({
|
|
18
38
|
region: REGION,
|
|
19
39
|
endpoint: ENDPOINT,
|
|
@@ -44,7 +64,7 @@ const userDdb = new BetterDDB({
|
|
|
44
64
|
});
|
|
45
65
|
|
|
46
66
|
beforeAll(async () => {
|
|
47
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
67
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
48
68
|
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
49
69
|
});
|
|
50
70
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { CreateTableCommandInput, DynamoDB } from '@aws-sdk/client-dynamodb';
|
|
2
2
|
|
|
3
|
-
export const createTestTable = async (tableName: string, keySchema: CreateTableCommandInput['KeySchema'], attributeDefinitions: CreateTableCommandInput['AttributeDefinitions']) => {
|
|
3
|
+
export const createTestTable = async (tableName: string, keySchema: CreateTableCommandInput['KeySchema'], attributeDefinitions: CreateTableCommandInput['AttributeDefinitions'], gsis: CreateTableCommandInput['GlobalSecondaryIndexes']) => {
|
|
4
4
|
const dynamoDB = new DynamoDB({
|
|
5
5
|
region: 'us-east-1',
|
|
6
6
|
endpoint: 'http://localhost:4566',
|
|
@@ -14,15 +14,7 @@ export const createTestTable = async (tableName: string, keySchema: CreateTableC
|
|
|
14
14
|
KeySchema: keySchema,
|
|
15
15
|
AttributeDefinitions: attributeDefinitions,
|
|
16
16
|
BillingMode: 'PAY_PER_REQUEST',
|
|
17
|
-
GlobalSecondaryIndexes:
|
|
18
|
-
{
|
|
19
|
-
IndexName: 'EmailIndex',
|
|
20
|
-
KeySchema: [{ AttributeName: 'email', KeyType: 'HASH' }],
|
|
21
|
-
Projection: {
|
|
22
|
-
ProjectionType: 'ALL',
|
|
23
|
-
},
|
|
24
|
-
},
|
|
25
|
-
],
|
|
17
|
+
GlobalSecondaryIndexes: gsis,
|
|
26
18
|
});
|
|
27
19
|
} catch (error: any) {
|
|
28
20
|
if (error.code === 'ResourceInUseException') {
|