@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.
Files changed (197) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +17 -0
  3. package/dist/builders/batch-get/create-batch-get-builder.d.ts +18 -0
  4. package/dist/builders/batch-get/create-batch-get-builder.d.ts.map +1 -0
  5. package/dist/builders/batch-get/create-batch-get-builder.js +71 -0
  6. package/dist/builders/batch-get/index.d.ts +3 -0
  7. package/dist/builders/batch-get/index.d.ts.map +1 -0
  8. package/dist/builders/batch-get/index.js +18 -0
  9. package/dist/builders/batch-get/types.d.ts +25 -0
  10. package/dist/builders/batch-get/types.d.ts.map +1 -0
  11. package/dist/builders/batch-get/types.js +2 -0
  12. package/dist/builders/batch-write/create-batch-write-builder.d.ts +12 -0
  13. package/dist/builders/batch-write/create-batch-write-builder.d.ts.map +1 -0
  14. package/dist/builders/batch-write/create-batch-write-builder.js +35 -0
  15. package/dist/builders/batch-write/index.d.ts +3 -0
  16. package/dist/builders/batch-write/index.d.ts.map +1 -0
  17. package/dist/builders/batch-write/index.js +18 -0
  18. package/dist/builders/batch-write/types.d.ts +30 -0
  19. package/dist/builders/batch-write/types.d.ts.map +1 -0
  20. package/dist/builders/batch-write/types.js +2 -0
  21. package/dist/builders/delete/create-delete-builder.d.ts +9 -0
  22. package/dist/builders/delete/create-delete-builder.d.ts.map +1 -0
  23. package/dist/builders/delete/create-delete-builder.js +72 -0
  24. package/dist/builders/delete/index.d.ts +3 -0
  25. package/dist/builders/delete/index.d.ts.map +1 -0
  26. package/dist/builders/delete/index.js +18 -0
  27. package/dist/builders/delete/types.d.ts +11 -0
  28. package/dist/builders/delete/types.d.ts.map +1 -0
  29. package/dist/builders/delete/types.js +2 -0
  30. package/dist/builders/get/create-get-builder.d.ts +12 -0
  31. package/dist/builders/get/create-get-builder.d.ts.map +1 -0
  32. package/dist/builders/get/create-get-builder.js +95 -0
  33. package/dist/builders/get/index.d.ts +3 -0
  34. package/dist/builders/get/index.d.ts.map +1 -0
  35. package/dist/builders/get/index.js +18 -0
  36. package/dist/builders/get/types.d.ts +28 -0
  37. package/dist/builders/get/types.d.ts.map +1 -0
  38. package/dist/builders/get/types.js +2 -0
  39. package/dist/builders/index.d.ts +12 -0
  40. package/dist/builders/index.d.ts.map +1 -0
  41. package/dist/builders/index.js +29 -0
  42. package/dist/builders/put/create-put-builder.d.ts +9 -0
  43. package/dist/builders/put/create-put-builder.d.ts.map +1 -0
  44. package/dist/builders/put/create-put-builder.js +104 -0
  45. package/dist/builders/put/index.d.ts +3 -0
  46. package/dist/builders/put/index.d.ts.map +1 -0
  47. package/dist/builders/put/index.js +18 -0
  48. package/dist/builders/put/types.d.ts +17 -0
  49. package/dist/builders/put/types.d.ts.map +1 -0
  50. package/dist/builders/put/types.js +2 -0
  51. package/dist/builders/query/create-query-builder.d.ts +9 -0
  52. package/dist/builders/query/create-query-builder.d.ts.map +1 -0
  53. package/dist/builders/query/create-query-builder.js +283 -0
  54. package/dist/builders/query/index.d.ts +3 -0
  55. package/dist/builders/query/index.d.ts.map +1 -0
  56. package/dist/builders/query/index.js +18 -0
  57. package/dist/builders/query/types.d.ts +79 -0
  58. package/dist/builders/query/types.d.ts.map +1 -0
  59. package/dist/builders/query/types.js +2 -0
  60. package/dist/builders/scan/create-scan-builder.d.ts +12 -0
  61. package/dist/builders/scan/create-scan-builder.d.ts.map +1 -0
  62. package/dist/builders/scan/create-scan-builder.js +103 -0
  63. package/dist/builders/scan/index.d.ts +3 -0
  64. package/dist/builders/scan/index.d.ts.map +1 -0
  65. package/dist/builders/scan/index.js +18 -0
  66. package/dist/builders/scan/types.d.ts +43 -0
  67. package/dist/builders/scan/types.d.ts.map +1 -0
  68. package/dist/builders/scan/types.js +2 -0
  69. package/dist/builders/shared/conditions.d.ts +18 -0
  70. package/dist/builders/shared/conditions.d.ts.map +1 -0
  71. package/dist/builders/shared/conditions.js +48 -0
  72. package/dist/builders/shared/index.d.ts +4 -0
  73. package/dist/builders/shared/index.d.ts.map +1 -0
  74. package/dist/builders/shared/index.js +19 -0
  75. package/dist/builders/shared/operators.d.ts +12 -0
  76. package/dist/builders/shared/operators.d.ts.map +1 -0
  77. package/dist/builders/shared/operators.js +197 -0
  78. package/dist/builders/shared/types.d.ts +77 -0
  79. package/dist/builders/shared/types.d.ts.map +1 -0
  80. package/dist/builders/shared/types.js +6 -0
  81. package/dist/builders/transact-get/create-transact-get-builder.d.ts +18 -0
  82. package/dist/builders/transact-get/create-transact-get-builder.d.ts.map +1 -0
  83. package/dist/builders/transact-get/create-transact-get-builder.js +60 -0
  84. package/dist/builders/transact-get/index.d.ts +3 -0
  85. package/dist/builders/transact-get/index.d.ts.map +1 -0
  86. package/dist/builders/transact-get/index.js +18 -0
  87. package/dist/builders/transact-get/types.d.ts +28 -0
  88. package/dist/builders/transact-get/types.d.ts.map +1 -0
  89. package/dist/builders/transact-get/types.js +2 -0
  90. package/dist/builders/transact-write/create-transact-write-builder.d.ts +18 -0
  91. package/dist/builders/transact-write/create-transact-write-builder.d.ts.map +1 -0
  92. package/dist/builders/transact-write/create-transact-write-builder.js +97 -0
  93. package/dist/builders/transact-write/index.d.ts +3 -0
  94. package/dist/builders/transact-write/index.d.ts.map +1 -0
  95. package/dist/builders/transact-write/index.js +18 -0
  96. package/dist/builders/transact-write/types.d.ts +34 -0
  97. package/dist/builders/transact-write/types.d.ts.map +1 -0
  98. package/dist/builders/transact-write/types.js +2 -0
  99. package/dist/builders/update/create-update-builder.d.ts +14 -0
  100. package/dist/builders/update/create-update-builder.d.ts.map +1 -0
  101. package/dist/builders/update/create-update-builder.js +180 -0
  102. package/dist/builders/update/index.d.ts +3 -0
  103. package/dist/builders/update/index.d.ts.map +1 -0
  104. package/dist/builders/update/index.js +18 -0
  105. package/dist/builders/update/types.d.ts +35 -0
  106. package/dist/builders/update/types.d.ts.map +1 -0
  107. package/dist/builders/update/types.js +2 -0
  108. package/dist/core/types.d.ts +224 -0
  109. package/dist/core/types.d.ts.map +1 -0
  110. package/dist/core/types.js +3 -0
  111. package/dist/entity.d.ts +73 -0
  112. package/dist/entity.d.ts.map +1 -0
  113. package/dist/entity.js +161 -0
  114. package/dist/index.d.ts +4 -0
  115. package/dist/index.d.ts.map +1 -0
  116. package/dist/index.js +20 -0
  117. package/dist/table.d.ts +70 -0
  118. package/dist/table.d.ts.map +1 -0
  119. package/dist/table.js +69 -0
  120. package/dist/utils/dynamodb-logger.d.ts +118 -0
  121. package/dist/utils/dynamodb-logger.d.ts.map +1 -0
  122. package/dist/utils/dynamodb-logger.js +125 -0
  123. package/dist/utils/model-utils.d.ts +24 -0
  124. package/dist/utils/model-utils.d.ts.map +1 -0
  125. package/dist/utils/model-utils.js +88 -0
  126. package/dist/utils/zod-utils.d.ts +13 -0
  127. package/dist/utils/zod-utils.d.ts.map +1 -0
  128. package/dist/utils/zod-utils.js +35 -0
  129. package/eslint.config.mjs +4 -0
  130. package/jest.config.js +11 -0
  131. package/package.json +36 -0
  132. package/src/builders/README.md +272 -0
  133. package/src/builders/batch-get/README.md +98 -0
  134. package/src/builders/batch-get/create-batch-get-builder.test.ts +165 -0
  135. package/src/builders/batch-get/create-batch-get-builder.ts +106 -0
  136. package/src/builders/batch-get/index.ts +2 -0
  137. package/src/builders/batch-get/types.ts +29 -0
  138. package/src/builders/batch-write/README.md +204 -0
  139. package/src/builders/batch-write/create-batch-write-builder.test.ts +173 -0
  140. package/src/builders/batch-write/create-batch-write-builder.ts +49 -0
  141. package/src/builders/batch-write/index.ts +2 -0
  142. package/src/builders/batch-write/types.ts +33 -0
  143. package/src/builders/delete/create-delete-builder.test.ts +294 -0
  144. package/src/builders/delete/create-delete-builder.ts +100 -0
  145. package/src/builders/delete/index.ts +2 -0
  146. package/src/builders/delete/types.ts +11 -0
  147. package/src/builders/get/create-get-builder.test.ts +272 -0
  148. package/src/builders/get/create-get-builder.ts +140 -0
  149. package/src/builders/get/index.ts +2 -0
  150. package/src/builders/get/types.ts +30 -0
  151. package/src/builders/index.ts +14 -0
  152. package/src/builders/put/create-put-builder.test.ts +213 -0
  153. package/src/builders/put/create-put-builder.ts +151 -0
  154. package/src/builders/put/index.ts +2 -0
  155. package/src/builders/put/types.ts +18 -0
  156. package/src/builders/query/create-query-builder.test.ts +230 -0
  157. package/src/builders/query/create-query-builder.ts +353 -0
  158. package/src/builders/query/index.ts +2 -0
  159. package/src/builders/query/types.ts +95 -0
  160. package/src/builders/scan/create-scan-builder.test.ts +260 -0
  161. package/src/builders/scan/create-scan-builder.ts +217 -0
  162. package/src/builders/scan/index.ts +2 -0
  163. package/src/builders/scan/types.ts +49 -0
  164. package/src/builders/shared/conditions.ts +58 -0
  165. package/src/builders/shared/index.ts +3 -0
  166. package/src/builders/shared/operators.test.ts +270 -0
  167. package/src/builders/shared/operators.ts +200 -0
  168. package/src/builders/shared/types.ts +100 -0
  169. package/src/builders/transact-get/README.md +167 -0
  170. package/src/builders/transact-get/create-transact-get-builder.test.ts +239 -0
  171. package/src/builders/transact-get/create-transact-get-builder.ts +67 -0
  172. package/src/builders/transact-get/index.ts +2 -0
  173. package/src/builders/transact-get/types.ts +31 -0
  174. package/src/builders/transact-write/README.md +166 -0
  175. package/src/builders/transact-write/create-transact-write-builder.test.ts +288 -0
  176. package/src/builders/transact-write/create-transact-write-builder.ts +118 -0
  177. package/src/builders/transact-write/index.ts +2 -0
  178. package/src/builders/transact-write/types.ts +33 -0
  179. package/src/builders/update/create-update-builder.test.ts +333 -0
  180. package/src/builders/update/create-update-builder.ts +284 -0
  181. package/src/builders/update/index.ts +2 -0
  182. package/src/builders/update/types.ts +42 -0
  183. package/src/core/types.test.ts +506 -0
  184. package/src/core/types.ts +290 -0
  185. package/src/entity.ts +337 -0
  186. package/src/index.ts +22 -0
  187. package/src/table.ts +109 -0
  188. package/src/utils/dynamodb-logger.test.ts +246 -0
  189. package/src/utils/dynamodb-logger.ts +175 -0
  190. package/src/utils/model-utils.test.ts +232 -0
  191. package/src/utils/model-utils.ts +101 -0
  192. package/src/utils/zod-utils.test.ts +272 -0
  193. package/src/utils/zod-utils.ts +36 -0
  194. package/tests/integration/instagram-clone.integration.test.ts +966 -0
  195. package/tests/integration/pagination-timestamps.integration.test.ts +375 -0
  196. package/tests/integration/transactions.integration.test.ts +529 -0
  197. package/tsconfig.json +12 -0
package/src/table.ts ADDED
@@ -0,0 +1,109 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { DynamoDBClient } from '@aws-sdk/client-dynamodb';
3
+ import {
4
+ InferKeyInput,
5
+ InferModelFromSchema,
6
+ InferInputFromSchema,
7
+ SchemaDefinition,
8
+ } from './core/types';
9
+ import { createEntityAPI, EntityAPI } from './entity';
10
+ import { createTransactWriteBuilder, TransactWriteBuilder } from './builders/transact-write';
11
+ import { createTransactGetBuilder, TransactGetBuilder } from './builders/transact-get';
12
+ import { DynamoDBLogger } from './utils/dynamodb-logger';
13
+
14
+ /**
15
+ * Configuration options for the Table instance
16
+ */
17
+ export type TableConfig<S extends SchemaDefinition> = {
18
+ /** The DynamoDB table name */
19
+ name: string;
20
+ /** AWS DynamoDB client instance */
21
+ client: DynamoDBClient;
22
+ /** Optional logger instance for DynamoDB operations */
23
+ logger?: DynamoDBLogger;
24
+ /** The schema definition for all models in the table */
25
+ schema: S;
26
+ };
27
+
28
+ /**
29
+ * Internal helper type to infer all entity APIs from schema definition
30
+ */
31
+ type EntityMap<S extends SchemaDefinition> = {
32
+ [K in keyof S['models']]: EntityAPI<
33
+ InferModelFromSchema<S, K>,
34
+ InferInputFromSchema<S, K>,
35
+ InferKeyInput<S['models'][K]>
36
+ >;
37
+ };
38
+
39
+ /**
40
+ * Represents a typed DynamoDB Table with entity APIs
41
+ *
42
+ * Provides access to all entity operations (get, put, delete, etc.)
43
+ * via `table.entities.<EntityName>`.
44
+ */
45
+ export class Table<S extends SchemaDefinition> {
46
+ /** Generated entity APIs */
47
+ public readonly entities: EntityMap<S>;
48
+
49
+ /** DynamoDB client */
50
+ private readonly client: DynamoDBClient;
51
+
52
+ constructor(config: TableConfig<S>) {
53
+ const { client, schema, logger, name: tableName } = config;
54
+
55
+ this.client = client;
56
+
57
+ const rawEntities: Record<string, any> = {};
58
+
59
+ for (const modelName in schema.models) {
60
+ const model = schema.models[modelName];
61
+ if (!model) {
62
+ throw new Error(`Model '${modelName}' is missing in schema`);
63
+ }
64
+
65
+ rawEntities[modelName] = createEntityAPI(tableName, modelName, model, client, {
66
+ logger,
67
+ timestamps: schema.params?.timestamps ?? false,
68
+ });
69
+ }
70
+
71
+ this.entities = rawEntities as EntityMap<S>;
72
+ }
73
+
74
+ /**
75
+ * Creates a new TransactWrite builder for atomic multi-item write operations
76
+ *
77
+ * @returns A TransactWriteBuilder instance
78
+ *
79
+ * @example
80
+ * ```typescript
81
+ * // Like a photo atomically
82
+ * await table.transactWrite()
83
+ * .addPut(table.entities.Like.put({ photoId: "123", likingUsername: "alice" }).dbParams())
84
+ * .addUpdate(table.entities.Photo.update({ username: "bob", photoId: "123" }).add("likesCount", 1).dbParams())
85
+ * .execute();
86
+ * ```
87
+ */
88
+ transactWrite(): TransactWriteBuilder {
89
+ return createTransactWriteBuilder(this.client);
90
+ }
91
+
92
+ /**
93
+ * Creates a new TransactGet builder for atomic multi-item read operations
94
+ *
95
+ * @returns A TransactGetBuilder instance
96
+ *
97
+ * @example
98
+ * ```typescript
99
+ * // Get user and photo atomically
100
+ * const [user, photo] = await table.transactGet()
101
+ * .addGet(table.entities.User.get({ username: "alice" }).dbParams())
102
+ * .addGet(table.entities.Photo.get({ username: "alice", photoId: "123" }).dbParams())
103
+ * .execute();
104
+ * ```
105
+ */
106
+ transactGet(): TransactGetBuilder {
107
+ return createTransactGetBuilder(this.client);
108
+ }
109
+ }
@@ -0,0 +1,246 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import { createDynamoDBLogger } from './dynamodb-logger';
3
+
4
+ describe('DynamoDB Logger', () => {
5
+ describe('createDynamoDBLogger', () => {
6
+ it('should not log when disabled', () => {
7
+ const mockLoggerFn = jest.fn();
8
+ const logger = createDynamoDBLogger({
9
+ enabled: false,
10
+ logParams: true,
11
+ logResponse: true,
12
+ loggerFn: mockLoggerFn,
13
+ });
14
+
15
+ logger.log('GetCommand', { TableName: 'Users' }, { Item: { id: '123' } });
16
+
17
+ expect(mockLoggerFn).not.toHaveBeenCalled();
18
+ });
19
+
20
+ it('should log when enabled', () => {
21
+ const mockLoggerFn = jest.fn();
22
+ const logger = createDynamoDBLogger({
23
+ enabled: true,
24
+ logParams: true,
25
+ logResponse: false,
26
+ loggerFn: mockLoggerFn,
27
+ });
28
+
29
+ logger.log('GetCommand', { TableName: 'Users' });
30
+
31
+ expect(mockLoggerFn).toHaveBeenCalledTimes(1);
32
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
33
+ expect(loggedMessage).toContain('[DynamoDB] GetCommand');
34
+ expect(loggedMessage).toContain('šŸ“¤ Request Parameters:');
35
+ expect(loggedMessage).toContain('"TableName": "Users"');
36
+ });
37
+
38
+ it('should log both params and response when configured', () => {
39
+ const mockLoggerFn = jest.fn();
40
+ const logger = createDynamoDBLogger({
41
+ enabled: true,
42
+ logParams: true,
43
+ logResponse: true,
44
+ loggerFn: mockLoggerFn,
45
+ });
46
+
47
+ const params = { TableName: 'Users', Key: { PK: 'USER#123' } };
48
+ const response = { Item: { PK: 'USER#123', username: 'johndoe' } };
49
+
50
+ logger.log('GetCommand', params, response);
51
+
52
+ expect(mockLoggerFn).toHaveBeenCalledTimes(1);
53
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
54
+
55
+ expect(loggedMessage).toContain('[DynamoDB] GetCommand');
56
+ expect(loggedMessage).toContain('šŸ“¤ Request Parameters:');
57
+ expect(loggedMessage).toContain('"TableName": "Users"');
58
+ expect(loggedMessage).toContain('šŸ“„ Response:');
59
+ expect(loggedMessage).toContain('"username": "johndoe"');
60
+ });
61
+
62
+ it('should log only response when logParams is false', () => {
63
+ const mockLoggerFn = jest.fn();
64
+ const logger = createDynamoDBLogger({
65
+ enabled: true,
66
+ logParams: false,
67
+ logResponse: true,
68
+ loggerFn: mockLoggerFn,
69
+ });
70
+
71
+ const params = { TableName: 'Users' };
72
+ const response = { Item: { id: '123' } };
73
+
74
+ logger.log('GetCommand', params, response);
75
+
76
+ expect(mockLoggerFn).toHaveBeenCalledTimes(1);
77
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
78
+
79
+ expect(loggedMessage).toContain('[DynamoDB] GetCommand');
80
+ expect(loggedMessage).not.toContain('šŸ“¤ Request Parameters:');
81
+ expect(loggedMessage).toContain('šŸ“„ Response:');
82
+ });
83
+
84
+ it('should log only params when logResponse is false', () => {
85
+ const mockLoggerFn = jest.fn();
86
+ const logger = createDynamoDBLogger({
87
+ enabled: true,
88
+ logParams: true,
89
+ logResponse: false,
90
+ loggerFn: mockLoggerFn,
91
+ });
92
+
93
+ const params = { TableName: 'Users' };
94
+ const response = { Item: { id: '123' } };
95
+
96
+ logger.log('GetCommand', params, response);
97
+
98
+ expect(mockLoggerFn).toHaveBeenCalledTimes(1);
99
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
100
+
101
+ expect(loggedMessage).toContain('[DynamoDB] GetCommand');
102
+ expect(loggedMessage).toContain('šŸ“¤ Request Parameters:');
103
+ expect(loggedMessage).not.toContain('šŸ“„ Response:');
104
+ });
105
+
106
+ it('should use console.log by default when no loggerFn is provided', () => {
107
+ const consoleLogSpy = jest.spyOn(console, 'log').mockImplementation();
108
+
109
+ const logger = createDynamoDBLogger({
110
+ enabled: true,
111
+ logParams: true,
112
+ logResponse: false,
113
+ });
114
+
115
+ logger.log('GetCommand', { TableName: 'Users' });
116
+
117
+ expect(consoleLogSpy).toHaveBeenCalledTimes(1);
118
+
119
+ consoleLogSpy.mockRestore();
120
+ });
121
+
122
+ it('should include operation name and timestamp in log', () => {
123
+ const mockLoggerFn = jest.fn();
124
+ const logger = createDynamoDBLogger({
125
+ enabled: true,
126
+ logParams: true,
127
+ logResponse: false,
128
+ loggerFn: mockLoggerFn,
129
+ });
130
+
131
+ logger.log('QueryCommand', { TableName: 'Posts' });
132
+
133
+ expect(mockLoggerFn).toHaveBeenCalledTimes(1);
134
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
135
+
136
+ expect(loggedMessage).toContain('[DynamoDB] QueryCommand');
137
+ // Timestamp should be in ISO format
138
+ expect(loggedMessage).toMatch(/\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/);
139
+ });
140
+
141
+ it('should format log with proper separators', () => {
142
+ const mockLoggerFn = jest.fn();
143
+ const logger = createDynamoDBLogger({
144
+ enabled: true,
145
+ logParams: true,
146
+ logResponse: false,
147
+ loggerFn: mockLoggerFn,
148
+ });
149
+
150
+ logger.log('GetCommand', { TableName: 'Users' });
151
+
152
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
153
+
154
+ // Should have separators (80 equal signs)
155
+ expect(loggedMessage).toContain('='.repeat(80));
156
+ });
157
+
158
+ it('should handle undefined response gracefully', () => {
159
+ const mockLoggerFn = jest.fn();
160
+ const logger = createDynamoDBLogger({
161
+ enabled: true,
162
+ logParams: true,
163
+ logResponse: true,
164
+ loggerFn: mockLoggerFn,
165
+ });
166
+
167
+ logger.log('GetCommand', { TableName: 'Users' }, undefined);
168
+
169
+ expect(mockLoggerFn).toHaveBeenCalledTimes(1);
170
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
171
+
172
+ expect(loggedMessage).toContain('šŸ“¤ Request Parameters:');
173
+ expect(loggedMessage).not.toContain('šŸ“„ Response:');
174
+ });
175
+
176
+ it('should work with different operation types', () => {
177
+ const mockLoggerFn = jest.fn();
178
+ const logger = createDynamoDBLogger({
179
+ enabled: true,
180
+ logParams: true,
181
+ logResponse: false,
182
+ loggerFn: mockLoggerFn,
183
+ });
184
+
185
+ const operations = [
186
+ 'GetCommand',
187
+ 'PutCommand',
188
+ 'UpdateCommand',
189
+ 'DeleteCommand',
190
+ 'QueryCommand',
191
+ 'ScanCommand',
192
+ ];
193
+
194
+ operations.forEach((op) => {
195
+ logger.log(op, { TableName: 'Test' });
196
+ });
197
+
198
+ expect(mockLoggerFn).toHaveBeenCalledTimes(operations.length);
199
+
200
+ operations.forEach((op, index) => {
201
+ const loggedMessage = mockLoggerFn.mock.calls[index][0];
202
+ expect(loggedMessage).toContain(`[DynamoDB] ${op}`);
203
+ });
204
+ });
205
+
206
+ it('should handle complex nested objects in params and response', () => {
207
+ const mockLoggerFn = jest.fn();
208
+ const logger = createDynamoDBLogger({
209
+ enabled: true,
210
+ logParams: true,
211
+ logResponse: true,
212
+ loggerFn: mockLoggerFn,
213
+ });
214
+
215
+ const params = {
216
+ TableName: 'Users',
217
+ Item: {
218
+ PK: 'USER#123',
219
+ SK: 'PROFILE',
220
+ nested: {
221
+ level1: {
222
+ level2: {
223
+ value: 'deep',
224
+ },
225
+ },
226
+ },
227
+ },
228
+ };
229
+
230
+ const response = {
231
+ Items: [
232
+ { id: '1', data: { nested: 'value1' } },
233
+ { id: '2', data: { nested: 'value2' } },
234
+ ],
235
+ };
236
+
237
+ logger.log('QueryCommand', params, response);
238
+
239
+ const loggedMessage = mockLoggerFn.mock.calls[0][0];
240
+
241
+ expect(loggedMessage).toContain('"value": "deep"');
242
+ expect(loggedMessage).toContain('"nested": "value1"');
243
+ expect(loggedMessage).toContain('"nested": "value2"');
244
+ });
245
+ });
246
+ });
@@ -0,0 +1,175 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+
3
+ /**
4
+ * Configuration for DynamoDB operation logging
5
+ */
6
+ export interface DynamoDBLoggerConfig {
7
+ /**
8
+ * Enable/disable logging. Default: false
9
+ */
10
+ enabled: boolean;
11
+
12
+ /**
13
+ * Log the request parameters. Default: true
14
+ */
15
+ logParams?: boolean;
16
+
17
+ /**
18
+ * Log the response data. Default: false
19
+ */
20
+ logResponse?: boolean;
21
+
22
+ /**
23
+ * Custom logger function. Default: console.log
24
+ */
25
+ loggerFn?: (message: string) => void;
26
+ }
27
+
28
+ /**
29
+ * DynamoDB Logger interface
30
+ */
31
+ export type DynamoDBLogger = {
32
+ /**
33
+ * Log a DynamoDB operation with its parameters and optionally the response
34
+ */
35
+ log: <TParams, TResponse>(operationName: string, params: TParams, response?: TResponse) => void;
36
+ };
37
+
38
+ /**
39
+ * Pure function to format log message
40
+ * This function has no side effects and is easily testable
41
+ */
42
+ const formatLogMessage = <TParams, TResponse>(
43
+ operationName: string,
44
+ timestamp: string,
45
+ params: TParams,
46
+ response: TResponse | undefined,
47
+ config: DynamoDBLoggerConfig
48
+ ): string => {
49
+ const parts: string[] = [
50
+ '\n' + '='.repeat(80),
51
+ `[DynamoDB] ${operationName} - ${timestamp}`,
52
+ '='.repeat(80),
53
+ ];
54
+
55
+ if (config.logParams && params) {
56
+ parts.push('\nšŸ“¤ Request Parameters:');
57
+ parts.push(JSON.stringify(params, null, 2));
58
+ }
59
+
60
+ if (config.logResponse && response) {
61
+ parts.push('\nšŸ“„ Response:');
62
+ parts.push(JSON.stringify(response, null, 2));
63
+ }
64
+
65
+ parts.push('='.repeat(80) + '\n');
66
+
67
+ return parts.join('\n');
68
+ };
69
+
70
+ /**
71
+ * Creates a DynamoDB logger instance with the given configuration
72
+ * This is the main factory function for creating loggers
73
+ *
74
+ * @example Basic logger with request parameters only
75
+ * ```typescript
76
+ * const logger = createDynamoDBLogger({
77
+ * enabled: true,
78
+ * logParams: true,
79
+ * logResponse: false,
80
+ * });
81
+ * ```
82
+ *
83
+ * @example Full logging (params + response)
84
+ * ```typescript
85
+ * const logger = createDynamoDBLogger({
86
+ * enabled: true,
87
+ * logParams: true,
88
+ * logResponse: true,
89
+ * });
90
+ * ```
91
+ *
92
+ * @example Using with a table
93
+ * ```typescript
94
+ * const logger = createDynamoDBLogger({
95
+ * enabled: true,
96
+ * logParams: true,
97
+ * logResponse: true,
98
+ * });
99
+ *
100
+ * const userTable = createTable({
101
+ * tableName: 'Users',
102
+ * client: dynamoDBClient,
103
+ * schema: userSchema,
104
+ * logger, // Pass logger to table
105
+ * });
106
+ *
107
+ * // Operations will be logged automatically
108
+ * const user = await userTable.get({ username: 'johndoe' }).execute();
109
+ * ```
110
+ *
111
+ * @example Custom logger function (Winston, Pino, etc.)
112
+ * ```typescript
113
+ * import winston from 'winston';
114
+ *
115
+ * const winstonLogger = winston.createLogger({
116
+ * level: 'info',
117
+ * format: winston.format.json(),
118
+ * transports: [new winston.transports.Console()],
119
+ * });
120
+ *
121
+ * const logger = createDynamoDBLogger({
122
+ * enabled: true,
123
+ * logParams: true,
124
+ * logResponse: true,
125
+ * loggerFn: (message) => winstonLogger.info(message),
126
+ * });
127
+ * ```
128
+ *
129
+ * @example Output format
130
+ * ```
131
+ * ================================================================================
132
+ * [DynamoDB] GetCommand - 2025-12-29T10:30:45.123Z
133
+ * ================================================================================
134
+ *
135
+ * šŸ“¤ Request Parameters:
136
+ * {
137
+ * "TableName": "Users",
138
+ * "Key": {
139
+ * "PK": "USER#johndoe",
140
+ * "SK": "PROFILE"
141
+ * }
142
+ * }
143
+ *
144
+ * šŸ“„ Response:
145
+ * {
146
+ * "Item": {
147
+ * "PK": "USER#johndoe",
148
+ * "SK": "PROFILE",
149
+ * "username": "johndoe",
150
+ * "email": "john@example.com"
151
+ * }
152
+ * }
153
+ * ================================================================================
154
+ * ```
155
+ */
156
+ export const createDynamoDBLogger = (config: DynamoDBLoggerConfig): DynamoDBLogger => {
157
+ const logFn = config.loggerFn || console.log;
158
+
159
+ return {
160
+ log: <TParams, TResponse>(
161
+ operationName: string,
162
+ params: TParams,
163
+ response?: TResponse
164
+ ): void => {
165
+ if (!config.enabled) {
166
+ return;
167
+ }
168
+
169
+ const timestamp = new Date().toISOString();
170
+ const message = formatLogMessage(operationName, timestamp, params, response, config);
171
+
172
+ logFn(message);
173
+ },
174
+ };
175
+ };