betterddb 0.4.8 → 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.
@@ -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 filters;
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(attribute: keyof T, operator: 'eq' | 'begins_with' | 'between', values: any | [any, any]): this;
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.filters = [];
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(attribute, operator, values) {
33
- const attrStr = String(attribute);
34
- const nameKey = `#attr_${attrStr}`;
35
- this.expressionAttributeNames[nameKey] = attrStr;
36
- if (operator === 'eq') {
37
- const valueKey = `:val_${attrStr}`;
38
- this.expressionAttributeValues[valueKey] = values;
39
- this.filters.push(`${nameKey} = ${valueKey}`);
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 = `:val_${attrStr}`;
43
- this.expressionAttributeValues[valueKey] = values;
44
- this.filters.push(`begins_with(${nameKey}, ${valueKey})`);
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 if (operator === 'between') {
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(`For 'between' operator, values must be a tuple of two items`);
88
+ throw new Error("For 'between' operator, values must be a tuple of two items");
49
89
  }
50
- const valueKeyStart = `:val_start_${attrStr}`;
51
- const valueKeyEnd = `:val_end_${attrStr}`;
52
- this.expressionAttributeValues[valueKeyStart] = values[0];
53
- this.expressionAttributeValues[valueKeyEnd] = values[1];
54
- this.filters.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
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
- throw new Error(`Unsupported operator: ${operator}`);
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'] = pkName;
82
- let keyConditionExpression = `#pk = :pk_value`;
83
- if (this.sortKeyCondition) {
84
- keyConditionExpression += ` AND ${this.sortKeyCondition}`;
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.expressionAttributeValues[':pk_value'] = builtKey[pkName];
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.filters.length > 0) {
98
- params.FilterExpression = this.filters.join(' AND ');
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: 'eq' | 'begins_with' | 'between', values: any | [any, any]): this;
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 === 'eq') {
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
- throw new Error(`Unsupported operator: ${operator}`);
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
  }
@@ -0,0 +1,2 @@
1
+ export type Operator = '==' | '!=' | '<' | '<=' | '>' | '>=' | 'begins_with' | 'between' | 'contains';
2
+ export declare function getOperatorExpression(operator: Operator, nameKey: string, valueKey: string, secondValueKey?: string): string;
@@ -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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "betterddb",
3
- "version": "0.4.8",
3
+ "version": "0.4.9",
4
4
  "description": "A definition-based DynamoDB wrapper library that provides a schema-driven and fully typesafe DAL.",
5
5
  "main": "lib/index.js",
6
6
  "types": "lib/index.d.ts",
@@ -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 filters: string[] = [];
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: 'eq' | 'begins_with' | 'between',
93
+ operator: Operator,
41
94
  values: any | [any, any]
42
95
  ): this {
43
96
  const attrStr = String(attribute);
44
- const nameKey = `#attr_${attrStr}`;
45
- this.expressionAttributeNames[nameKey] = attrStr;
46
-
47
- if (operator === 'eq') {
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(`For 'between' operator, values must be a tuple of two items`);
102
+ throw new Error("For 'between' operator, values must be a tuple of two items");
58
103
  }
59
- const valueKeyStart = `:val_start_${attrStr}`;
60
- const valueKeyEnd = `:val_end_${attrStr}`;
61
- this.expressionAttributeValues[valueKeyStart] = values[0];
62
- this.expressionAttributeValues[valueKeyEnd] = values[1];
63
- this.filters.push(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
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
- throw new Error(`Unsupported operator: ${operator}`);
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'] = pkName;
92
-
93
- let keyConditionExpression = `#pk = :pk_value`;
94
- if (this.sortKeyCondition) {
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.expressionAttributeValues[':pk_value'] = builtKey[pkName];
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.filters.length > 0) {
112
- params.FilterExpression = this.filters.join(' AND ');
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: 'eq' | 'begins_with' | 'between',
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 === 'eq') {
23
- const valueKey = `:val_${attrStr}`;
24
- this.expressionAttributeValues[valueKey] = values;
25
- this.filters.push(`${nameKey} = ${valueKey}`);
26
- } else if (operator === 'begins_with') {
27
- const valueKey = `:val_${attrStr}`;
28
- this.expressionAttributeValues[valueKey] = values;
29
- this.filters.push(`begins_with(${nameKey}, ${valueKey})`);
30
- } else if (operator === 'between') {
22
+ if (operator === 'between') {
31
23
  if (!Array.isArray(values) || values.length !== 2) {
32
- throw new Error(`For 'between' operator, values must be a tuple of two items`);
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(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
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
- throw new Error(`Unsupported operator: ${operator}`);
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
  }
@@ -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
+ }
@@ -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
10
  const ENTITY_NAME = 'USER';
11
- const PRIMARY_KEY = 'id';
11
+ const PRIMARY_KEY = 'pk';
12
12
  const PRIMARY_KEY_TYPE = 'S';
13
- const SORT_KEY = 'email';
13
+ const SORT_KEY = 'sk';
14
14
  const SORT_KEY_TYPE = 'S';
15
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
16
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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,
@@ -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
  await userDdb.create({ id: 'user-124', name: 'John Doe', email: 'john@example.com' } as any).execute();
49
69
  await userDdb.create({ id: 'user-125', name: 'Bob Doe', email: 'bob@example.com' } as any).execute();
@@ -57,7 +77,7 @@ describe('BetterDDB - Get Operation', () => {
57
77
  it('should retrieve an item using GetBuilder', async () => {
58
78
  const users = await userDdb.batchGet([{ id: 'user-123', email: 'john@example.com' }, { id: 'user-124', email: 'john@example.com' }]).execute();
59
79
  expect(users.length).toEqual(2);
60
- expect(users[0].id).toBe('user-123');
61
- expect(users[1].id).toBe('user-124');
80
+ expect(users.some(user => user.id === 'user-123')).toBe(true);
81
+ expect(users.some(user => user.id === 'user-124')).toBe(true);
62
82
  });
63
83
  });
@@ -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 = 'id';
12
+ const PRIMARY_KEY = 'pk';
13
13
  const PRIMARY_KEY_TYPE = 'S';
14
- const SORT_KEY = 'email';
14
+ const SORT_KEY = 'sk';
15
15
  const SORT_KEY_TYPE = 'S';
16
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
17
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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: { id: 'user-123', email: 'john@example.com' } }));
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();
@@ -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 = 'id';
12
+ const PRIMARY_KEY = 'pk';
13
13
  const PRIMARY_KEY_TYPE = 'S';
14
- const SORT_KEY = 'email';
14
+ const SORT_KEY = 'sk';
15
15
  const SORT_KEY_TYPE = 'S';
16
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
17
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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 = 'id';
12
+ const PRIMARY_KEY = 'pk';
13
13
  const PRIMARY_KEY_TYPE = 'S';
14
- const SORT_KEY = 'email';
14
+ const SORT_KEY = 'sk';
15
15
  const SORT_KEY_TYPE = 'S';
16
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
17
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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
 
@@ -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 = 'id';
11
+ const PRIMARY_KEY = 'pk';
12
12
  const PRIMARY_KEY_TYPE = 'S';
13
- const SORT_KEY = 'email';
13
+ const SORT_KEY = 'sk';
14
14
  const SORT_KEY_TYPE = 'S';
15
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
16
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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: { name: PRIMARY_KEY, definition: PRIMARY_KEY },
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: { name: 'email', definition: 'email' }
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
- for (const item of items) {
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
- .where('name', 'begins_with', 'Alice')
70
- .limitResults(5).execute();
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 = 'id';
11
+ const PRIMARY_KEY = 'pk';
12
12
  const PRIMARY_KEY_TYPE = 'S';
13
- const SORT_KEY = 'email';
13
+ const SORT_KEY = 'sk';
14
14
  const SORT_KEY_TYPE = 'S';
15
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
16
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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: PRIMARY_KEY },
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-4', name: 'Charlie', email: 'charlie@example.com' },
49
- { id: 'user-5', name: 'Dave', email: 'dave@example.com' }
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
- for (const item of items) {
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', 'char')
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(/^char/i);
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
  });
@@ -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 = 'id';
11
+ const PRIMARY_KEY = 'pk';
12
12
  const PRIMARY_KEY_TYPE = 'S';
13
- const SORT_KEY = 'email';
13
+ const SORT_KEY = 'sk';
14
14
  const SORT_KEY_TYPE = 'S';
15
- const KEY_SCHEMA = [{ AttributeName: PRIMARY_KEY, KeyType: 'HASH' }, { AttributeName: SORT_KEY, KeyType: 'RANGE' }] as KeySchemaElement[];
16
- const ATTRIBUTE_DEFINITIONS = [{ AttributeName: PRIMARY_KEY, AttributeType: PRIMARY_KEY_TYPE }, { AttributeName: SORT_KEY, AttributeType: SORT_KEY_TYPE }] as AttributeDefinition[];
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') {