@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.
Files changed (85) hide show
  1. package/CHANGELOG.md +19 -0
  2. package/README.md +528 -2
  3. package/dist/builders/delete/types.d.ts +6 -1
  4. package/dist/builders/delete/types.d.ts.map +1 -1
  5. package/dist/builders/get/types.d.ts +6 -1
  6. package/dist/builders/get/types.d.ts.map +1 -1
  7. package/dist/builders/put/types.d.ts +6 -1
  8. package/dist/builders/put/types.d.ts.map +1 -1
  9. package/dist/builders/query/create-query-builder.d.ts +1 -1
  10. package/dist/builders/query/create-query-builder.d.ts.map +1 -1
  11. package/dist/builders/query/types.d.ts +9 -10
  12. package/dist/builders/query/types.d.ts.map +1 -1
  13. package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -1
  14. package/dist/builders/transact-write/create-transact-write-builder.js +1 -1
  15. package/dist/builders/transact-write/types.d.ts +33 -6
  16. package/dist/builders/transact-write/types.d.ts.map +1 -1
  17. package/dist/builders/update/create-update-builder.d.ts.map +1 -1
  18. package/dist/builders/update/create-update-builder.js +30 -9
  19. package/dist/builders/update/types.d.ts +8 -2
  20. package/dist/builders/update/types.d.ts.map +1 -1
  21. package/dist/core/types.d.ts +96 -11
  22. package/dist/core/types.d.ts.map +1 -1
  23. package/dist/entity/create-entity-api.d.ts +15 -0
  24. package/dist/entity/create-entity-api.d.ts.map +1 -0
  25. package/dist/entity/create-entity-api.js +124 -0
  26. package/dist/entity/index.d.ts +12 -0
  27. package/dist/entity/index.d.ts.map +1 -0
  28. package/dist/entity/index.js +18 -0
  29. package/dist/entity/middleware/factories.d.ts +6 -0
  30. package/dist/entity/middleware/factories.d.ts.map +1 -0
  31. package/dist/entity/middleware/factories.js +15 -0
  32. package/dist/entity/middleware/types.d.ts +8 -0
  33. package/dist/entity/middleware/types.d.ts.map +1 -0
  34. package/dist/entity/middleware/types.js +2 -0
  35. package/dist/entity/middleware/with-middleware.d.ts +8 -0
  36. package/dist/entity/middleware/with-middleware.d.ts.map +1 -0
  37. package/dist/entity/middleware/with-middleware.js +29 -0
  38. package/dist/{entity.d.ts → entity/types.d.ts} +6 -17
  39. package/dist/entity/types.d.ts.map +1 -0
  40. package/dist/entity/types.js +2 -0
  41. package/dist/entity/validation/key-validation.d.ts +7 -0
  42. package/dist/entity/validation/key-validation.d.ts.map +1 -0
  43. package/dist/entity/validation/key-validation.js +25 -0
  44. package/dist/index.d.ts +1 -1
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/table.d.ts +8 -1
  47. package/dist/table.d.ts.map +1 -1
  48. package/dist/table.js +1 -0
  49. package/dist/utils/model-utils.d.ts +7 -1
  50. package/dist/utils/model-utils.d.ts.map +1 -1
  51. package/dist/utils/model-utils.js +32 -3
  52. package/dist/utils/zod-utils.d.ts +3 -2
  53. package/dist/utils/zod-utils.d.ts.map +1 -1
  54. package/dist/utils/zod-utils.js +33 -11
  55. package/package.json +3 -2
  56. package/src/builders/README.md +68 -1
  57. package/src/builders/delete/types.ts +7 -1
  58. package/src/builders/get/types.ts +7 -1
  59. package/src/builders/put/types.ts +7 -1
  60. package/src/builders/query/create-query-builder.ts +4 -6
  61. package/src/builders/query/types.ts +9 -12
  62. package/src/builders/transact-write/README.md +37 -1
  63. package/src/builders/transact-write/create-transact-write-builder.ts +20 -14
  64. package/src/builders/transact-write/types.ts +44 -6
  65. package/src/builders/update/create-update-builder.test.ts +43 -0
  66. package/src/builders/update/create-update-builder.ts +47 -9
  67. package/src/builders/update/types.ts +9 -2
  68. package/src/core/types.test.ts +137 -0
  69. package/src/core/types.ts +97 -20
  70. package/src/entity/create-entity-api.ts +212 -0
  71. package/src/entity/index.ts +19 -0
  72. package/src/entity/middleware/factories.ts +15 -0
  73. package/src/entity/middleware/types.ts +7 -0
  74. package/src/entity/middleware/with-middleware.ts +37 -0
  75. package/src/entity/types.ts +79 -0
  76. package/src/entity/validation/key-validation.ts +34 -0
  77. package/src/index.ts +1 -0
  78. package/src/table.ts +10 -3
  79. package/src/utils/model-utils.test.ts +131 -1
  80. package/src/utils/model-utils.ts +35 -2
  81. package/src/utils/zod-utils.test.ts +97 -2
  82. package/src/utils/zod-utils.ts +31 -12
  83. package/dist/entity.d.ts.map +0 -1
  84. package/dist/entity.js +0 -161
  85. 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, M extends ModelDefinition = any> extends ExecutableBuilder<
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, M>;
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, M>;
41
+ scanIndexForward(forward: boolean): QueryExecutor<Model>;
45
42
 
46
43
  /**
47
44
  * Use a secondary index
48
45
  */
49
- useIndex(indexName: string): QueryExecutor<Model, M>;
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, M>;
51
+ select(attrs: (keyof Model)[]): QueryExecutor<Model>;
55
52
 
56
53
  /**
57
54
  * Use consistent read
58
55
  */
59
- consistentRead(): QueryExecutor<Model, M>;
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, M>;
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, M extends ModelDefinition = any> {
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, M>;
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 { TransactWriteBuilder, TransactWriteState } from './types';
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: any): TransactWriteState => ({
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: any): TransactWriteState => ({
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: any): TransactWriteState => ({
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: any): TransactWriteState => ({
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: any = {
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: any) => createBuilder(addPutItem(state)(params)),
93
- addUpdate: (params: any) => createBuilder(addUpdateItem(state)(params)),
94
- addDelete: (params: any) => createBuilder(addDeleteItem(state)(params)),
95
- addConditionCheck: (params: any) => createBuilder(addConditionCheckItem(state)(params)),
96
- withClientRequestToken: (token: string) => createBuilder(setClientRequestToken(state)(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: any) => TransactWriteBuilder;
27
- readonly addUpdate: (params: any) => TransactWriteBuilder;
28
- readonly addDelete: (params: any) => TransactWriteBuilder;
29
- readonly addConditionCheck: (params: any) => TransactWriteBuilder;
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: () => any;
32
- readonly execute: () => Promise<any>;
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(attr, value) {
63
- const attrName = normalizeAttr(attr);
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 updated item based on returnMode
271
- if (returnMode === 'ALL_NEW' && response.Attributes) {
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
- // For other modes, return the key as a fallback
279
- return key as Model;
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
  }
@@ -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
+ });