Skip to content

Commit 958dfe0

Browse files
committed
wip: DynamoDBProvider
1 parent a09e4df commit 958dfe0

File tree

4 files changed

+245
-3
lines changed

4 files changed

+245
-3
lines changed

Diff for: packages/parameters/src/BaseProvider.ts

+6-3
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ExpirableValue } from './ExpirableValue';
55
import { TRANSFORM_METHOD_BINARY, TRANSFORM_METHOD_JSON } from './constants';
66
import { GetParameterError, TransformParameterError } from './Exceptions';
77
import type { BaseProviderInterface, GetMultipleOptionsInterface, GetOptionsInterface, TransformOptions } from './types';
8+
import type { DynamoDBGetOptionsInterface, DynamoDBGetMultipleOptionsInterface } from 'types/DynamoDBProvider';
89

910
abstract class BaseProvider implements BaseProviderInterface {
1011
protected store: Map<string, ExpirableValue>;
@@ -35,8 +36,9 @@ abstract class BaseProvider implements BaseProviderInterface {
3536
* this should be an acceptable tradeoff.
3637
*
3738
* @param {string} name - Parameter name
38-
* @param {GetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
39+
* @param {GetOptionsInterface|DynamoDBGetOptionsInterface} options - Options to configure maximum age, trasformation, AWS SDK options, or force fetch
3940
*/
41+
public async get(name: string, options?: DynamoDBGetOptionsInterface): Promise<undefined | string | Record<string, unknown>>;
4042
public async get(name: string, options?: GetOptionsInterface): Promise<undefined | string | Record<string, unknown>> {
4143
const configs = new GetOptions(options);
4244
const key = [ name, configs.transform ].toString();
@@ -49,7 +51,7 @@ abstract class BaseProvider implements BaseProviderInterface {
4951

5052
let value;
5153
try {
52-
value = await this._get(name, options?.sdkOptions);
54+
value = await this._get(name, options);
5355
} catch (error) {
5456
throw new GetParameterError((error as Error).message);
5557
}
@@ -66,6 +68,7 @@ abstract class BaseProvider implements BaseProviderInterface {
6668
return value;
6769
}
6870

71+
public async getMultiple(path: string, options?: DynamoDBGetMultipleOptionsInterface): Promise<undefined | Record<string, unknown>>;
6972
public async getMultiple(path: string, options?: GetMultipleOptionsInterface): Promise<undefined | Record<string, unknown>> {
7073
const configs = new GetMultipleOptions(options || {});
7174
const key = [ path, configs.transform ].toString();
@@ -78,7 +81,7 @@ abstract class BaseProvider implements BaseProviderInterface {
7881

7982
let values: Record<string, unknown> = {};
8083
try {
81-
values = await this._getMultiple(path, options?.sdkOptions);
84+
values = await this._getMultiple(path, options);
8285
} catch (error) {
8386
throw new GetParameterError((error as Error).message);
8487
}

Diff for: packages/parameters/src/DynamoDBProvider.ts

+79
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { BaseProvider } from './BaseProvider';
2+
import { DynamoDBClient, GetItemCommand, paginateQuery } from '@aws-sdk/client-dynamodb';
3+
import type {
4+
DynamoDBProviderOptions,
5+
DynamoDBGetOptionsInterface,
6+
DynamoDBGetMultipleOptionsInterface
7+
} from './types/DynamoDBProvider';
8+
import type { GetItemCommandInput, QueryCommandInput } from '@aws-sdk/client-dynamodb';
9+
import { marshall, unmarshall } from '@aws-sdk/util-dynamodb';
10+
import type { PaginationConfiguration } from '@aws-sdk/types';
11+
12+
class DynamoDBProvider extends BaseProvider {
13+
public client: DynamoDBClient;
14+
protected keyAttr: string = 'id';
15+
protected sortAttr: string = 'sk';
16+
protected tableName: string;
17+
protected valueAttr: string = 'value';
18+
19+
public constructor(config: DynamoDBProviderOptions) {
20+
super();
21+
22+
const clientConfig = config.clientConfig || {};
23+
this.client = new DynamoDBClient(clientConfig);
24+
this.tableName = config.tableName;
25+
if (config.keyAttr) this.keyAttr = config.keyAttr;
26+
if (config.sortAttr) this.sortAttr = config.sortAttr;
27+
if (config.valueAttr) this.valueAttr = config.valueAttr;
28+
}
29+
30+
protected async _get(name: string, options?: DynamoDBGetOptionsInterface): Promise<string | undefined> {
31+
const sdkOptions: GetItemCommandInput = {
32+
TableName: this.tableName,
33+
Key: marshall({ [this.keyAttr]: name }),
34+
};
35+
if (options && options.hasOwnProperty('sdkOptions')) {
36+
// Explicit arguments passed to the constructor will take precedence over ones passed to the method
37+
delete options.sdkOptions?.Key;
38+
// TODO: check if TableName is overridable
39+
Object.assign(sdkOptions, options.sdkOptions);
40+
}
41+
const result = await this.client.send(new GetItemCommand(sdkOptions));
42+
43+
return result.Item ? unmarshall(result.Item)[this.valueAttr] : undefined;
44+
}
45+
46+
protected async _getMultiple(path: string, options?: DynamoDBGetMultipleOptionsInterface): Promise<Record<string, string | undefined>> {
47+
const sdkOptions: QueryCommandInput = {
48+
TableName: this.tableName,
49+
KeyConditionExpression: `${this.keyAttr} = :key`,
50+
ExpressionAttributeValues: marshall({ ':key': path }),
51+
};
52+
const paginationOptions: PaginationConfiguration = {
53+
client: this.client,
54+
};
55+
if (options && options.hasOwnProperty('sdkOptions')) {
56+
// Explicit arguments passed to the constructor will take precedence over ones passed to the method
57+
delete options.sdkOptions?.KeyConditionExpression;
58+
delete options.sdkOptions?.ExpressionAttributeValues;
59+
if (options.sdkOptions?.hasOwnProperty('Limit')) {
60+
paginationOptions.pageSize = options.sdkOptions.Limit;
61+
}
62+
Object.assign(sdkOptions, options.sdkOptions);
63+
}
64+
65+
const parameters: Record<string, string | undefined> = {};
66+
for await (const page of paginateQuery(paginationOptions, sdkOptions)) {
67+
for (const item of page.Items || []) {
68+
const unmarshalledItem = unmarshall(item);
69+
parameters[unmarshalledItem[this.keyAttr]] = unmarshalledItem[this.valueAttr];
70+
}
71+
}
72+
73+
return parameters;
74+
}
75+
}
76+
77+
export {
78+
DynamoDBProvider,
79+
};

Diff for: packages/parameters/src/types/DynamoDBProvider.ts

+41
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
import type { TransformOptions } from './BaseProvider';
2+
import type { GetItemCommandInput, QueryCommandInput, DynamoDBClientConfig } from '@aws-sdk/client-dynamodb';
3+
4+
// TODO: move this to BaseProvider.ts
5+
interface GetBaseOptionsInterface {
6+
maxAge?: number
7+
forceFetch?: boolean
8+
decrypt?: boolean
9+
transform?: TransformOptions
10+
}
11+
12+
// TODO: move this to BaseProvider.ts
13+
interface GetMultipleBaseOptionsInterface extends GetBaseOptionsInterface {
14+
throwOnTransformError?: boolean
15+
}
16+
17+
interface DynamoDBProviderOptions {
18+
tableName: string
19+
keyAttr?: string
20+
sortAttr?: string
21+
valueAttr?: string
22+
clientConfig?: DynamoDBClientConfig
23+
}
24+
25+
interface DynamoDBGetOptionsInterface {
26+
maxAge?: number
27+
forceFetch?: boolean
28+
decrypt?: boolean
29+
transform?: TransformOptions
30+
sdkOptions?: Partial<GetItemCommandInput>
31+
}
32+
33+
interface DynamoDBGetMultipleOptionsInterface extends GetMultipleBaseOptionsInterface {
34+
sdkOptions?: Partial<QueryCommandInput>
35+
}
36+
37+
export type {
38+
DynamoDBProviderOptions,
39+
DynamoDBGetOptionsInterface,
40+
DynamoDBGetMultipleOptionsInterface,
41+
};
+119
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* Test DynamoDBProvider class
3+
*
4+
* @group unit/parameters/DynamoDBProvider/class
5+
*/
6+
import { DynamoDBProvider } from '../../src/DynamoDBProvider';
7+
import { DynamoDBClient, GetItemCommand } from '@aws-sdk/client-dynamodb';
8+
import { marshall } from '@aws-sdk/util-dynamodb';
9+
import { mockClient } from 'aws-sdk-client-mock';
10+
import 'aws-sdk-client-mock-jest';
11+
/* import type {
12+
DynamoDBGetOptionsInterface,
13+
DynamoDBGetMultipleOptionsInterface,
14+
} from '../../src/types/DynamoDBProvider'; */
15+
16+
describe('Class: DynamoDBProvider', () => {
17+
18+
beforeEach(() => {
19+
jest.clearAllMocks();
20+
});
21+
22+
describe('Method: _get', () => {
23+
24+
test('when called without attribute options, it gets the parameter using the default attribute values', async () => {
25+
26+
// Prepare
27+
const provider = new DynamoDBProvider({
28+
tableName: 'test-table',
29+
});
30+
const parameterName = 'foo';
31+
const parameterValue = 'bar';
32+
const client = mockClient(DynamoDBClient).on(GetItemCommand).resolves({
33+
Item: marshall({
34+
id: parameterName,
35+
value: parameterValue,
36+
})
37+
});
38+
39+
// Act
40+
const value = await provider.get(parameterName);
41+
42+
// Assess
43+
expect(client).toReceiveCommandWith(GetItemCommand, {
44+
TableName: 'test-table',
45+
Key: marshall({
46+
id: parameterName,
47+
}),
48+
});
49+
expect(value).toEqual(parameterValue);
50+
51+
});
52+
53+
test('when called without attribute options, it gets the parameter using the attribute values provided to the constructor', async () => {
54+
55+
// Prepare
56+
const provider = new DynamoDBProvider({
57+
tableName: 'test-table',
58+
keyAttr: 'key',
59+
valueAttr: 'val',
60+
});
61+
const parameterName = 'foo';
62+
const parameterValue = 'bar';
63+
const client = mockClient(DynamoDBClient).on(GetItemCommand).resolves({
64+
Item: marshall({
65+
key: parameterName,
66+
val: parameterValue,
67+
})
68+
});
69+
70+
// Act
71+
const value = await provider.get(parameterName);
72+
73+
// Assess
74+
expect(client).toReceiveCommandWith(GetItemCommand, {
75+
TableName: 'test-table',
76+
Key: marshall({
77+
key: parameterName,
78+
}),
79+
});
80+
expect(value).toEqual(parameterValue);
81+
82+
});
83+
84+
test('when called with attribute options, it gets the parameter using the default attribute values', async () => {
85+
86+
// Prepare
87+
const provider = new DynamoDBProvider({
88+
tableName: 'test-table',
89+
});
90+
const parameterName = 'foo';
91+
const parameterValue = 'bar';
92+
const client = mockClient(DynamoDBClient).on(GetItemCommand).resolves({
93+
Item: marshall({
94+
id: parameterName,
95+
value: parameterValue,
96+
})
97+
});
98+
99+
// Act
100+
const value = await provider.get(parameterName, {
101+
sdkOptions: {
102+
TableName: 'test-table',
103+
}
104+
});
105+
106+
// Assess
107+
expect(client).toReceiveCommandWith(GetItemCommand, {
108+
TableName: 'test-table',
109+
Key: marshall({
110+
id: parameterName,
111+
}),
112+
});
113+
expect(value).toEqual(parameterValue);
114+
115+
});
116+
117+
});
118+
119+
});

0 commit comments

Comments
 (0)