betterddb 0.4.8 → 0.5.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/README.md +2 -2
- package/lib/betterddb.d.ts +7 -6
- package/lib/betterddb.js +9 -6
- package/lib/builders/create-builder.js +4 -6
- package/lib/builders/query-builder.d.ts +7 -4
- package/lib/builders/query-builder.js +80 -31
- package/lib/builders/scan-builder.d.ts +4 -2
- package/lib/builders/scan-builder.js +13 -13
- package/lib/builders/update-builder.d.ts +1 -2
- package/lib/builders/update-builder.js +1 -24
- package/lib/operator.d.ts +2 -0
- package/lib/operator.js +31 -0
- package/lib/types/paginated-result.d.ts +4 -0
- package/lib/types/paginated-result.js +2 -0
- package/package.json +1 -1
- package/src/betterddb.ts +14 -10
- package/src/builders/create-builder.ts +5 -6
- package/src/builders/query-builder.ts +90 -35
- package/src/builders/scan-builder.ts +21 -15
- package/src/builders/update-builder.ts +1 -27
- package/src/operator.ts +43 -0
- package/src/types/paginated-result.ts +4 -0
- package/test/batch-get.test.ts +31 -13
- package/test/create.test.ts +30 -12
- package/test/delete.test.ts +29 -11
- package/test/get.test.ts +30 -11
- package/test/query.test.ts +118 -23
- package/test/scan.test.ts +64 -21
- package/test/update.test.ts +29 -12
- package/test/utils/table-setup.ts +2 -10
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
import { QueryCommand, QueryCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
2
|
import { BetterDDB, GSIConfig } from '../betterddb';
|
|
3
|
+
import { getOperatorExpression, Operator } from '../operator';
|
|
4
|
+
import { PaginatedResult } from '../types/paginated-result';
|
|
3
5
|
|
|
4
6
|
export class QueryBuilder<T> {
|
|
5
|
-
private
|
|
7
|
+
private keyConditions: string[] = [];
|
|
8
|
+
private filterConditions: string[] = [];
|
|
6
9
|
private expressionAttributeNames: Record<string, string> = {};
|
|
7
10
|
private expressionAttributeValues: Record<string, any> = {};
|
|
8
11
|
private index?: GSIConfig<T>;
|
|
9
|
-
private sortKeyCondition?: string;
|
|
10
12
|
private limit?: number;
|
|
11
13
|
private lastKey?: Record<string, any>;
|
|
12
14
|
private ascending: boolean = true;
|
|
13
15
|
|
|
14
|
-
constructor(private parent: BetterDDB<T>, private key: Partial<T
|
|
16
|
+
constructor(private parent: BetterDDB<T>, private key: Partial<T>, ) {}
|
|
15
17
|
|
|
16
18
|
public usingIndex(indexName: string): this {
|
|
17
19
|
if (!this.parent.getKeys().gsis) {
|
|
@@ -36,34 +38,87 @@ export class QueryBuilder<T> {
|
|
|
36
38
|
}
|
|
37
39
|
|
|
38
40
|
public where(
|
|
41
|
+
operator: Operator,
|
|
42
|
+
values: Partial<T> | [Partial<T>, Partial<T>]
|
|
43
|
+
): this {
|
|
44
|
+
const keys = this.parent.getKeys();
|
|
45
|
+
// Determine the sort key name from either the index or the primary keys.
|
|
46
|
+
const sortKeyName = this.index ? this.index.sort?.name : keys.sort?.name;
|
|
47
|
+
if (!sortKeyName) {
|
|
48
|
+
throw new Error('Sort key is not defined for this table/index.');
|
|
49
|
+
}
|
|
50
|
+
const nameKey = '#sk';
|
|
51
|
+
this.expressionAttributeNames[nameKey] = sortKeyName;
|
|
52
|
+
|
|
53
|
+
// Enforce that a complex sort key requires an object input.
|
|
54
|
+
if (typeof values !== 'object' || values === null) {
|
|
55
|
+
throw new Error(`For complex sort keys, please provide an object with all necessary properties.`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (operator === 'between') {
|
|
59
|
+
if (!Array.isArray(values) || values.length !== 2) {
|
|
60
|
+
throw new Error(`For 'between' operator, values must be a tuple of two objects`);
|
|
61
|
+
}
|
|
62
|
+
const valueKeyStart = ':sk_start';
|
|
63
|
+
const valueKeyEnd = ':sk_end';
|
|
64
|
+
// Use the key definition's build function to build the key from the full object.
|
|
65
|
+
this.expressionAttributeValues[valueKeyStart] = this.index
|
|
66
|
+
? this.parent.buildIndexes(values[0])[sortKeyName]
|
|
67
|
+
: this.parent.buildKey(values[0])[sortKeyName];
|
|
68
|
+
this.expressionAttributeValues[valueKeyEnd] = this.index
|
|
69
|
+
? this.parent.buildIndexes(values[1])[sortKeyName]
|
|
70
|
+
: this.parent.buildKey(values[1])[sortKeyName];
|
|
71
|
+
this.keyConditions.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
|
|
72
|
+
} else if (operator === 'begins_with') {
|
|
73
|
+
const valueKey = ':sk_value';
|
|
74
|
+
this.expressionAttributeValues[valueKey] = this.index
|
|
75
|
+
? this.parent.buildIndexes(values as Partial<T>)[sortKeyName]
|
|
76
|
+
: this.parent.buildKey(values as Partial<T>)[sortKeyName];
|
|
77
|
+
this.keyConditions.push(`begins_with(${nameKey}, ${valueKey})`);
|
|
78
|
+
} else {
|
|
79
|
+
// For eq, lt, lte, gt, gte:
|
|
80
|
+
const valueKey = ':sk_value';
|
|
81
|
+
this.expressionAttributeValues[valueKey] = this.index
|
|
82
|
+
? this.parent.buildIndexes(values as Partial<T>)[sortKeyName]
|
|
83
|
+
: this.parent.buildKey(values as Partial<T>)[sortKeyName];
|
|
84
|
+
const condition = getOperatorExpression(operator, nameKey, valueKey);
|
|
85
|
+
this.keyConditions.push(condition);
|
|
86
|
+
}
|
|
87
|
+
return this;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
public filter(
|
|
39
94
|
attribute: keyof T,
|
|
40
|
-
operator:
|
|
95
|
+
operator: Operator,
|
|
41
96
|
values: any | [any, any]
|
|
42
97
|
): this {
|
|
43
98
|
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') {
|
|
99
|
+
const randomString = Math.random().toString(36).substring(2, 15);
|
|
100
|
+
const placeholderName = `#attr_${attrStr}_${randomString}`;
|
|
101
|
+
this.expressionAttributeNames[placeholderName] = attrStr;
|
|
102
|
+
if (operator === 'between') {
|
|
56
103
|
if (!Array.isArray(values) || values.length !== 2) {
|
|
57
|
-
throw new Error(
|
|
104
|
+
throw new Error("For 'between' operator, values must be a tuple of two items");
|
|
58
105
|
}
|
|
59
|
-
const
|
|
60
|
-
const
|
|
61
|
-
this.expressionAttributeValues[
|
|
62
|
-
this.expressionAttributeValues[
|
|
63
|
-
this.
|
|
106
|
+
const placeholderValueStart = `:val_start_${attrStr}_${randomString}`;
|
|
107
|
+
const placeholderValueEnd = `:val_end_${attrStr}_${randomString}`;
|
|
108
|
+
this.expressionAttributeValues[placeholderValueStart] = values[0];
|
|
109
|
+
this.expressionAttributeValues[placeholderValueEnd] = values[1];
|
|
110
|
+
this.filterConditions.push(`${placeholderName} BETWEEN ${placeholderValueStart} AND ${placeholderValueEnd}`);
|
|
111
|
+
} else if (operator === 'begins_with' || operator === 'contains') {
|
|
112
|
+
const placeholderValue = `:val_${attrStr}_${randomString}`;
|
|
113
|
+
this.expressionAttributeValues[placeholderValue] = values;
|
|
114
|
+
this.filterConditions.push(`${operator}(${placeholderName}, ${placeholderValue})`);
|
|
64
115
|
} else {
|
|
65
|
-
|
|
116
|
+
const placeholderValue = `:val_${attrStr}_${randomString}`;
|
|
117
|
+
this.expressionAttributeValues[placeholderValue] = values;
|
|
118
|
+
const condition = getOperatorExpression(operator, placeholderName, placeholderValue);
|
|
119
|
+
this.filterConditions.push(condition);
|
|
66
120
|
}
|
|
121
|
+
|
|
67
122
|
return this;
|
|
68
123
|
}
|
|
69
124
|
|
|
@@ -80,7 +135,7 @@ export class QueryBuilder<T> {
|
|
|
80
135
|
/**
|
|
81
136
|
* Executes the query and returns a Promise that resolves with an array of items.
|
|
82
137
|
*/
|
|
83
|
-
public async execute(): Promise<T
|
|
138
|
+
public async execute(): Promise<PaginatedResult<T>> {
|
|
84
139
|
const keys = this.parent.getKeys();
|
|
85
140
|
let pkName = keys.primary.name;
|
|
86
141
|
let builtKey = this.parent.buildKey(this.key) as Record<string, any>;
|
|
@@ -88,14 +143,13 @@ export class QueryBuilder<T> {
|
|
|
88
143
|
pkName = this.index.primary.name;
|
|
89
144
|
builtKey = this.parent.buildIndexes(this.key);
|
|
90
145
|
}
|
|
91
|
-
this.expressionAttributeNames['#pk']
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
keyConditionExpression += ` AND ${this.sortKeyCondition}`;
|
|
146
|
+
if (!this.expressionAttributeNames['#pk']) {
|
|
147
|
+
this.expressionAttributeNames['#pk'] = pkName;
|
|
148
|
+
this.expressionAttributeValues[':pk_value'] = builtKey[pkName];
|
|
149
|
+
this.keyConditions.unshift(`#pk = :pk_value`);
|
|
96
150
|
}
|
|
97
151
|
|
|
98
|
-
this.
|
|
152
|
+
const keyConditionExpression = this.keyConditions.join(' AND ');
|
|
99
153
|
|
|
100
154
|
const params: QueryCommandInput = {
|
|
101
155
|
TableName: this.parent.getTableName(),
|
|
@@ -105,14 +159,15 @@ export class QueryBuilder<T> {
|
|
|
105
159
|
ScanIndexForward: this.ascending,
|
|
106
160
|
Limit: this.limit,
|
|
107
161
|
ExclusiveStartKey: this.lastKey,
|
|
108
|
-
IndexName: this.index?.name ?? undefined
|
|
162
|
+
IndexName: this.index?.name ?? undefined,
|
|
109
163
|
};
|
|
110
164
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
165
|
+
this.filterConditions.push(`#entity = :entity_value`);
|
|
166
|
+
this.expressionAttributeNames['#entity'] = 'entityType';
|
|
167
|
+
this.expressionAttributeValues[':entity_value'] = this.parent.getEntityType();
|
|
168
|
+
params.FilterExpression = this.filterConditions.join(' AND ');
|
|
114
169
|
|
|
115
170
|
const result = await this.parent.getClient().send(new QueryCommand(params));
|
|
116
|
-
return this.parent.getSchema().array().parse(result.Items) as T[];
|
|
171
|
+
return {items: this.parent.getSchema().array().parse(result.Items) as T[], lastKey: result.LastEvaluatedKey ?? undefined};
|
|
117
172
|
}
|
|
118
173
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { ScanCommand, ScanCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
2
2
|
import { BetterDDB } from '../betterddb';
|
|
3
|
+
import { getOperatorExpression, Operator } from '../operator';
|
|
4
|
+
import { PaginatedResult } from '../types/paginated-result';
|
|
3
5
|
|
|
4
6
|
export class ScanBuilder<T> {
|
|
5
7
|
private filters: string[] = [];
|
|
@@ -12,32 +14,35 @@ export class ScanBuilder<T> {
|
|
|
12
14
|
|
|
13
15
|
public where(
|
|
14
16
|
attribute: keyof T,
|
|
15
|
-
operator:
|
|
17
|
+
operator: Operator,
|
|
16
18
|
values: any | [any, any]
|
|
17
19
|
): this {
|
|
18
20
|
const attrStr = String(attribute);
|
|
19
21
|
const nameKey = `#attr_${attrStr}`;
|
|
20
22
|
this.expressionAttributeNames[nameKey] = attrStr;
|
|
21
23
|
|
|
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') {
|
|
24
|
+
if (operator === 'between') {
|
|
31
25
|
if (!Array.isArray(values) || values.length !== 2) {
|
|
32
|
-
throw new Error(
|
|
26
|
+
throw new Error(
|
|
27
|
+
`For 'between' operator, values must be a tuple of two items`
|
|
28
|
+
);
|
|
33
29
|
}
|
|
34
30
|
const valueKeyStart = `:val_start_${attrStr}`;
|
|
35
31
|
const valueKeyEnd = `:val_end_${attrStr}`;
|
|
36
32
|
this.expressionAttributeValues[valueKeyStart] = values[0];
|
|
37
33
|
this.expressionAttributeValues[valueKeyEnd] = values[1];
|
|
38
|
-
this.filters.push(
|
|
34
|
+
this.filters.push(
|
|
35
|
+
`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`
|
|
36
|
+
);
|
|
37
|
+
} else if (operator === 'begins_with' || operator === 'contains') {
|
|
38
|
+
const valueKey = `:val_${attrStr}`;
|
|
39
|
+
this.expressionAttributeValues[valueKey] = values;
|
|
40
|
+
this.filters.push(`${operator}(${nameKey}, ${valueKey})`);
|
|
39
41
|
} else {
|
|
40
|
-
|
|
42
|
+
const valueKey = `:val_${attrStr}`;
|
|
43
|
+
this.expressionAttributeValues[valueKey] = values;
|
|
44
|
+
const condition = getOperatorExpression(operator, nameKey, valueKey);
|
|
45
|
+
this.filters.push(condition);
|
|
41
46
|
}
|
|
42
47
|
return this;
|
|
43
48
|
}
|
|
@@ -55,7 +60,7 @@ export class ScanBuilder<T> {
|
|
|
55
60
|
/**
|
|
56
61
|
* Executes the scan and returns a Promise that resolves with an array of items.
|
|
57
62
|
*/
|
|
58
|
-
public async execute(): Promise<T
|
|
63
|
+
public async execute(): Promise<PaginatedResult<T>> {
|
|
59
64
|
const params: ScanCommandInput = {
|
|
60
65
|
TableName: this.parent.getTableName(),
|
|
61
66
|
ExpressionAttributeNames: this.expressionAttributeNames,
|
|
@@ -69,6 +74,7 @@ export class ScanBuilder<T> {
|
|
|
69
74
|
}
|
|
70
75
|
|
|
71
76
|
const result = await this.parent.getClient().send(new ScanCommand(params));
|
|
72
|
-
|
|
77
|
+
|
|
78
|
+
return {items: this.parent.getSchema().array().parse(result.Items) as T[], lastKey: result.LastEvaluatedKey ?? undefined};
|
|
73
79
|
}
|
|
74
80
|
}
|
|
@@ -13,14 +13,11 @@ interface UpdateActions<T> {
|
|
|
13
13
|
export class UpdateBuilder<T> {
|
|
14
14
|
private actions: UpdateActions<T> = {};
|
|
15
15
|
private condition?: { expression: string; attributeValues: Record<string, any> };
|
|
16
|
-
private expectedVersion?: number;
|
|
17
16
|
// When using transaction mode, we store extra transaction items.
|
|
18
17
|
private extraTransactItems: TransactWriteItem[] = [];
|
|
19
18
|
|
|
20
19
|
// Reference to the parent BetterDDB instance and key.
|
|
21
|
-
constructor(private parent: BetterDDB<T>, private key: Partial<T
|
|
22
|
-
this.expectedVersion = expectedVersion;
|
|
23
|
-
}
|
|
20
|
+
constructor(private parent: BetterDDB<T>, private key: Partial<T>) {}
|
|
24
21
|
|
|
25
22
|
// Chainable methods:
|
|
26
23
|
public set(attrs: Partial<T>): this {
|
|
@@ -136,29 +133,6 @@ export class UpdateBuilder<T> {
|
|
|
136
133
|
}
|
|
137
134
|
}
|
|
138
135
|
|
|
139
|
-
// Incorporate expectedVersion if provided.
|
|
140
|
-
if (this.expectedVersion !== undefined) {
|
|
141
|
-
ExpressionAttributeNames['#version'] = 'version';
|
|
142
|
-
ExpressionAttributeValues[':expectedVersion'] = this.expectedVersion;
|
|
143
|
-
ExpressionAttributeValues[':newVersion'] = this.expectedVersion + 1;
|
|
144
|
-
|
|
145
|
-
// Append version update in SET clause.
|
|
146
|
-
const versionClause = '#version = :newVersion';
|
|
147
|
-
const setIndex = clauses.findIndex(clause => clause.startsWith('SET '));
|
|
148
|
-
if (setIndex >= 0) {
|
|
149
|
-
clauses[setIndex] += `, ${versionClause}`;
|
|
150
|
-
} else {
|
|
151
|
-
clauses.push(`SET ${versionClause}`);
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
// Ensure condition expression includes version check.
|
|
155
|
-
if (this.condition && this.condition.expression) {
|
|
156
|
-
this.condition.expression += ` AND #version = :expectedVersion`;
|
|
157
|
-
} else {
|
|
158
|
-
this.condition = { expression: '#version = :expectedVersion', attributeValues: {} };
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
136
|
// Merge any provided condition attribute values.
|
|
163
137
|
if (this.condition) {
|
|
164
138
|
Object.assign(ExpressionAttributeValues, this.condition.attributeValues);
|
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
|
+
}
|
package/test/batch-get.test.ts
CHANGED
|
@@ -2,18 +2,38 @@ 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 { KeySchemaElement, AttributeDefinition } from '@aws-sdk/client-dynamodb';
|
|
7
7
|
const TEST_TABLE = "batch-get-test-table";
|
|
8
8
|
const ENDPOINT = 'http://localhost:4566';
|
|
9
9
|
const REGION = 'us-east-1';
|
|
10
|
-
const
|
|
11
|
-
const PRIMARY_KEY = '
|
|
10
|
+
const ENTITY_TYPE = 'USER';
|
|
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,
|
|
@@ -24,8 +44,6 @@ const UserSchema = z.object({
|
|
|
24
44
|
id: z.string(),
|
|
25
45
|
name: z.string(),
|
|
26
46
|
email: z.string().email(),
|
|
27
|
-
createdAt: z.string(),
|
|
28
|
-
updatedAt: z.string(),
|
|
29
47
|
});
|
|
30
48
|
|
|
31
49
|
type User = z.infer<typeof UserSchema>;
|
|
@@ -33,17 +51,17 @@ type User = z.infer<typeof UserSchema>;
|
|
|
33
51
|
const userDdb = new BetterDDB({
|
|
34
52
|
schema: UserSchema,
|
|
35
53
|
tableName: TEST_TABLE,
|
|
36
|
-
|
|
54
|
+
entityType: ENTITY_TYPE,
|
|
37
55
|
keys: {
|
|
38
56
|
primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
|
|
39
57
|
sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
|
|
40
58
|
},
|
|
41
59
|
client,
|
|
42
|
-
|
|
60
|
+
timestamps: true,
|
|
43
61
|
});
|
|
44
62
|
|
|
45
63
|
beforeAll(async () => {
|
|
46
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
64
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
47
65
|
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
48
66
|
await userDdb.create({ id: 'user-124', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
49
67
|
await userDdb.create({ id: 'user-125', name: 'Bob Doe', email: 'bob@example.com' } as any).execute();
|
|
@@ -57,7 +75,7 @@ describe('BetterDDB - Get Operation', () => {
|
|
|
57
75
|
it('should retrieve an item using GetBuilder', async () => {
|
|
58
76
|
const users = await userDdb.batchGet([{ id: 'user-123', email: 'john@example.com' }, { id: 'user-124', email: 'john@example.com' }]).execute();
|
|
59
77
|
expect(users.length).toEqual(2);
|
|
60
|
-
expect(users
|
|
61
|
-
expect(users
|
|
78
|
+
expect(users.some(user => user.id === 'user-123')).toBe(true);
|
|
79
|
+
expect(users.some(user => user.id === 'user-124')).toBe(true);
|
|
62
80
|
});
|
|
63
81
|
});
|
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
|
-
const
|
|
12
|
-
const PRIMARY_KEY = '
|
|
11
|
+
const ENTITY_TYPE = 'USER';
|
|
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,
|
|
@@ -24,8 +44,6 @@ const UserSchema = z.object({
|
|
|
24
44
|
id: z.string(),
|
|
25
45
|
name: z.string(),
|
|
26
46
|
email: z.string().email(),
|
|
27
|
-
createdAt: z.string(),
|
|
28
|
-
updatedAt: z.string(),
|
|
29
47
|
});
|
|
30
48
|
|
|
31
49
|
type User = z.infer<typeof UserSchema>;
|
|
@@ -33,18 +51,18 @@ type User = z.infer<typeof UserSchema>;
|
|
|
33
51
|
const userDdb = new BetterDDB({
|
|
34
52
|
schema: UserSchema,
|
|
35
53
|
tableName: TEST_TABLE,
|
|
36
|
-
|
|
54
|
+
entityType: ENTITY_TYPE,
|
|
37
55
|
keys: {
|
|
38
56
|
primary: { name: "pk", definition: { build: (raw) => `USER#${raw.id}` } },
|
|
39
57
|
sort: { name: "sk", definition: { build: (raw) => `EMAIL#${raw.email}` } },
|
|
40
58
|
gsis: { gsi1: { name: 'gsi1', primary: { name: "gsi1pk", definition: { build: (raw) => "NAME" } }, sort: { name: "gsi1sk", definition: { build: (raw) => `NAME#${raw.name}` } } } },
|
|
41
59
|
},
|
|
42
60
|
client,
|
|
43
|
-
|
|
61
|
+
timestamps: true,
|
|
44
62
|
});
|
|
45
63
|
|
|
46
64
|
beforeAll(async () => {
|
|
47
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
65
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
48
66
|
});
|
|
49
67
|
|
|
50
68
|
afterAll(async () => {
|
|
@@ -57,7 +75,7 @@ describe('BetterDDB - Create Operation', () => {
|
|
|
57
75
|
|
|
58
76
|
await userDdb.create(user as any).execute();
|
|
59
77
|
|
|
60
|
-
const result = await client.send(new GetCommand({ TableName: TEST_TABLE, Key: {
|
|
78
|
+
const result = await client.send(new GetCommand({ TableName: TEST_TABLE, Key: { pk: 'USER#user-123', sk: 'EMAIL#john@example.com' } }));
|
|
61
79
|
|
|
62
80
|
expect(result).not.toBeNull();
|
|
63
81
|
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
|
-
const
|
|
12
|
-
const PRIMARY_KEY = '
|
|
11
|
+
const ENTITY_TYPE = 'USER';
|
|
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,
|
|
@@ -24,8 +44,6 @@ const UserSchema = z.object({
|
|
|
24
44
|
id: z.string(),
|
|
25
45
|
name: z.string(),
|
|
26
46
|
email: z.string().email(),
|
|
27
|
-
createdAt: z.string(),
|
|
28
|
-
updatedAt: z.string(),
|
|
29
47
|
});
|
|
30
48
|
|
|
31
49
|
type User = z.infer<typeof UserSchema>;
|
|
@@ -33,17 +51,17 @@ type User = z.infer<typeof UserSchema>;
|
|
|
33
51
|
const userDdb = new BetterDDB({
|
|
34
52
|
schema: UserSchema,
|
|
35
53
|
tableName: TEST_TABLE,
|
|
36
|
-
|
|
54
|
+
entityType: ENTITY_TYPE,
|
|
37
55
|
keys: {
|
|
38
56
|
primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
|
|
39
57
|
sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
|
|
40
58
|
},
|
|
41
59
|
client,
|
|
42
|
-
|
|
60
|
+
timestamps: true,
|
|
43
61
|
});
|
|
44
62
|
|
|
45
63
|
beforeAll(async () => {
|
|
46
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
64
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
47
65
|
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
48
66
|
});
|
|
49
67
|
|
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
|
-
const
|
|
12
|
-
const PRIMARY_KEY = '
|
|
11
|
+
const ENTITY_TYPE = 'USER';
|
|
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,
|
|
@@ -25,8 +46,6 @@ const UserSchema = z.object({
|
|
|
25
46
|
id: z.string(),
|
|
26
47
|
name: z.string(),
|
|
27
48
|
email: z.string().email(),
|
|
28
|
-
createdAt: z.string(),
|
|
29
|
-
updatedAt: z.string(),
|
|
30
49
|
});
|
|
31
50
|
|
|
32
51
|
type User = z.infer<typeof UserSchema>;
|
|
@@ -34,17 +53,17 @@ type User = z.infer<typeof UserSchema>;
|
|
|
34
53
|
const userDdb = new BetterDDB({
|
|
35
54
|
schema: UserSchema,
|
|
36
55
|
tableName: TEST_TABLE,
|
|
37
|
-
|
|
56
|
+
entityType: ENTITY_TYPE,
|
|
38
57
|
keys: {
|
|
39
58
|
primary: { name: PRIMARY_KEY, definition: { build: (raw) => raw.id! } },
|
|
40
59
|
sort: { name: SORT_KEY, definition: { build: (raw) => raw.email! } },
|
|
41
60
|
},
|
|
42
61
|
client,
|
|
43
|
-
|
|
62
|
+
timestamps: true,
|
|
44
63
|
});
|
|
45
64
|
|
|
46
65
|
beforeAll(async () => {
|
|
47
|
-
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS);
|
|
66
|
+
await createTestTable(TEST_TABLE, KEY_SCHEMA, ATTRIBUTE_DEFINITIONS, GSIS);
|
|
48
67
|
await userDdb.create({ id: 'user-123', name: 'John Doe', email: 'john@example.com' } as any).execute();
|
|
49
68
|
});
|
|
50
69
|
|