@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
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
|
+
};
|