@ftschopp/dynatable-core 1.0.0 → 1.2.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 +19 -0
- package/README.md +528 -2
- package/dist/builders/delete/types.d.ts +6 -1
- package/dist/builders/delete/types.d.ts.map +1 -1
- package/dist/builders/get/types.d.ts +6 -1
- package/dist/builders/get/types.d.ts.map +1 -1
- package/dist/builders/put/types.d.ts +6 -1
- package/dist/builders/put/types.d.ts.map +1 -1
- package/dist/builders/query/create-query-builder.d.ts +1 -1
- package/dist/builders/query/create-query-builder.d.ts.map +1 -1
- package/dist/builders/query/types.d.ts +9 -10
- package/dist/builders/query/types.d.ts.map +1 -1
- package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -1
- package/dist/builders/transact-write/create-transact-write-builder.js +1 -1
- package/dist/builders/transact-write/types.d.ts +33 -6
- package/dist/builders/transact-write/types.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.d.ts.map +1 -1
- package/dist/builders/update/create-update-builder.js +30 -9
- package/dist/builders/update/types.d.ts +8 -2
- package/dist/builders/update/types.d.ts.map +1 -1
- package/dist/core/types.d.ts +96 -11
- package/dist/core/types.d.ts.map +1 -1
- package/dist/entity/create-entity-api.d.ts +15 -0
- package/dist/entity/create-entity-api.d.ts.map +1 -0
- package/dist/entity/create-entity-api.js +124 -0
- package/dist/entity/index.d.ts +12 -0
- package/dist/entity/index.d.ts.map +1 -0
- package/dist/entity/index.js +18 -0
- package/dist/entity/middleware/factories.d.ts +6 -0
- package/dist/entity/middleware/factories.d.ts.map +1 -0
- package/dist/entity/middleware/factories.js +15 -0
- package/dist/entity/middleware/types.d.ts +8 -0
- package/dist/entity/middleware/types.d.ts.map +1 -0
- package/dist/entity/middleware/types.js +2 -0
- package/dist/entity/middleware/with-middleware.d.ts +8 -0
- package/dist/entity/middleware/with-middleware.d.ts.map +1 -0
- package/dist/entity/middleware/with-middleware.js +29 -0
- package/dist/{entity.d.ts → entity/types.d.ts} +6 -17
- package/dist/entity/types.d.ts.map +1 -0
- package/dist/entity/types.js +2 -0
- package/dist/entity/validation/key-validation.d.ts +7 -0
- package/dist/entity/validation/key-validation.d.ts.map +1 -0
- package/dist/entity/validation/key-validation.js +25 -0
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/table.d.ts +8 -1
- package/dist/table.d.ts.map +1 -1
- package/dist/table.js +1 -0
- package/dist/utils/model-utils.d.ts +7 -1
- package/dist/utils/model-utils.d.ts.map +1 -1
- package/dist/utils/model-utils.js +32 -3
- package/dist/utils/zod-utils.d.ts +3 -2
- package/dist/utils/zod-utils.d.ts.map +1 -1
- package/dist/utils/zod-utils.js +33 -11
- package/package.json +3 -2
- package/src/builders/README.md +68 -1
- package/src/builders/delete/types.ts +7 -1
- package/src/builders/get/types.ts +7 -1
- package/src/builders/put/types.ts +7 -1
- package/src/builders/query/create-query-builder.ts +4 -6
- package/src/builders/query/types.ts +9 -12
- package/src/builders/transact-write/README.md +37 -1
- package/src/builders/transact-write/create-transact-write-builder.ts +20 -14
- package/src/builders/transact-write/types.ts +44 -6
- package/src/builders/update/create-update-builder.test.ts +43 -0
- package/src/builders/update/create-update-builder.ts +47 -9
- package/src/builders/update/types.ts +9 -2
- package/src/core/types.test.ts +137 -0
- package/src/core/types.ts +97 -20
- package/src/entity/create-entity-api.ts +212 -0
- package/src/entity/index.ts +19 -0
- package/src/entity/middleware/factories.ts +15 -0
- package/src/entity/middleware/types.ts +7 -0
- package/src/entity/middleware/with-middleware.ts +37 -0
- package/src/entity/types.ts +79 -0
- package/src/entity/validation/key-validation.ts +34 -0
- package/src/index.ts +1 -0
- package/src/table.ts +10 -3
- package/src/utils/model-utils.test.ts +131 -1
- package/src/utils/model-utils.ts +35 -2
- package/src/utils/zod-utils.test.ts +97 -2
- package/src/utils/zod-utils.ts +31 -12
- package/dist/entity.d.ts.map +0 -1
- package/dist/entity.js +0 -161
- package/src/entity.ts +0 -337
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { ExecutableBuilder, AttrBuilder, OpBuilder, Condition } from '../shared';
|
|
3
|
-
import { ModelDefinition } from '../../core/types';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Query result with pagination support
|
|
@@ -30,38 +29,36 @@ export interface QueryResult<Model> {
|
|
|
30
29
|
/**
|
|
31
30
|
* Query executor with additional query-specific options
|
|
32
31
|
*/
|
|
33
|
-
export interface QueryExecutor<Model
|
|
34
|
-
Model[]
|
|
35
|
-
> {
|
|
32
|
+
export interface QueryExecutor<Model> extends ExecutableBuilder<Model[]> {
|
|
36
33
|
/**
|
|
37
34
|
* Limit the number of items to return
|
|
38
35
|
*/
|
|
39
|
-
limit(count: number): QueryExecutor<Model
|
|
36
|
+
limit(count: number): QueryExecutor<Model>;
|
|
40
37
|
|
|
41
38
|
/**
|
|
42
39
|
* Scan index forward (ascending) or backward (descending)
|
|
43
40
|
*/
|
|
44
|
-
scanIndexForward(forward: boolean): QueryExecutor<Model
|
|
41
|
+
scanIndexForward(forward: boolean): QueryExecutor<Model>;
|
|
45
42
|
|
|
46
43
|
/**
|
|
47
44
|
* Use a secondary index
|
|
48
45
|
*/
|
|
49
|
-
useIndex(indexName: string): QueryExecutor<Model
|
|
46
|
+
useIndex(indexName: string): QueryExecutor<Model>;
|
|
50
47
|
|
|
51
48
|
/**
|
|
52
49
|
* Select specific attributes to return
|
|
53
50
|
*/
|
|
54
|
-
select(attrs: (keyof Model)[]): QueryExecutor<Model
|
|
51
|
+
select(attrs: (keyof Model)[]): QueryExecutor<Model>;
|
|
55
52
|
|
|
56
53
|
/**
|
|
57
54
|
* Use consistent read
|
|
58
55
|
*/
|
|
59
|
-
consistentRead(): QueryExecutor<Model
|
|
56
|
+
consistentRead(): QueryExecutor<Model>;
|
|
60
57
|
|
|
61
58
|
/**
|
|
62
59
|
* Start query from a specific key (for pagination)
|
|
63
60
|
*/
|
|
64
|
-
startFrom(key: Record<string, any>): QueryExecutor<Model
|
|
61
|
+
startFrom(key: Record<string, any>): QueryExecutor<Model>;
|
|
65
62
|
|
|
66
63
|
/**
|
|
67
64
|
* Returns the raw DynamoDB query parameters
|
|
@@ -82,7 +79,7 @@ export interface QueryExecutor<Model, M extends ModelDefinition = any> extends E
|
|
|
82
79
|
/**
|
|
83
80
|
* Main Query Builder interface with type-safe where clause
|
|
84
81
|
*/
|
|
85
|
-
export interface QueryBuilder<Model
|
|
82
|
+
export interface QueryBuilder<Model> {
|
|
86
83
|
/**
|
|
87
84
|
* Build a condition expression using attributes and operators
|
|
88
85
|
* Usage: .where((attr, op) => op.eq(attr.username, 'juanca'))
|
|
@@ -91,5 +88,5 @@ export interface QueryBuilder<Model, M extends ModelDefinition = any> {
|
|
|
91
88
|
* op.gt(attr.age, 18)
|
|
92
89
|
* ))
|
|
93
90
|
*/
|
|
94
|
-
where(fn: (attr: AttrBuilder<Model>, op: OpBuilder) => Condition): QueryExecutor<Model
|
|
91
|
+
where(fn: (attr: AttrBuilder<Model>, op: OpBuilder) => Condition): QueryExecutor<Model>;
|
|
95
92
|
}
|
|
@@ -5,9 +5,10 @@ TransactWrite provides atomic multi-item write operations in DynamoDB. All opera
|
|
|
5
5
|
## Features
|
|
6
6
|
|
|
7
7
|
- **Functional API**: Immutable builders using pure functions
|
|
8
|
-
- **Type-safe**: Full TypeScript support
|
|
8
|
+
- **Type-safe**: Full TypeScript support with AWS SDK types
|
|
9
9
|
- **Composable**: Chain operations fluently
|
|
10
10
|
- **Idempotent**: Support for client request tokens
|
|
11
|
+
- **Flexible**: Accepts both entity builder outputs and raw DynamoDB parameters
|
|
11
12
|
|
|
12
13
|
## Usage
|
|
13
14
|
|
|
@@ -144,6 +145,41 @@ Use client request tokens for idempotent operations:
|
|
|
144
145
|
await table.transactWrite().addPut(params).withClientRequestToken('unique-id-12345').execute();
|
|
145
146
|
```
|
|
146
147
|
|
|
148
|
+
## Type Safety
|
|
149
|
+
|
|
150
|
+
All operations are fully typed using AWS SDK types:
|
|
151
|
+
|
|
152
|
+
```typescript
|
|
153
|
+
import type {
|
|
154
|
+
TransactPutParams,
|
|
155
|
+
TransactUpdateParams,
|
|
156
|
+
TransactDeleteParams,
|
|
157
|
+
TransactConditionCheckParams,
|
|
158
|
+
} from '@ftschopp/dynatable-core';
|
|
159
|
+
|
|
160
|
+
// addPut accepts PutCommandInput from @aws-sdk/lib-dynamodb
|
|
161
|
+
const putParams: TransactPutParams = {
|
|
162
|
+
TableName: 'MyTable',
|
|
163
|
+
Item: { pk: 'USER#123', sk: 'USER#123', name: 'John' },
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
// TypeScript validates all parameters at compile time
|
|
167
|
+
table.transactWrite().addPut(putParams).execute();
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Using Entity Builders
|
|
171
|
+
|
|
172
|
+
Entity builders automatically provide correctly typed parameters via `.dbParams()`:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
// The entity builder's dbParams() returns PutCommandInput
|
|
176
|
+
const params = table.entities.User.put({ username: 'john' }).ifNotExists().dbParams();
|
|
177
|
+
// params is typed as PutCommandInput
|
|
178
|
+
|
|
179
|
+
// Pass it to the transaction builder
|
|
180
|
+
table.transactWrite().addPut(params).execute();
|
|
181
|
+
```
|
|
182
|
+
|
|
147
183
|
## Functional Design
|
|
148
184
|
|
|
149
185
|
The builder is immutable - each method returns a new instance:
|
|
@@ -1,7 +1,13 @@
|
|
|
1
|
-
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
1
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
2
|
import { TransactWriteCommand } from '@aws-sdk/lib-dynamodb';
|
|
4
|
-
import {
|
|
3
|
+
import {
|
|
4
|
+
TransactWriteBuilder,
|
|
5
|
+
TransactWriteState,
|
|
6
|
+
TransactPutParams,
|
|
7
|
+
TransactUpdateParams,
|
|
8
|
+
TransactDeleteParams,
|
|
9
|
+
TransactConditionCheckParams,
|
|
10
|
+
} from './types';
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
13
|
* Creates the initial state for a TransactWrite builder
|
|
@@ -16,7 +22,7 @@ const createInitialState = (client: DynamoDBClient): TransactWriteState => ({
|
|
|
16
22
|
*/
|
|
17
23
|
const addPutItem =
|
|
18
24
|
(state: TransactWriteState) =>
|
|
19
|
-
(params:
|
|
25
|
+
(params: TransactPutParams): TransactWriteState => ({
|
|
20
26
|
...state,
|
|
21
27
|
items: [...state.items, { Put: params }],
|
|
22
28
|
});
|
|
@@ -26,7 +32,7 @@ const addPutItem =
|
|
|
26
32
|
*/
|
|
27
33
|
const addUpdateItem =
|
|
28
34
|
(state: TransactWriteState) =>
|
|
29
|
-
(params:
|
|
35
|
+
(params: TransactUpdateParams): TransactWriteState => ({
|
|
30
36
|
...state,
|
|
31
37
|
items: [...state.items, { Update: params }],
|
|
32
38
|
});
|
|
@@ -36,7 +42,7 @@ const addUpdateItem =
|
|
|
36
42
|
*/
|
|
37
43
|
const addDeleteItem =
|
|
38
44
|
(state: TransactWriteState) =>
|
|
39
|
-
(params:
|
|
45
|
+
(params: TransactDeleteParams): TransactWriteState => ({
|
|
40
46
|
...state,
|
|
41
47
|
items: [...state.items, { Delete: params }],
|
|
42
48
|
});
|
|
@@ -46,7 +52,7 @@ const addDeleteItem =
|
|
|
46
52
|
*/
|
|
47
53
|
const addConditionCheckItem =
|
|
48
54
|
(state: TransactWriteState) =>
|
|
49
|
-
(params:
|
|
55
|
+
(params: TransactConditionCheckParams): TransactWriteState => ({
|
|
50
56
|
...state,
|
|
51
57
|
items: [...state.items, { ConditionCheck: params }],
|
|
52
58
|
});
|
|
@@ -64,9 +70,9 @@ const setClientRequestToken =
|
|
|
64
70
|
/**
|
|
65
71
|
* Converts the builder state to DynamoDB parameters
|
|
66
72
|
*/
|
|
67
|
-
const toDbParams = (state: TransactWriteState) => {
|
|
68
|
-
const params:
|
|
69
|
-
TransactItems: state.items,
|
|
73
|
+
const toDbParams = (state: TransactWriteState): ReturnType<TransactWriteBuilder['dbParams']> => {
|
|
74
|
+
const params: ReturnType<TransactWriteBuilder['dbParams']> = {
|
|
75
|
+
TransactItems: [...state.items],
|
|
70
76
|
};
|
|
71
77
|
|
|
72
78
|
if (state.clientRequestToken) {
|
|
@@ -89,11 +95,11 @@ const execute = async (state: TransactWriteState) => {
|
|
|
89
95
|
* Creates a builder from the current state
|
|
90
96
|
*/
|
|
91
97
|
const createBuilder = (state: TransactWriteState): TransactWriteBuilder => ({
|
|
92
|
-
addPut: (params
|
|
93
|
-
addUpdate: (params
|
|
94
|
-
addDelete: (params
|
|
95
|
-
addConditionCheck: (params
|
|
96
|
-
withClientRequestToken: (token
|
|
98
|
+
addPut: (params) => createBuilder(addPutItem(state)(params)),
|
|
99
|
+
addUpdate: (params) => createBuilder(addUpdateItem(state)(params)),
|
|
100
|
+
addDelete: (params) => createBuilder(addDeleteItem(state)(params)),
|
|
101
|
+
addConditionCheck: (params) => createBuilder(addConditionCheckItem(state)(params)),
|
|
102
|
+
withClientRequestToken: (token) => createBuilder(setClientRequestToken(state)(token)),
|
|
97
103
|
dbParams: () => toDbParams(state),
|
|
98
104
|
execute: () => execute(state),
|
|
99
105
|
});
|
|
@@ -1,8 +1,46 @@
|
|
|
1
1
|
/* eslint-disable @typescript-eslint/no-explicit-any */
|
|
2
2
|
import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
|
|
3
|
+
import type {
|
|
4
|
+
TransactWriteCommandInput,
|
|
5
|
+
TransactWriteCommandOutput,
|
|
6
|
+
PutCommandInput,
|
|
7
|
+
UpdateCommandInput,
|
|
8
|
+
DeleteCommandInput,
|
|
9
|
+
} from '@aws-sdk/lib-dynamodb';
|
|
10
|
+
|
|
11
|
+
// Extract types from AWS SDK's TransactWriteCommandInput
|
|
12
|
+
type TransactWriteItems = NonNullable<TransactWriteCommandInput['TransactItems']>;
|
|
13
|
+
type StrictTransactPut = NonNullable<TransactWriteItems[number]['Put']>;
|
|
14
|
+
type StrictTransactUpdate = NonNullable<TransactWriteItems[number]['Update']>;
|
|
15
|
+
type StrictTransactDelete = NonNullable<TransactWriteItems[number]['Delete']>;
|
|
16
|
+
type StrictTransactConditionCheck = NonNullable<TransactWriteItems[number]['ConditionCheck']>;
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Parameters for a Put operation within a transaction
|
|
20
|
+
* Accepts both regular PutCommandInput and strict transaction Put params
|
|
21
|
+
*/
|
|
22
|
+
export type TransactPutParams = PutCommandInput | StrictTransactPut;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Parameters for an Update operation within a transaction
|
|
26
|
+
* Accepts both regular UpdateCommandInput and strict transaction Update params
|
|
27
|
+
*/
|
|
28
|
+
export type TransactUpdateParams = UpdateCommandInput | StrictTransactUpdate;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parameters for a Delete operation within a transaction
|
|
32
|
+
* Accepts both regular DeleteCommandInput and strict transaction Delete params
|
|
33
|
+
*/
|
|
34
|
+
export type TransactDeleteParams = DeleteCommandInput | StrictTransactDelete;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Parameters for a ConditionCheck operation within a transaction
|
|
38
|
+
*/
|
|
39
|
+
export type TransactConditionCheckParams = StrictTransactConditionCheck;
|
|
3
40
|
|
|
4
41
|
/**
|
|
5
42
|
* Represents a single item in a TransactWrite operation
|
|
43
|
+
* Using any internally to allow flexibility between CommandInput and strict transaction types
|
|
6
44
|
*/
|
|
7
45
|
export type TransactWriteItem =
|
|
8
46
|
| { Put: any }
|
|
@@ -23,11 +61,11 @@ export type TransactWriteState = {
|
|
|
23
61
|
* TransactWrite builder interface
|
|
24
62
|
*/
|
|
25
63
|
export type TransactWriteBuilder = {
|
|
26
|
-
readonly addPut: (params:
|
|
27
|
-
readonly addUpdate: (params:
|
|
28
|
-
readonly addDelete: (params:
|
|
29
|
-
readonly addConditionCheck: (params:
|
|
64
|
+
readonly addPut: (params: TransactPutParams) => TransactWriteBuilder;
|
|
65
|
+
readonly addUpdate: (params: TransactUpdateParams) => TransactWriteBuilder;
|
|
66
|
+
readonly addDelete: (params: TransactDeleteParams) => TransactWriteBuilder;
|
|
67
|
+
readonly addConditionCheck: (params: TransactConditionCheckParams) => TransactWriteBuilder;
|
|
30
68
|
readonly withClientRequestToken: (token: string) => TransactWriteBuilder;
|
|
31
|
-
readonly dbParams: () =>
|
|
32
|
-
readonly execute: () => Promise<
|
|
69
|
+
readonly dbParams: () => TransactWriteCommandInput;
|
|
70
|
+
readonly execute: () => Promise<TransactWriteCommandOutput>;
|
|
33
71
|
};
|
|
@@ -52,6 +52,49 @@ describe('UpdateBuilder', () => {
|
|
|
52
52
|
':score_2': 100,
|
|
53
53
|
});
|
|
54
54
|
});
|
|
55
|
+
|
|
56
|
+
test('should build params with multiple SET operations using object', () => {
|
|
57
|
+
const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
|
|
58
|
+
const params = createUpdateBuilder<TestModel>(tableName, key, client)
|
|
59
|
+
.set({
|
|
60
|
+
name: 'John Doe',
|
|
61
|
+
age: 30,
|
|
62
|
+
score: 100,
|
|
63
|
+
})
|
|
64
|
+
.dbParams();
|
|
65
|
+
|
|
66
|
+
expect(params.UpdateExpression).toBe('SET #name = :name_0, #age = :age_1, #score = :score_2');
|
|
67
|
+
expect(params.ExpressionAttributeNames).toEqual({
|
|
68
|
+
'#name': 'name',
|
|
69
|
+
'#age': 'age',
|
|
70
|
+
'#score': 'score',
|
|
71
|
+
});
|
|
72
|
+
expect(params.ExpressionAttributeValues).toEqual({
|
|
73
|
+
':name_0': 'John Doe',
|
|
74
|
+
':age_1': 30,
|
|
75
|
+
':score_2': 100,
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('should combine single and multiple SET operations', () => {
|
|
80
|
+
const key: Partial<TestModel> = { pk: 'USER#1', sk: 'USER#1' };
|
|
81
|
+
const params = createUpdateBuilder<TestModel>(tableName, key, client)
|
|
82
|
+
.set('name', 'John Doe')
|
|
83
|
+
.set({ age: 30, score: 100 })
|
|
84
|
+
.dbParams();
|
|
85
|
+
|
|
86
|
+
expect(params.UpdateExpression).toBe('SET #name = :name_0, #age = :age_1, #score = :score_2');
|
|
87
|
+
expect(params.ExpressionAttributeNames).toEqual({
|
|
88
|
+
'#name': 'name',
|
|
89
|
+
'#age': 'age',
|
|
90
|
+
'#score': 'score',
|
|
91
|
+
});
|
|
92
|
+
expect(params.ExpressionAttributeValues).toEqual({
|
|
93
|
+
':name_0': 'John Doe',
|
|
94
|
+
':age_1': 30,
|
|
95
|
+
':score_2': 100,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
55
98
|
});
|
|
56
99
|
|
|
57
100
|
describe('REMOVE operations', () => {
|
|
@@ -59,8 +59,49 @@ export function createUpdateBuilder<Model>(
|
|
|
59
59
|
);
|
|
60
60
|
},
|
|
61
61
|
|
|
62
|
-
set(
|
|
63
|
-
|
|
62
|
+
set(attrOrUpdates: keyof Model | AttrRef | Partial<Model>, value?: any) {
|
|
63
|
+
// Check if this is a multiple updates object
|
|
64
|
+
// An AttrRef has only 'name' property and is a string value
|
|
65
|
+
// Multiple updates is an object with no value parameter
|
|
66
|
+
if (
|
|
67
|
+
value === undefined &&
|
|
68
|
+
typeof attrOrUpdates === 'object' &&
|
|
69
|
+
attrOrUpdates !== null &&
|
|
70
|
+
!(
|
|
71
|
+
Object.keys(attrOrUpdates).length === 1 &&
|
|
72
|
+
'name' in attrOrUpdates &&
|
|
73
|
+
typeof (attrOrUpdates as any).name === 'string'
|
|
74
|
+
)
|
|
75
|
+
) {
|
|
76
|
+
// Multiple updates case
|
|
77
|
+
const updates = attrOrUpdates as Partial<Model>;
|
|
78
|
+
const newActions: UpdateAction[] = [];
|
|
79
|
+
|
|
80
|
+
for (const [attr, val] of Object.entries(updates)) {
|
|
81
|
+
const attrName = attr;
|
|
82
|
+
const valueName = getUniqueValueName(attrName);
|
|
83
|
+
newActions.push({
|
|
84
|
+
expression: `#${attrName} = :${valueName}`,
|
|
85
|
+
names: { [`#${attrName}`]: attrName },
|
|
86
|
+
values: { [`:${valueName}`]: val },
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
return createUpdateBuilder(
|
|
91
|
+
tableName,
|
|
92
|
+
key,
|
|
93
|
+
client,
|
|
94
|
+
conditions,
|
|
95
|
+
{ ...updateActions, set: [...updateActions.set, ...newActions] },
|
|
96
|
+
returnMode,
|
|
97
|
+
valueCounter,
|
|
98
|
+
enableTimestamps,
|
|
99
|
+
logger
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Single update case
|
|
104
|
+
const attrName = normalizeAttr(attrOrUpdates as keyof Model | AttrRef);
|
|
64
105
|
const valueName = getUniqueValueName(attrName);
|
|
65
106
|
const action: UpdateAction = {
|
|
66
107
|
expression: `#${attrName} = :${valueName}`,
|
|
@@ -267,16 +308,13 @@ export function createUpdateBuilder<Model>(
|
|
|
267
308
|
const response = await client.send(new UpdateCommand(params));
|
|
268
309
|
logger?.log('UpdateCommand', params, response);
|
|
269
310
|
|
|
270
|
-
// Return the
|
|
271
|
-
if (
|
|
272
|
-
return response.Attributes as Model;
|
|
273
|
-
}
|
|
274
|
-
if (returnMode === 'UPDATED_NEW' && response.Attributes) {
|
|
311
|
+
// Return the item based on returnMode
|
|
312
|
+
if (response.Attributes) {
|
|
275
313
|
return response.Attributes as Model;
|
|
276
314
|
}
|
|
277
315
|
|
|
278
|
-
//
|
|
279
|
-
return
|
|
316
|
+
// If no attributes returned (NONE mode), return undefined
|
|
317
|
+
return undefined as unknown as Model;
|
|
280
318
|
},
|
|
281
319
|
});
|
|
282
320
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { UpdateCommandInput } from '@aws-sdk/lib-dynamodb';
|
|
1
2
|
import { OperationBuilder, AttrRef } from '../shared';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -12,11 +13,12 @@ export type UpdateAction = {
|
|
|
12
13
|
/**
|
|
13
14
|
* Builder interface for DynamoDB UpdateItem operations
|
|
14
15
|
*/
|
|
15
|
-
export interface UpdateBuilder<Model> extends OperationBuilder<Model> {
|
|
16
|
+
export interface UpdateBuilder<Model> extends Omit<OperationBuilder<Model>, 'dbParams'> {
|
|
16
17
|
/**
|
|
17
|
-
* Sets an attribute to a specific value
|
|
18
|
+
* Sets an attribute to a specific value, or sets multiple attributes at once
|
|
18
19
|
*/
|
|
19
20
|
set(attr: keyof Model | AttrRef, value: any): UpdateBuilder<Model>;
|
|
21
|
+
set(updates: Partial<Model>): UpdateBuilder<Model>;
|
|
20
22
|
|
|
21
23
|
/**
|
|
22
24
|
* Removes an attribute from the item
|
|
@@ -39,4 +41,9 @@ export interface UpdateBuilder<Model> extends OperationBuilder<Model> {
|
|
|
39
41
|
returning(
|
|
40
42
|
mode: 'NONE' | 'ALL_OLD' | 'ALL_NEW' | 'UPDATED_OLD' | 'UPDATED_NEW'
|
|
41
43
|
): UpdateBuilder<Model>;
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Converts the builder state to DynamoDB UpdateItem parameters
|
|
47
|
+
*/
|
|
48
|
+
dbParams(): UpdateCommandInput;
|
|
42
49
|
}
|
package/src/core/types.test.ts
CHANGED
|
@@ -504,3 +504,140 @@ describe('Timestamp Type Inference', () => {
|
|
|
504
504
|
expect(timestamps.updatedAt).toBe('2024-01-01T00:00:00.000Z');
|
|
505
505
|
});
|
|
506
506
|
});
|
|
507
|
+
|
|
508
|
+
// ============================================================================
|
|
509
|
+
// Nested Object & Array Type Inference Tests
|
|
510
|
+
// ============================================================================
|
|
511
|
+
|
|
512
|
+
describe('Nested Object and Array Type Inference', () => {
|
|
513
|
+
const TransactionSchema = {
|
|
514
|
+
format: 'dynatable:1.0.0',
|
|
515
|
+
version: '1.0.0',
|
|
516
|
+
indexes: { primary: { hash: 'PK', sort: 'SK' } },
|
|
517
|
+
models: {
|
|
518
|
+
Transaction: {
|
|
519
|
+
key: {
|
|
520
|
+
PK: { type: String, value: 'USER#${userId}' },
|
|
521
|
+
SK: { type: String, value: 'TX#${txId}' },
|
|
522
|
+
},
|
|
523
|
+
attributes: {
|
|
524
|
+
userId: { type: String, required: true },
|
|
525
|
+
txId: { type: String, required: true },
|
|
526
|
+
status: { type: String, required: true },
|
|
527
|
+
// Nested object with schema
|
|
528
|
+
address: {
|
|
529
|
+
type: Object,
|
|
530
|
+
schema: {
|
|
531
|
+
street: { type: String },
|
|
532
|
+
city: { type: String, required: true },
|
|
533
|
+
country: { type: String, required: true },
|
|
534
|
+
},
|
|
535
|
+
},
|
|
536
|
+
// Array of scalar items
|
|
537
|
+
tags: {
|
|
538
|
+
type: Array,
|
|
539
|
+
items: { type: String },
|
|
540
|
+
default: [],
|
|
541
|
+
},
|
|
542
|
+
// Array of objects with schema (like history in the real example)
|
|
543
|
+
history: {
|
|
544
|
+
type: Array,
|
|
545
|
+
default: [],
|
|
546
|
+
items: {
|
|
547
|
+
type: Object,
|
|
548
|
+
schema: {
|
|
549
|
+
date: { type: String },
|
|
550
|
+
description: { type: String },
|
|
551
|
+
status: { type: String, required: true },
|
|
552
|
+
},
|
|
553
|
+
},
|
|
554
|
+
},
|
|
555
|
+
// Deeply nested object
|
|
556
|
+
amount: {
|
|
557
|
+
type: Object,
|
|
558
|
+
schema: {
|
|
559
|
+
value: { type: Number, required: true },
|
|
560
|
+
currency: { type: String, required: true },
|
|
561
|
+
breakdown: {
|
|
562
|
+
type: Object,
|
|
563
|
+
schema: {
|
|
564
|
+
base: { type: Number, required: true },
|
|
565
|
+
fees: { type: Number, required: true },
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
},
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
},
|
|
572
|
+
},
|
|
573
|
+
} as const satisfies SchemaDefinition;
|
|
574
|
+
|
|
575
|
+
type Transaction = InferModelFromSchema<typeof TransactionSchema, 'Transaction'>;
|
|
576
|
+
|
|
577
|
+
test('schema with nested Object attribute compiles', () => {
|
|
578
|
+
expect(TransactionSchema.models.Transaction.attributes.address.type).toBe(Object);
|
|
579
|
+
});
|
|
580
|
+
|
|
581
|
+
test('schema with Array of objects compiles', () => {
|
|
582
|
+
expect(TransactionSchema.models.Transaction.attributes.history.type).toBe(Array);
|
|
583
|
+
});
|
|
584
|
+
|
|
585
|
+
test('inferred type accepts valid nested object', () => {
|
|
586
|
+
const tx: Transaction = {
|
|
587
|
+
userId: 'u1',
|
|
588
|
+
txId: 'tx1',
|
|
589
|
+
status: 'PENDING',
|
|
590
|
+
address: { city: 'Buenos Aires', country: 'AR' },
|
|
591
|
+
tags: ['fast', 'verified'],
|
|
592
|
+
history: [
|
|
593
|
+
{ date: '2024-01-01', description: 'Created', status: 'PENDING' },
|
|
594
|
+
{ status: 'COMPLETED' },
|
|
595
|
+
],
|
|
596
|
+
amount: {
|
|
597
|
+
value: 100,
|
|
598
|
+
currency: 'USD',
|
|
599
|
+
breakdown: { base: 90, fees: 10 },
|
|
600
|
+
},
|
|
601
|
+
};
|
|
602
|
+
|
|
603
|
+
expect(tx.userId).toBe('u1');
|
|
604
|
+
expect(tx.address?.city).toBe('Buenos Aires');
|
|
605
|
+
expect(tx.history?.[0]?.status).toBe('PENDING');
|
|
606
|
+
expect(tx.amount?.breakdown?.base).toBe(90);
|
|
607
|
+
expect(tx.tags?.[0]).toBe('fast');
|
|
608
|
+
});
|
|
609
|
+
|
|
610
|
+
test('inferred nested object type requires its required fields', () => {
|
|
611
|
+
type Address = NonNullable<Transaction['address']>;
|
|
612
|
+
|
|
613
|
+
// city and country are required — this must compile
|
|
614
|
+
const validAddress: Address = { city: 'BA', country: 'AR' };
|
|
615
|
+
expect(validAddress.city).toBe('BA');
|
|
616
|
+
|
|
617
|
+
// Verify the shape: optional `street`, required `city` and `country`
|
|
618
|
+
type AddressKeys = keyof Required<Address>;
|
|
619
|
+
const _keys: AddressKeys[] = ['street', 'city', 'country'];
|
|
620
|
+
expect(_keys).toContain('city');
|
|
621
|
+
});
|
|
622
|
+
|
|
623
|
+
test('model with nested attributes satisfies ModelDefinition', () => {
|
|
624
|
+
const model = {
|
|
625
|
+
key: {
|
|
626
|
+
PK: { type: String, value: 'ITEM#${id}' },
|
|
627
|
+
SK: { type: String, value: 'ITEM#${id}' },
|
|
628
|
+
},
|
|
629
|
+
attributes: {
|
|
630
|
+
id: { type: String, required: true },
|
|
631
|
+
meta: {
|
|
632
|
+
type: Object,
|
|
633
|
+
schema: {
|
|
634
|
+
createdBy: { type: String },
|
|
635
|
+
tags: { type: Array, items: { type: String } },
|
|
636
|
+
},
|
|
637
|
+
},
|
|
638
|
+
},
|
|
639
|
+
} as const satisfies ModelDefinition;
|
|
640
|
+
|
|
641
|
+
expect(model.attributes.meta.type).toBe(Object);
|
|
642
|
+
});
|
|
643
|
+
});
|