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.
@@ -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 filters: string[] = [];
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: 'eq' | 'begins_with' | 'between',
95
+ operator: Operator,
41
96
  values: any | [any, any]
42
97
  ): this {
43
98
  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') {
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(`For 'between' operator, values must be a tuple of two items`);
104
+ throw new Error("For 'between' operator, values must be a tuple of two items");
58
105
  }
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}`);
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
- throw new Error(`Unsupported operator: ${operator}`);
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'] = pkName;
92
-
93
- let keyConditionExpression = `#pk = :pk_value`;
94
- if (this.sortKeyCondition) {
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.expressionAttributeValues[':pk_value'] = builtKey[pkName];
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
- if (this.filters.length > 0) {
112
- params.FilterExpression = this.filters.join(' AND ');
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: 'eq' | 'begins_with' | 'between',
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 === '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') {
24
+ if (operator === 'between') {
31
25
  if (!Array.isArray(values) || values.length !== 2) {
32
- throw new Error(`For 'between' operator, values must be a tuple of two items`);
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(`${nameKey} BETWEEN ${valueKeyStart} AND ${valueKeyEnd}`);
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
- throw new Error(`Unsupported operator: ${operator}`);
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
- return this.parent.getSchema().array().parse(result.Items) as T[];
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>, expectedVersion?: number) {
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);
@@ -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,4 @@
1
+ export type PaginatedResult<T> = {
2
+ items: T[];
3
+ lastKey: Record<string, any> | undefined;
4
+ }
@@ -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 ENTITY_NAME = 'USER';
11
- const PRIMARY_KEY = 'id';
10
+ const ENTITY_TYPE = 'USER';
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,
@@ -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
- entityName: ENTITY_NAME,
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
- autoTimestamps: true,
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[0].id).toBe('user-123');
61
- expect(users[1].id).toBe('user-124');
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
  });
@@ -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 ENTITY_NAME = 'USER';
12
- const PRIMARY_KEY = 'id';
11
+ const ENTITY_TYPE = 'USER';
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,
@@ -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
- entityName: ENTITY_NAME,
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
- autoTimestamps: true,
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: { id: 'user-123', email: 'john@example.com' } }));
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();
@@ -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 ENTITY_NAME = 'USER';
12
- const PRIMARY_KEY = 'id';
11
+ const ENTITY_TYPE = 'USER';
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,
@@ -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
- entityName: ENTITY_NAME,
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
- autoTimestamps: true,
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 ENTITY_NAME = 'USER';
12
- const PRIMARY_KEY = 'id';
11
+ const ENTITY_TYPE = 'USER';
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,
@@ -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
- entityName: ENTITY_NAME,
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
- autoTimestamps: true,
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