@ftschopp/dynatable-core 1.0.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/CHANGELOG.md +62 -0
- package/README.md +17 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts +18 -0
- package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -0
- package/dist/builders/batch-get/create-batch-get-builder.js +71 -0
- package/dist/builders/batch-get/index.d.ts +3 -0
- package/dist/builders/batch-get/index.d.ts.map +1 -0
- package/dist/builders/batch-get/index.js +18 -0
- package/dist/builders/batch-get/types.d.ts +25 -0
- package/dist/builders/batch-get/types.d.ts.map +1 -0
- package/dist/builders/batch-get/types.js +2 -0
- package/dist/builders/batch-write/create-batch-write-builder.d.ts +12 -0
- package/dist/builders/batch-write/create-batch-write-builder.d.ts.map +1 -0
- package/dist/builders/batch-write/create-batch-write-builder.js +35 -0
- package/dist/builders/batch-write/index.d.ts +3 -0
- package/dist/builders/batch-write/index.d.ts.map +1 -0
- package/dist/builders/batch-write/index.js +18 -0
- package/dist/builders/batch-write/types.d.ts +30 -0
- package/dist/builders/batch-write/types.d.ts.map +1 -0
- package/dist/builders/batch-write/types.js +2 -0
- package/dist/builders/delete/create-delete-builder.d.ts +9 -0
- package/dist/builders/delete/create-delete-builder.d.ts.map +1 -0
- package/dist/builders/delete/create-delete-builder.js +72 -0
- package/dist/builders/delete/index.d.ts +3 -0
- package/dist/builders/delete/index.d.ts.map +1 -0
- package/dist/builders/delete/index.js +18 -0
- package/dist/builders/delete/types.d.ts +11 -0
- package/dist/builders/delete/types.d.ts.map +1 -0
- package/dist/builders/delete/types.js +2 -0
- package/dist/builders/get/create-get-builder.d.ts +12 -0
- package/dist/builders/get/create-get-builder.d.ts.map +1 -0
- package/dist/builders/get/create-get-builder.js +95 -0
- package/dist/builders/get/index.d.ts +3 -0
- package/dist/builders/get/index.d.ts.map +1 -0
- package/dist/builders/get/index.js +18 -0
- package/dist/builders/get/types.d.ts +28 -0
- package/dist/builders/get/types.d.ts.map +1 -0
- package/dist/builders/get/types.js +2 -0
- package/dist/builders/index.d.ts +12 -0
- package/dist/builders/index.d.ts.map +1 -0
- package/dist/builders/index.js +29 -0
- package/dist/builders/put/create-put-builder.d.ts +9 -0
- package/dist/builders/put/create-put-builder.d.ts.map +1 -0
- package/dist/builders/put/create-put-builder.js +104 -0
- package/dist/builders/put/index.d.ts +3 -0
- package/dist/builders/put/index.d.ts.map +1 -0
- package/dist/builders/put/index.js +18 -0
- package/dist/builders/put/types.d.ts +17 -0
- package/dist/builders/put/types.d.ts.map +1 -0
- package/dist/builders/put/types.js +2 -0
- package/dist/builders/query/create-query-builder.d.ts +9 -0
- package/dist/builders/query/create-query-builder.d.ts.map +1 -0
- package/dist/builders/query/create-query-builder.js +283 -0
- package/dist/builders/query/index.d.ts +3 -0
- package/dist/builders/query/index.d.ts.map +1 -0
- package/dist/builders/query/index.js +18 -0
- package/dist/builders/query/types.d.ts +79 -0
- package/dist/builders/query/types.d.ts.map +1 -0
- package/dist/builders/query/types.js +2 -0
- package/dist/builders/scan/create-scan-builder.d.ts +12 -0
- package/dist/builders/scan/create-scan-builder.d.ts.map +1 -0
- package/dist/builders/scan/create-scan-builder.js +103 -0
- package/dist/builders/scan/index.d.ts +3 -0
- package/dist/builders/scan/index.d.ts.map +1 -0
- package/dist/builders/scan/index.js +18 -0
- package/dist/builders/scan/types.d.ts +43 -0
- package/dist/builders/scan/types.d.ts.map +1 -0
- package/dist/builders/scan/types.js +2 -0
- package/dist/builders/shared/conditions.d.ts +18 -0
- package/dist/builders/shared/conditions.d.ts.map +1 -0
- package/dist/builders/shared/conditions.js +48 -0
- package/dist/builders/shared/index.d.ts +4 -0
- package/dist/builders/shared/index.d.ts.map +1 -0
- package/dist/builders/shared/index.js +19 -0
- package/dist/builders/shared/operators.d.ts +12 -0
- package/dist/builders/shared/operators.d.ts.map +1 -0
- package/dist/builders/shared/operators.js +197 -0
- package/dist/builders/shared/types.d.ts +77 -0
- package/dist/builders/shared/types.d.ts.map +1 -0
- package/dist/builders/shared/types.js +6 -0
- package/dist/builders/transact-get/create-transact-get-builder.d.ts +18 -0
- package/dist/builders/transact-get/create-transact-get-builder.d.ts.map +1 -0
- package/dist/builders/transact-get/create-transact-get-builder.js +60 -0
- package/dist/builders/transact-get/index.d.ts +3 -0
- package/dist/builders/transact-get/index.d.ts.map +1 -0
- package/dist/builders/transact-get/index.js +18 -0
- package/dist/builders/transact-get/types.d.ts +28 -0
- package/dist/builders/transact-get/types.d.ts.map +1 -0
- package/dist/builders/transact-get/types.js +2 -0
- package/dist/builders/transact-write/create-transact-write-builder.d.ts +18 -0
- package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -0
- package/dist/builders/transact-write/create-transact-write-builder.js +97 -0
- package/dist/builders/transact-write/index.d.ts +3 -0
- package/dist/builders/transact-write/index.d.ts.map +1 -0
- package/dist/builders/transact-write/index.js +18 -0
- package/dist/builders/transact-write/types.d.ts +34 -0
- package/dist/builders/transact-write/types.d.ts.map +1 -0
- package/dist/builders/transact-write/types.js +2 -0
- package/dist/builders/update/create-update-builder.d.ts +14 -0
- package/dist/builders/update/create-update-builder.d.ts.map +1 -0
- package/dist/builders/update/create-update-builder.js +180 -0
- package/dist/builders/update/index.d.ts +3 -0
- package/dist/builders/update/index.d.ts.map +1 -0
- package/dist/builders/update/index.js +18 -0
- package/dist/builders/update/types.d.ts +35 -0
- package/dist/builders/update/types.d.ts.map +1 -0
- package/dist/builders/update/types.js +2 -0
- package/dist/core/types.d.ts +224 -0
- package/dist/core/types.d.ts.map +1 -0
- package/dist/core/types.js +3 -0
- package/dist/entity.d.ts +73 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +161 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +20 -0
- package/dist/table.d.ts +70 -0
- package/dist/table.d.ts.map +1 -0
- package/dist/table.js +69 -0
- package/dist/utils/dynamodb-logger.d.ts +118 -0
- package/dist/utils/dynamodb-logger.d.ts.map +1 -0
- package/dist/utils/dynamodb-logger.js +125 -0
- package/dist/utils/model-utils.d.ts +24 -0
- package/dist/utils/model-utils.d.ts.map +1 -0
- package/dist/utils/model-utils.js +88 -0
- package/dist/utils/zod-utils.d.ts +13 -0
- package/dist/utils/zod-utils.d.ts.map +1 -0
- package/dist/utils/zod-utils.js +35 -0
- package/eslint.config.mjs +4 -0
- package/jest.config.js +11 -0
- package/package.json +36 -0
- package/src/builders/README.md +272 -0
- package/src/builders/batch-get/README.md +98 -0
- package/src/builders/batch-get/create-batch-get-builder.test.ts +165 -0
- package/src/builders/batch-get/create-batch-get-builder.ts +106 -0
- package/src/builders/batch-get/index.ts +2 -0
- package/src/builders/batch-get/types.ts +29 -0
- package/src/builders/batch-write/README.md +204 -0
- package/src/builders/batch-write/create-batch-write-builder.test.ts +173 -0
- package/src/builders/batch-write/create-batch-write-builder.ts +49 -0
- package/src/builders/batch-write/index.ts +2 -0
- package/src/builders/batch-write/types.ts +33 -0
- package/src/builders/delete/create-delete-builder.test.ts +294 -0
- package/src/builders/delete/create-delete-builder.ts +100 -0
- package/src/builders/delete/index.ts +2 -0
- package/src/builders/delete/types.ts +11 -0
- package/src/builders/get/create-get-builder.test.ts +272 -0
- package/src/builders/get/create-get-builder.ts +140 -0
- package/src/builders/get/index.ts +2 -0
- package/src/builders/get/types.ts +30 -0
- package/src/builders/index.ts +14 -0
- package/src/builders/put/create-put-builder.test.ts +213 -0
- package/src/builders/put/create-put-builder.ts +151 -0
- package/src/builders/put/index.ts +2 -0
- package/src/builders/put/types.ts +18 -0
- package/src/builders/query/create-query-builder.test.ts +230 -0
- package/src/builders/query/create-query-builder.ts +353 -0
- package/src/builders/query/index.ts +2 -0
- package/src/builders/query/types.ts +95 -0
- package/src/builders/scan/create-scan-builder.test.ts +260 -0
- package/src/builders/scan/create-scan-builder.ts +217 -0
- package/src/builders/scan/index.ts +2 -0
- package/src/builders/scan/types.ts +49 -0
- package/src/builders/shared/conditions.ts +58 -0
- package/src/builders/shared/index.ts +3 -0
- package/src/builders/shared/operators.test.ts +270 -0
- package/src/builders/shared/operators.ts +200 -0
- package/src/builders/shared/types.ts +100 -0
- package/src/builders/transact-get/README.md +167 -0
- package/src/builders/transact-get/create-transact-get-builder.test.ts +239 -0
- package/src/builders/transact-get/create-transact-get-builder.ts +67 -0
- package/src/builders/transact-get/index.ts +2 -0
- package/src/builders/transact-get/types.ts +31 -0
- package/src/builders/transact-write/README.md +166 -0
- package/src/builders/transact-write/create-transact-write-builder.test.ts +288 -0
- package/src/builders/transact-write/create-transact-write-builder.ts +118 -0
- package/src/builders/transact-write/index.ts +2 -0
- package/src/builders/transact-write/types.ts +33 -0
- package/src/builders/update/create-update-builder.test.ts +333 -0
- package/src/builders/update/create-update-builder.ts +284 -0
- package/src/builders/update/index.ts +2 -0
- package/src/builders/update/types.ts +42 -0
- package/src/core/types.test.ts +506 -0
- package/src/core/types.ts +290 -0
- package/src/entity.ts +337 -0
- package/src/index.ts +22 -0
- package/src/table.ts +109 -0
- package/src/utils/dynamodb-logger.test.ts +246 -0
- package/src/utils/dynamodb-logger.ts +175 -0
- package/src/utils/model-utils.test.ts +232 -0
- package/src/utils/model-utils.ts +101 -0
- package/src/utils/zod-utils.test.ts +272 -0
- package/src/utils/zod-utils.ts +36 -0
- package/tests/integration/instagram-clone.integration.test.ts +966 -0
- package/tests/integration/pagination-timestamps.integration.test.ts +375 -0
- package/tests/integration/transactions.integration.test.ts +529 -0
- package/tsconfig.json +12 -0
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import { GetCommand, GetCommandInput, GetCommandOutput } from '@aws-sdk/lib-dynamodb';
|
|
4
|
+
import { GetBuilder } from './types';
|
|
5
|
+
import { DynamoDBLogger } from '../../utils/dynamodb-logger';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Builds a ProjectionExpression with proper ExpressionAttributeNames
|
|
9
|
+
* to handle reserved words and special characters.
|
|
10
|
+
*/
|
|
11
|
+
function buildProjectionExpression(attrs: string[]): {
|
|
12
|
+
ProjectionExpression: string;
|
|
13
|
+
ExpressionAttributeNames: Record<string, string>;
|
|
14
|
+
} {
|
|
15
|
+
const names: Record<string, string> = {};
|
|
16
|
+
const projectionParts: string[] = [];
|
|
17
|
+
|
|
18
|
+
attrs.forEach((attr) => {
|
|
19
|
+
// Use a placeholder for the attribute name to avoid reserved word conflicts
|
|
20
|
+
const placeholder = `#${attr}`;
|
|
21
|
+
names[placeholder] = attr;
|
|
22
|
+
projectionParts.push(placeholder);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
return {
|
|
26
|
+
ProjectionExpression: projectionParts.join(', '),
|
|
27
|
+
ExpressionAttributeNames: names,
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Creates a GetBuilder to retrieve an item by its key.
|
|
33
|
+
*/
|
|
34
|
+
export function createGetBuilder<KeyInput, Model>(
|
|
35
|
+
tableName: string,
|
|
36
|
+
key: Record<string, any>,
|
|
37
|
+
client: DynamoDBClient,
|
|
38
|
+
options?: {
|
|
39
|
+
projection?: (keyof Model)[];
|
|
40
|
+
consistentRead?: boolean;
|
|
41
|
+
returnConsumedCapacity?: 'INDEXES' | 'TOTAL' | 'NONE';
|
|
42
|
+
},
|
|
43
|
+
logger?: DynamoDBLogger
|
|
44
|
+
): GetBuilder<KeyInput, Model> {
|
|
45
|
+
const projection = options?.projection ?? [];
|
|
46
|
+
const isConsistent = options?.consistentRead ?? false;
|
|
47
|
+
const consumedCapacity = options?.returnConsumedCapacity;
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
/**
|
|
51
|
+
* Select only specific attributes to retrieve.
|
|
52
|
+
*/
|
|
53
|
+
select(attrs) {
|
|
54
|
+
return createGetBuilder(
|
|
55
|
+
tableName,
|
|
56
|
+
key,
|
|
57
|
+
client,
|
|
58
|
+
{
|
|
59
|
+
projection: attrs,
|
|
60
|
+
consistentRead: isConsistent,
|
|
61
|
+
returnConsumedCapacity: consumedCapacity,
|
|
62
|
+
},
|
|
63
|
+
logger
|
|
64
|
+
);
|
|
65
|
+
},
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Enable strongly consistent read.
|
|
69
|
+
*/
|
|
70
|
+
consistentRead() {
|
|
71
|
+
return createGetBuilder(
|
|
72
|
+
tableName,
|
|
73
|
+
key,
|
|
74
|
+
client,
|
|
75
|
+
{
|
|
76
|
+
projection,
|
|
77
|
+
consistentRead: true,
|
|
78
|
+
returnConsumedCapacity: consumedCapacity,
|
|
79
|
+
},
|
|
80
|
+
logger
|
|
81
|
+
);
|
|
82
|
+
},
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Configure ReturnConsumedCapacity parameter.
|
|
86
|
+
*/
|
|
87
|
+
returnConsumedCapacity(mode) {
|
|
88
|
+
return createGetBuilder(
|
|
89
|
+
tableName,
|
|
90
|
+
key,
|
|
91
|
+
client,
|
|
92
|
+
{
|
|
93
|
+
projection,
|
|
94
|
+
consistentRead: isConsistent,
|
|
95
|
+
returnConsumedCapacity: mode,
|
|
96
|
+
},
|
|
97
|
+
logger
|
|
98
|
+
);
|
|
99
|
+
},
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build the underlying DynamoDB input parameters.
|
|
103
|
+
*/
|
|
104
|
+
dbParams(): GetCommandInput {
|
|
105
|
+
const params: GetCommandInput = {
|
|
106
|
+
TableName: tableName,
|
|
107
|
+
Key: key,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
// Add projection with proper ExpressionAttributeNames
|
|
111
|
+
if (projection.length > 0) {
|
|
112
|
+
const projectionExpr = buildProjectionExpression(projection as string[]);
|
|
113
|
+
params.ProjectionExpression = projectionExpr.ProjectionExpression;
|
|
114
|
+
params.ExpressionAttributeNames = projectionExpr.ExpressionAttributeNames;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Add consistent read if enabled
|
|
118
|
+
if (isConsistent) {
|
|
119
|
+
params.ConsistentRead = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Add ReturnConsumedCapacity if specified
|
|
123
|
+
if (consumedCapacity) {
|
|
124
|
+
params.ReturnConsumedCapacity = consumedCapacity;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
return params;
|
|
128
|
+
},
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Execute the GetItem command and return the item if found.
|
|
132
|
+
*/
|
|
133
|
+
async execute(): Promise<Model | undefined> {
|
|
134
|
+
const params = this.dbParams();
|
|
135
|
+
const result: GetCommandOutput = await client.send(new GetCommand(params));
|
|
136
|
+
logger?.log('GetCommand', params, result);
|
|
137
|
+
return result.Item as unknown as Model | undefined;
|
|
138
|
+
},
|
|
139
|
+
};
|
|
140
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { ExecutableBuilder } from '../shared';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builder for DynamoDB GetItem operations.
|
|
5
|
+
* Supports projection, consistent read, and inspection of the final parameters.
|
|
6
|
+
*/
|
|
7
|
+
export interface GetBuilder<KeyInput, Model> extends ExecutableBuilder<Model | undefined> {
|
|
8
|
+
/**
|
|
9
|
+
* Adds a projection expression to only return specific attributes.
|
|
10
|
+
* Returns a new immutable builder.
|
|
11
|
+
*/
|
|
12
|
+
select(attrs: (keyof Model)[]): GetBuilder<KeyInput, Model>;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Enables strongly consistent read (eventual consistency is default).
|
|
16
|
+
* Returns a new immutable builder.
|
|
17
|
+
*/
|
|
18
|
+
consistentRead(): GetBuilder<KeyInput, Model>;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Configures the ReturnConsumedCapacity parameter.
|
|
22
|
+
* Returns a new immutable builder.
|
|
23
|
+
*
|
|
24
|
+
* @param mode - 'INDEXES' | 'TOTAL' | 'NONE'
|
|
25
|
+
* - INDEXES: Returns consumed capacity for table and indexes
|
|
26
|
+
* - TOTAL: Returns total consumed capacity
|
|
27
|
+
* - NONE: No consumed capacity data returned (default)
|
|
28
|
+
*/
|
|
29
|
+
returnConsumedCapacity(mode: 'INDEXES' | 'TOTAL' | 'NONE'): GetBuilder<KeyInput, Model>;
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
// Shared utilities and types
|
|
2
|
+
export * from './shared';
|
|
3
|
+
|
|
4
|
+
// Operation builders
|
|
5
|
+
export * from './get';
|
|
6
|
+
export * from './put';
|
|
7
|
+
export * from './query';
|
|
8
|
+
export * from './scan';
|
|
9
|
+
export * from './update';
|
|
10
|
+
export * from './delete';
|
|
11
|
+
export * from './batch-get';
|
|
12
|
+
export * from './batch-write';
|
|
13
|
+
export * from './transact-write';
|
|
14
|
+
export * from './transact-get';
|
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
2
|
+
import { mockClient } from 'aws-sdk-client-mock';
|
|
3
|
+
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
4
|
+
import { createPutBuilder } from './create-put-builder';
|
|
5
|
+
|
|
6
|
+
const ddbMock = mockClient(DynamoDBClient);
|
|
7
|
+
|
|
8
|
+
describe('createPutBuilder', () => {
|
|
9
|
+
const tableName = 'test-table';
|
|
10
|
+
const client = new DynamoDBClient({});
|
|
11
|
+
|
|
12
|
+
beforeEach(() => {
|
|
13
|
+
ddbMock.reset();
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
describe('Basic functionality', () => {
|
|
17
|
+
it('should create a put builder with minimal config', () => {
|
|
18
|
+
const item = { id: '123', name: 'Test' };
|
|
19
|
+
const builder = createPutBuilder(tableName, item, client);
|
|
20
|
+
|
|
21
|
+
const params = builder.dbParams();
|
|
22
|
+
|
|
23
|
+
expect(params).toEqual({
|
|
24
|
+
TableName: tableName,
|
|
25
|
+
Item: item,
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
it('should build params with conditions', () => {
|
|
30
|
+
const item = { id: '123', name: 'Test' };
|
|
31
|
+
const builder = createPutBuilder(tableName, item, client).where((attrs, ops) =>
|
|
32
|
+
ops.eq(attrs.id, '123')
|
|
33
|
+
);
|
|
34
|
+
|
|
35
|
+
const params = builder.dbParams();
|
|
36
|
+
|
|
37
|
+
expect(params.ConditionExpression).toBe('#id = :id_0');
|
|
38
|
+
expect(params.ExpressionAttributeNames).toEqual({ '#id': 'id' });
|
|
39
|
+
expect(params.ExpressionAttributeValues).toEqual({ ':id_0': '123' });
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('Timestamps support', () => {
|
|
44
|
+
it('should add createdAt and updatedAt when enableTimestamps is true', () => {
|
|
45
|
+
const item = { id: '123', name: 'Test' };
|
|
46
|
+
const builder = createPutBuilder(tableName, item, client, [], false, 'NONE', true);
|
|
47
|
+
|
|
48
|
+
const params = builder.dbParams();
|
|
49
|
+
|
|
50
|
+
expect(params.Item).toHaveProperty('createdAt');
|
|
51
|
+
expect(params.Item).toHaveProperty('updatedAt');
|
|
52
|
+
expect(typeof params.Item.createdAt).toBe('string');
|
|
53
|
+
expect(typeof params.Item.updatedAt).toBe('string');
|
|
54
|
+
expect(params.Item.createdAt).toBe(params.Item.updatedAt);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('should not add timestamps when enableTimestamps is false', () => {
|
|
58
|
+
const item = { id: '123', name: 'Test' };
|
|
59
|
+
const builder = createPutBuilder(tableName, item, client, [], false, 'NONE', false);
|
|
60
|
+
|
|
61
|
+
const params = builder.dbParams();
|
|
62
|
+
|
|
63
|
+
expect(params.Item).not.toHaveProperty('createdAt');
|
|
64
|
+
expect(params.Item).not.toHaveProperty('updatedAt');
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should preserve original item data with timestamps', () => {
|
|
68
|
+
const item = { id: '123', name: 'Test', value: 42 };
|
|
69
|
+
const builder = createPutBuilder(tableName, item, client, [], false, 'NONE', true);
|
|
70
|
+
|
|
71
|
+
const params = builder.dbParams();
|
|
72
|
+
|
|
73
|
+
expect(params.Item.id).toBe('123');
|
|
74
|
+
expect(params.Item.name).toBe('Test');
|
|
75
|
+
expect(params.Item.value).toBe(42);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
describe('ifNotExists functionality', () => {
|
|
80
|
+
it('should add attribute_not_exists conditions for PK and SK', () => {
|
|
81
|
+
const item = { PK: 'user#123', SK: 'profile', name: 'Test' };
|
|
82
|
+
const builder = createPutBuilder(tableName, item, client).ifNotExists();
|
|
83
|
+
|
|
84
|
+
const params = builder.dbParams();
|
|
85
|
+
|
|
86
|
+
expect(params.ConditionExpression).toContain('attribute_not_exists(#PK)');
|
|
87
|
+
expect(params.ConditionExpression).toContain('attribute_not_exists(#SK)');
|
|
88
|
+
expect(params.ExpressionAttributeNames).toHaveProperty('#PK');
|
|
89
|
+
expect(params.ExpressionAttributeNames).toHaveProperty('#SK');
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
it('should handle lowercase pk and sk', () => {
|
|
93
|
+
const item = { pk: 'user#123', sk: 'profile', name: 'Test' };
|
|
94
|
+
const builder = createPutBuilder(tableName, item, client).ifNotExists();
|
|
95
|
+
|
|
96
|
+
const params = builder.dbParams();
|
|
97
|
+
|
|
98
|
+
expect(params.ConditionExpression).toContain('attribute_not_exists(#pk)');
|
|
99
|
+
expect(params.ConditionExpression).toContain('attribute_not_exists(#sk)');
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it('should combine ifNotExists with other conditions', () => {
|
|
103
|
+
const item = { PK: 'user#123', SK: 'profile', name: 'Test' };
|
|
104
|
+
const builder = createPutBuilder(tableName, item, client)
|
|
105
|
+
.where((attrs, ops) => ops.eq(attrs.name, 'Test'))
|
|
106
|
+
.ifNotExists();
|
|
107
|
+
|
|
108
|
+
const params = builder.dbParams();
|
|
109
|
+
|
|
110
|
+
expect(params.ConditionExpression).toContain('attribute_not_exists(#PK)');
|
|
111
|
+
expect(params.ConditionExpression).toContain('#name = :name_0');
|
|
112
|
+
});
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
describe('returning functionality', () => {
|
|
116
|
+
it('should set ReturnValues to NONE by default', () => {
|
|
117
|
+
const item = { id: '123', name: 'Test' };
|
|
118
|
+
const builder = createPutBuilder(tableName, item, client);
|
|
119
|
+
|
|
120
|
+
const params = builder.dbParams();
|
|
121
|
+
|
|
122
|
+
expect(params.ReturnValues).toBeUndefined();
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('should set ReturnValues to ALL_OLD when specified', () => {
|
|
126
|
+
const item = { id: '123', name: 'Test' };
|
|
127
|
+
const builder = createPutBuilder(tableName, item, client).returning('ALL_OLD');
|
|
128
|
+
|
|
129
|
+
const params = builder.dbParams();
|
|
130
|
+
|
|
131
|
+
expect(params.ReturnValues).toBe('ALL_OLD');
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('execute functionality', () => {
|
|
136
|
+
it('should return the new item when returnMode is NONE', async () => {
|
|
137
|
+
const item = { id: '123', name: 'Test' };
|
|
138
|
+
|
|
139
|
+
ddbMock.on(PutCommand).resolves({});
|
|
140
|
+
|
|
141
|
+
const builder = createPutBuilder(tableName, item, client);
|
|
142
|
+
const result = await builder.execute();
|
|
143
|
+
|
|
144
|
+
expect(result).toEqual(item);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it('should return the old item when returnMode is ALL_OLD and item was replaced', async () => {
|
|
148
|
+
const item = { id: '123', name: 'New Name' };
|
|
149
|
+
const oldItem = { id: '123', name: 'Old Name' };
|
|
150
|
+
|
|
151
|
+
ddbMock.on(PutCommand).resolves({
|
|
152
|
+
Attributes: oldItem,
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const builder = createPutBuilder(tableName, item, client).returning('ALL_OLD');
|
|
156
|
+
const result = await builder.execute();
|
|
157
|
+
|
|
158
|
+
expect(result).toEqual(oldItem);
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it('should return the new item when returnMode is ALL_OLD but no old item existed', async () => {
|
|
162
|
+
const item = { id: '123', name: 'Test' };
|
|
163
|
+
|
|
164
|
+
ddbMock.on(PutCommand).resolves({});
|
|
165
|
+
|
|
166
|
+
const builder = createPutBuilder(tableName, item, client).returning('ALL_OLD');
|
|
167
|
+
const result = await builder.execute();
|
|
168
|
+
|
|
169
|
+
expect(result).toEqual(item);
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
it('should return item with timestamps when timestamps are enabled', async () => {
|
|
173
|
+
const item = { id: '123', name: 'Test' };
|
|
174
|
+
|
|
175
|
+
ddbMock.on(PutCommand).resolves({});
|
|
176
|
+
|
|
177
|
+
const builder = createPutBuilder(tableName, item, client, [], false, 'NONE', true);
|
|
178
|
+
const result = await builder.execute();
|
|
179
|
+
|
|
180
|
+
expect(result).toHaveProperty('createdAt');
|
|
181
|
+
expect(result).toHaveProperty('updatedAt');
|
|
182
|
+
expect(result.id).toBe('123');
|
|
183
|
+
expect(result.name).toBe('Test');
|
|
184
|
+
});
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
describe('Method chaining', () => {
|
|
188
|
+
it('should allow chaining multiple methods', () => {
|
|
189
|
+
const item = { PK: 'user#123', SK: 'profile', name: 'Test' };
|
|
190
|
+
const builder = createPutBuilder(tableName, item, client)
|
|
191
|
+
.where((attrs, ops) => ops.eq(attrs.name, 'Test'))
|
|
192
|
+
.ifNotExists()
|
|
193
|
+
.returning('ALL_OLD');
|
|
194
|
+
|
|
195
|
+
const params = builder.dbParams();
|
|
196
|
+
|
|
197
|
+
expect(params.ConditionExpression).toBeTruthy();
|
|
198
|
+
expect(params.ReturnValues).toBe('ALL_OLD');
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
it('should preserve timestamps through chaining', () => {
|
|
202
|
+
const item = { id: '123', name: 'Test' };
|
|
203
|
+
const builder = createPutBuilder(tableName, item, client, [], false, 'NONE', true)
|
|
204
|
+
.where((attrs, ops) => ops.eq(attrs.id, '123'))
|
|
205
|
+
.returning('ALL_OLD');
|
|
206
|
+
|
|
207
|
+
const params = builder.dbParams();
|
|
208
|
+
|
|
209
|
+
expect(params.Item).toHaveProperty('createdAt');
|
|
210
|
+
expect(params.Item).toHaveProperty('updatedAt');
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
});
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
|
+
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import { PutCommand } from '@aws-sdk/lib-dynamodb';
|
|
4
|
+
import { buildExpression, AttrBuilder, Condition, createOpBuilder } from '../shared';
|
|
5
|
+
import { PutBuilder } from './types';
|
|
6
|
+
import { DynamoDBLogger } from '../../utils/dynamodb-logger';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Utility to infer if attribute exists in the item and returns condition.
|
|
10
|
+
*/
|
|
11
|
+
function buildNotExistsConditions(item: any): Condition[] {
|
|
12
|
+
// Check for common key names (both uppercase and lowercase)
|
|
13
|
+
const possibleKeys = ['PK', 'SK', 'pk', 'sk'];
|
|
14
|
+
const foundKeys = possibleKeys.filter((key) => key in item);
|
|
15
|
+
|
|
16
|
+
return foundKeys.map((key) => ({
|
|
17
|
+
expression: `attribute_not_exists(#${key})`,
|
|
18
|
+
names: { [`#${key}`]: key },
|
|
19
|
+
}));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Creates a PutBuilder for an item and table.
|
|
24
|
+
*/
|
|
25
|
+
export function createPutBuilder<Model>(
|
|
26
|
+
tableName: string,
|
|
27
|
+
item: Model,
|
|
28
|
+
client: DynamoDBClient,
|
|
29
|
+
prevConditions: Condition[] = [],
|
|
30
|
+
shouldCheckNotExists = false,
|
|
31
|
+
returnMode: 'NONE' | 'ALL_OLD' = 'NONE',
|
|
32
|
+
enableTimestamps = false,
|
|
33
|
+
logger?: DynamoDBLogger
|
|
34
|
+
): PutBuilder<Model> {
|
|
35
|
+
const conditions = [...prevConditions];
|
|
36
|
+
|
|
37
|
+
const build = (): PutBuilder<Model> => ({
|
|
38
|
+
where(fn) {
|
|
39
|
+
const attrs = new Proxy({} as AttrBuilder<Model>, {
|
|
40
|
+
get(_, prop: string) {
|
|
41
|
+
return { name: prop };
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
// Create a scoped opBuilder for this operation to avoid global state
|
|
45
|
+
const opBuilder = createOpBuilder();
|
|
46
|
+
const condition = fn(attrs, opBuilder);
|
|
47
|
+
return createPutBuilder(
|
|
48
|
+
tableName,
|
|
49
|
+
item,
|
|
50
|
+
client,
|
|
51
|
+
[...conditions, condition],
|
|
52
|
+
shouldCheckNotExists,
|
|
53
|
+
returnMode,
|
|
54
|
+
enableTimestamps,
|
|
55
|
+
logger
|
|
56
|
+
);
|
|
57
|
+
},
|
|
58
|
+
|
|
59
|
+
ifNotExists() {
|
|
60
|
+
return createPutBuilder(tableName, item, client, conditions, true, returnMode, enableTimestamps, logger);
|
|
61
|
+
},
|
|
62
|
+
|
|
63
|
+
returning(mode) {
|
|
64
|
+
return createPutBuilder(
|
|
65
|
+
tableName,
|
|
66
|
+
item,
|
|
67
|
+
client,
|
|
68
|
+
conditions,
|
|
69
|
+
shouldCheckNotExists,
|
|
70
|
+
mode,
|
|
71
|
+
enableTimestamps,
|
|
72
|
+
logger
|
|
73
|
+
);
|
|
74
|
+
},
|
|
75
|
+
|
|
76
|
+
dbParams() {
|
|
77
|
+
let finalConditions = [...conditions];
|
|
78
|
+
|
|
79
|
+
if (shouldCheckNotExists) {
|
|
80
|
+
finalConditions = [...finalConditions, ...buildNotExistsConditions(item)];
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Add timestamps if enabled
|
|
84
|
+
let finalItem = item;
|
|
85
|
+
if (enableTimestamps) {
|
|
86
|
+
const now = new Date().toISOString();
|
|
87
|
+
finalItem = {
|
|
88
|
+
...item,
|
|
89
|
+
createdAt: now,
|
|
90
|
+
updatedAt: now,
|
|
91
|
+
} as Model;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Build ConditionExpression from condition tree
|
|
95
|
+
let conditionExpression = '';
|
|
96
|
+
let expressionAttributeNames = {};
|
|
97
|
+
let expressionAttributeValues = {};
|
|
98
|
+
|
|
99
|
+
if (finalConditions.length > 0) {
|
|
100
|
+
// If there's only one condition, use it directly
|
|
101
|
+
// Otherwise, combine with AND
|
|
102
|
+
const combinedCondition =
|
|
103
|
+
finalConditions.length === 1 && finalConditions[0]
|
|
104
|
+
? finalConditions[0]
|
|
105
|
+
: {
|
|
106
|
+
expression: '',
|
|
107
|
+
operator: 'AND' as const,
|
|
108
|
+
children: finalConditions,
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const result = buildExpression(combinedCondition);
|
|
112
|
+
conditionExpression = result.expression;
|
|
113
|
+
expressionAttributeNames = result.names;
|
|
114
|
+
expressionAttributeValues = result.values;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const extra: any = returnMode !== 'NONE' ? { ReturnValues: returnMode } : {};
|
|
118
|
+
|
|
119
|
+
return {
|
|
120
|
+
TableName: tableName,
|
|
121
|
+
Item: finalItem,
|
|
122
|
+
...(conditionExpression && {
|
|
123
|
+
ConditionExpression: conditionExpression,
|
|
124
|
+
}),
|
|
125
|
+
...(Object.keys(expressionAttributeNames).length && {
|
|
126
|
+
ExpressionAttributeNames: expressionAttributeNames,
|
|
127
|
+
}),
|
|
128
|
+
...(Object.keys(expressionAttributeValues).length && {
|
|
129
|
+
ExpressionAttributeValues: expressionAttributeValues,
|
|
130
|
+
}),
|
|
131
|
+
...extra,
|
|
132
|
+
};
|
|
133
|
+
},
|
|
134
|
+
|
|
135
|
+
async execute() {
|
|
136
|
+
const params = build().dbParams();
|
|
137
|
+
const response = await client.send(new PutCommand(params));
|
|
138
|
+
logger?.log('PutCommand', params, response);
|
|
139
|
+
|
|
140
|
+
// Return the old item if returnMode is ALL_OLD and it was replaced
|
|
141
|
+
if (returnMode === 'ALL_OLD' && response.Attributes) {
|
|
142
|
+
return response.Attributes as Model;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// For NONE mode or when no old item existed, return the new item with timestamps
|
|
146
|
+
return params.Item as Model;
|
|
147
|
+
},
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
return build();
|
|
151
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { OperationBuilder } from '../shared';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builder interface for DynamoDB PutItem operations
|
|
5
|
+
*/
|
|
6
|
+
export interface PutBuilder<Model> extends OperationBuilder<Model> {
|
|
7
|
+
/**
|
|
8
|
+
* Adds a condition that the item must not exist (checks pk and sk)
|
|
9
|
+
*/
|
|
10
|
+
ifNotExists(): PutBuilder<Model>;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Configures what values should be returned after the put operation.
|
|
14
|
+
* - NONE: Nothing is returned (default)
|
|
15
|
+
* - ALL_OLD: Returns the item as it was before being replaced (if it existed)
|
|
16
|
+
*/
|
|
17
|
+
returning(mode: 'NONE' | 'ALL_OLD'): PutBuilder<Model>;
|
|
18
|
+
}
|