@kravc/dos-dynamodb 1.0.0-alpha.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 (136) hide show
  1. package/README.md +30 -0
  2. package/bin/table.js +88 -0
  3. package/config/default.yaml +36 -0
  4. package/config/test.yaml +0 -0
  5. package/dist/example/Activity.d.ts +34 -0
  6. package/dist/example/Activity.d.ts.map +1 -0
  7. package/dist/example/Activity.js +66 -0
  8. package/dist/example/Activity.js.map +1 -0
  9. package/dist/example/ActivityAttributes.d.ts +23 -0
  10. package/dist/example/ActivityAttributes.d.ts.map +1 -0
  11. package/dist/example/ActivityAttributes.js +54 -0
  12. package/dist/example/ActivityAttributes.js.map +1 -0
  13. package/dist/example/Asset.d.ts +36 -0
  14. package/dist/example/Asset.d.ts.map +1 -0
  15. package/dist/example/Asset.js +56 -0
  16. package/dist/example/Asset.js.map +1 -0
  17. package/dist/example/AssetAttributes.d.ts +28 -0
  18. package/dist/example/AssetAttributes.d.ts.map +1 -0
  19. package/dist/example/AssetAttributes.js +72 -0
  20. package/dist/example/AssetAttributes.js.map +1 -0
  21. package/dist/example/Organization.d.ts +19 -0
  22. package/dist/example/Organization.d.ts.map +1 -0
  23. package/dist/example/Organization.js +42 -0
  24. package/dist/example/Organization.js.map +1 -0
  25. package/dist/example/OrganizationAttributes.d.ts +13 -0
  26. package/dist/example/OrganizationAttributes.d.ts.map +1 -0
  27. package/dist/example/OrganizationAttributes.js +24 -0
  28. package/dist/example/OrganizationAttributes.js.map +1 -0
  29. package/dist/example/index.d.ts +5 -0
  30. package/dist/example/index.d.ts.map +1 -0
  31. package/dist/example/index.js +13 -0
  32. package/dist/example/index.js.map +1 -0
  33. package/dist/src/Document/Document.d.ts +67 -0
  34. package/dist/src/Document/Document.d.ts.map +1 -0
  35. package/dist/src/Document/Document.js +216 -0
  36. package/dist/src/Document/Document.js.map +1 -0
  37. package/dist/src/Document/DocumentWithHashId.d.ts +22 -0
  38. package/dist/src/Document/DocumentWithHashId.d.ts.map +1 -0
  39. package/dist/src/Document/DocumentWithHashId.js +73 -0
  40. package/dist/src/Document/DocumentWithHashId.js.map +1 -0
  41. package/dist/src/Document/__tests__/__helpers.d.ts +21 -0
  42. package/dist/src/Document/__tests__/__helpers.d.ts.map +1 -0
  43. package/dist/src/Document/__tests__/__helpers.js +92 -0
  44. package/dist/src/Document/__tests__/__helpers.js.map +1 -0
  45. package/dist/src/Document/helpers/composeIndexKeys.d.ts +11 -0
  46. package/dist/src/Document/helpers/composeIndexKeys.d.ts.map +1 -0
  47. package/dist/src/Document/helpers/composeIndexKeys.js +81 -0
  48. package/dist/src/Document/helpers/composeIndexKeys.js.map +1 -0
  49. package/dist/src/Document/helpers/index.d.ts +3 -0
  50. package/dist/src/Document/helpers/index.d.ts.map +1 -0
  51. package/dist/src/Document/helpers/index.js +9 -0
  52. package/dist/src/Document/helpers/index.js.map +1 -0
  53. package/dist/src/Table/Table.d.ts +56 -0
  54. package/dist/src/Table/Table.d.ts.map +1 -0
  55. package/dist/src/Table/Table.js +228 -0
  56. package/dist/src/Table/Table.js.map +1 -0
  57. package/dist/src/Table/helpers/buildConditionExpression.d.ts +22 -0
  58. package/dist/src/Table/helpers/buildConditionExpression.d.ts.map +1 -0
  59. package/dist/src/Table/helpers/buildConditionExpression.js +128 -0
  60. package/dist/src/Table/helpers/buildConditionExpression.js.map +1 -0
  61. package/dist/src/Table/helpers/buildQueryCommandInput.d.ts +12 -0
  62. package/dist/src/Table/helpers/buildQueryCommandInput.d.ts.map +1 -0
  63. package/dist/src/Table/helpers/buildQueryCommandInput.js +60 -0
  64. package/dist/src/Table/helpers/buildQueryCommandInput.js.map +1 -0
  65. package/dist/src/Table/helpers/buildQueryConditionExpression.d.ts +17 -0
  66. package/dist/src/Table/helpers/buildQueryConditionExpression.d.ts.map +1 -0
  67. package/dist/src/Table/helpers/buildQueryConditionExpression.js +77 -0
  68. package/dist/src/Table/helpers/buildQueryConditionExpression.js.map +1 -0
  69. package/dist/src/Table/helpers/buildTableSchema.d.ts +6 -0
  70. package/dist/src/Table/helpers/buildTableSchema.d.ts.map +1 -0
  71. package/dist/src/Table/helpers/buildTableSchema.js +100 -0
  72. package/dist/src/Table/helpers/buildTableSchema.js.map +1 -0
  73. package/dist/src/Table/helpers/buildUpdateExpression.d.ts +10 -0
  74. package/dist/src/Table/helpers/buildUpdateExpression.d.ts.map +1 -0
  75. package/dist/src/Table/helpers/buildUpdateExpression.js +69 -0
  76. package/dist/src/Table/helpers/buildUpdateExpression.js.map +1 -0
  77. package/dist/src/Table/helpers/filterConditionExpression.d.ts +5 -0
  78. package/dist/src/Table/helpers/filterConditionExpression.d.ts.map +1 -0
  79. package/dist/src/Table/helpers/filterConditionExpression.js +68 -0
  80. package/dist/src/Table/helpers/filterConditionExpression.js.map +1 -0
  81. package/dist/src/Table/helpers/getRawClientConfig.d.ts +5 -0
  82. package/dist/src/Table/helpers/getRawClientConfig.d.ts.map +1 -0
  83. package/dist/src/Table/helpers/getRawClientConfig.js +29 -0
  84. package/dist/src/Table/helpers/getRawClientConfig.js.map +1 -0
  85. package/dist/src/Table/helpers/getTableOptions.d.ts +51 -0
  86. package/dist/src/Table/helpers/getTableOptions.d.ts.map +1 -0
  87. package/dist/src/Table/helpers/getTableOptions.js +144 -0
  88. package/dist/src/Table/helpers/getTableOptions.js.map +1 -0
  89. package/dist/src/Table/helpers/index.d.ts +10 -0
  90. package/dist/src/Table/helpers/index.d.ts.map +1 -0
  91. package/dist/src/Table/helpers/index.js +21 -0
  92. package/dist/src/Table/helpers/index.js.map +1 -0
  93. package/dist/src/Table/index.d.ts +8 -0
  94. package/dist/src/Table/index.d.ts.map +1 -0
  95. package/dist/src/Table/index.js +13 -0
  96. package/dist/src/Table/index.js.map +1 -0
  97. package/dist/src/index.d.ts +7 -0
  98. package/dist/src/index.d.ts.map +1 -0
  99. package/dist/src/index.js +14 -0
  100. package/dist/src/index.js.map +1 -0
  101. package/docker-compose.yaml +10 -0
  102. package/eslint.config.mjs +35 -0
  103. package/example/Activity.ts +123 -0
  104. package/example/ActivityAttributes.ts +72 -0
  105. package/example/Asset.ts +78 -0
  106. package/example/AssetAttributes.ts +87 -0
  107. package/example/Organization.ts +61 -0
  108. package/example/OrganizationAttributes.ts +28 -0
  109. package/example/index.ts +9 -0
  110. package/jest.config.mjs +10 -0
  111. package/package.json +50 -0
  112. package/src/Document/DefaultAttributes.d.ts +16 -0
  113. package/src/Document/Document.ts +257 -0
  114. package/src/Document/DocumentWithHashId.ts +85 -0
  115. package/src/Document/__tests__/Document.test.ts +596 -0
  116. package/src/Document/__tests__/DocumentWithHashId.test.ts +81 -0
  117. package/src/Document/__tests__/__helpers.ts +115 -0
  118. package/src/Document/helpers/__tests__/composeIndexKeys.test.ts +40 -0
  119. package/src/Document/helpers/composeIndexKeys.ts +137 -0
  120. package/src/Document/helpers/index.ts +5 -0
  121. package/src/Table/Table.ts +354 -0
  122. package/src/Table/__tests__/Table.test.ts +64 -0
  123. package/src/Table/helpers/__tests__/buildQueryCommandInput.test.ts +14 -0
  124. package/src/Table/helpers/__tests__/buildTableSchema.test.ts +19 -0
  125. package/src/Table/helpers/buildConditionExpression.ts +151 -0
  126. package/src/Table/helpers/buildQueryCommandInput.ts +113 -0
  127. package/src/Table/helpers/buildQueryConditionExpression.ts +109 -0
  128. package/src/Table/helpers/buildTableSchema.ts +151 -0
  129. package/src/Table/helpers/buildUpdateExpression.ts +95 -0
  130. package/src/Table/helpers/filterConditionExpression.ts +87 -0
  131. package/src/Table/helpers/getRawClientConfig.ts +35 -0
  132. package/src/Table/helpers/getTableOptions.ts +228 -0
  133. package/src/Table/helpers/index.ts +21 -0
  134. package/src/Table/index.ts +18 -0
  135. package/src/index.ts +15 -0
  136. package/tsconfig.json +26 -0
@@ -0,0 +1,115 @@
1
+ import { Schema, Validator } from '@kravc/schema';
2
+ import { Asset, Activity, Organization } from '../../../example';
3
+ import { Context, type Identity, type Request, type LambdaRequest, type ExtraContext } from '@kravc/dos';
4
+
5
+ const operationId = 'CreateAsset';
6
+
7
+ const DEFAULT_LAMBDA_REQUEST = {
8
+ headers: {},
9
+ operationId,
10
+ } as LambdaRequest;
11
+
12
+ type Props = {
13
+ request?: Request;
14
+ schemas?: Schema[];
15
+ identity?: Identity;
16
+ extraContext?: ExtraContext;
17
+ };
18
+
19
+ /** Deletes and recreates the table. */
20
+ export const resetTable = async () => await Asset.table.reset();
21
+
22
+ /** Creates an instance of an operation context. */
23
+ export const createContext = (props: Props = {}): Context => {
24
+ const {
25
+ request = DEFAULT_LAMBDA_REQUEST,
26
+ schemas = [ Asset.schema, Activity.schema, Organization.schema ],
27
+ identity = {
28
+ sub: 'USR_1',
29
+ organizationId: 'ORG_1',
30
+ email: 'john.doe@example.com',
31
+ lastName: 'Doe',
32
+ firstName: 'John',
33
+ },
34
+ extraContext = {},
35
+ } = props;
36
+
37
+ const validator = new Validator(schemas);
38
+
39
+ const spec = {
40
+ basePath: '/api',
41
+ paths: {
42
+ [`/${operationId}`]: {
43
+ post: {
44
+ operationId,
45
+ },
46
+ }
47
+ }
48
+ };
49
+
50
+ const context = new Context({ spec, validator }, request, extraContext);
51
+ context.identity = identity;
52
+
53
+ return context;
54
+ };
55
+
56
+ /** Creates an asset. */
57
+ export const createAsset = async (extraAttributes: Record<string, unknown> = {}) => {
58
+ const context = createContext();
59
+
60
+ const attributes = {
61
+ name: 'Text Document',
62
+ tags: [ 'Tag A', 'Tag B' ],
63
+ type: 'CONTENT',
64
+ status: 'ACTIVE',
65
+ content: 'Text document content.',
66
+ ...extraAttributes,
67
+ };
68
+
69
+ const asset = await Asset.create(context, attributes);
70
+
71
+ return asset;
72
+ };
73
+
74
+ /** Creates an activity. */
75
+ export const createActivity = async (extraAttributes: Record<string, unknown> = {}) => {
76
+ const context = createContext();
77
+
78
+ const attributes = {
79
+ operationId: 'CreateAsset',
80
+ summaryTemplate: 'Asset "Text Document" has been created by {user:createdBy}',
81
+ parametersJson: '{ "mutation": { "name": "Text Document" } }',
82
+ resultDocumentJson: '{ "name": "Text Document", "createdBy": "USR_1" }',
83
+ userId: 'USR_1',
84
+ traceId: 'TRACE_ID',
85
+ userAgent: 'USER_AGENT',
86
+ ipAddress: '127.0.0.1',
87
+ identity: {
88
+ sub: 'USR_1',
89
+ email: 'john.doe@example.com',
90
+ firstName: 'John',
91
+ lastName: 'Doe',
92
+ permissions: [ 'assets-write' ],
93
+ },
94
+ ...extraAttributes,
95
+ };
96
+
97
+ const activity = await Activity.create(context, attributes);
98
+
99
+ return activity;
100
+ };
101
+
102
+ /** Creates an organization. */
103
+ export const createOrganization = async (extraAttributes: Record<string, unknown> = {}) => {
104
+ const context = createContext();
105
+
106
+ const attributes = {
107
+ name: 'Organization 1',
108
+ status: 'ACTIVE',
109
+ ...extraAttributes,
110
+ };
111
+
112
+ const organization = await Organization.create(context, attributes);
113
+
114
+ return organization;
115
+ };
@@ -0,0 +1,40 @@
1
+ import getTableOptions from '../../../Table/helpers/getTableOptions';
2
+ import composeIndexKeys from '../composeIndexKeys';
3
+
4
+ describe('composeIndexKeys(props)', () => {
5
+ it('returns empty object if no composers are defined for the document', () => {
6
+ const tableOptions = getTableOptions('partition', 'id');
7
+
8
+ const props = {
9
+ idPrefix: 'FIL',
10
+ attributes: {},
11
+ documentName: 'File',
12
+ tableOptions
13
+ };
14
+
15
+ const composedKeys = composeIndexKeys(props);
16
+
17
+ expect(composedKeys).toEqual({});
18
+ });
19
+
20
+ it('throws an exception if the index is not defined in the table config', () => {
21
+ const tableOptions = getTableOptions('partition', 'id');
22
+
23
+ tableOptions.indexKeys = {
24
+ lsi4: {
25
+ documentName: 'Activity',
26
+ sk: '#updatedAt',
27
+ }
28
+ };
29
+
30
+ const props = {
31
+ idPrefix: 'ACT',
32
+ attributes: {},
33
+ documentName: 'Activity',
34
+ tableOptions
35
+ };
36
+
37
+ expect(() => composeIndexKeys(props))
38
+ .toThrow('Index "lsi4" is not defined');
39
+ });
40
+ });
@@ -0,0 +1,137 @@
1
+ import { get } from 'lodash';
2
+ import { got } from '@kravc/dos';
3
+ import type { TableOptions, LocalIndexKeyComposer, GlobalIndexKeyComposer } from '../../Table';
4
+
5
+ type Attributes = Record<string, unknown>;
6
+
7
+ const PLACEHOLDER_REGEX = /#(\w+)/g;
8
+
9
+ /** Replaces each #name in the template with the value from attributes. */
10
+ const composeValue = (
11
+ attributes: Attributes,
12
+ template: string
13
+ ): string | undefined => {
14
+ const names = [...template.matchAll(PLACEHOLDER_REGEX)].map(([ , name ]) => name);
15
+
16
+ for (const name of names) {
17
+ const value = get(attributes, name);
18
+
19
+ if (!value) {
20
+ return;
21
+ }
22
+ }
23
+
24
+ const composedValue = template.replace(PLACEHOLDER_REGEX, (_, name) =>
25
+ `#${attributes[name]}`
26
+ );
27
+
28
+ return composedValue;
29
+ };
30
+
31
+ /** Returns sort key value using ID prefix and template. */
32
+ const composeSortKeyValue = (
33
+ attributes: Attributes,
34
+ template: string
35
+ ): string | undefined => {
36
+ return composeValue(attributes, template);
37
+ };
38
+
39
+ /** Returns partition key value using template. */
40
+ const composePartitionKeyValue = (
41
+ idPrefix: string,
42
+ attributes: Attributes,
43
+ template: string
44
+ ): string | undefined => {
45
+ const composedValue = composeValue(attributes, template);
46
+
47
+ if (!composedValue) {
48
+ return;
49
+ }
50
+
51
+ return `${idPrefix}#${composedValue}`.replace('##', '#');
52
+ };
53
+
54
+ /** Returns index keys composed from item attributes. */
55
+ const composeIndexKeys = ({
56
+ idPrefix,
57
+ attributes,
58
+ documentName,
59
+ tableOptions,
60
+ }: {
61
+ idPrefix: string,
62
+ attributes: Attributes
63
+ documentName: string,
64
+ tableOptions: TableOptions,
65
+ }): Attributes => {
66
+ const {
67
+ indexKeys,
68
+ isLocalSecondaryIndex,
69
+ getLocalIndexSortKeyName,
70
+ getGlobalIndexSortKeyName,
71
+ getGlobalIndexPartitionKeyName,
72
+ } = tableOptions;
73
+
74
+ const indexNames = Object.keys(indexKeys);
75
+
76
+ const localKeyComposers = indexNames
77
+ .filter(indexName => isLocalSecondaryIndex(indexName))
78
+ .filter(indexName => got(indexKeys[indexName] as LocalIndexKeyComposer, 'documentName') === documentName)
79
+ .map(indexName => ({
80
+ localIndexComposer: indexKeys[indexName] as LocalIndexKeyComposer,
81
+ indexName,
82
+ }));
83
+
84
+ const globalKeyComposers = indexNames
85
+ .filter(indexName => !isLocalSecondaryIndex(indexName))
86
+ .filter(indexName => !!get((indexKeys[indexName] as Record<string, GlobalIndexKeyComposer>), documentName))
87
+ .map(indexName => ({
88
+ globalIndexComposer: got(indexKeys[indexName], documentName) as GlobalIndexKeyComposer,
89
+ indexName,
90
+ }));
91
+
92
+ const composedKeys = {} as Attributes;
93
+ const indexKeysJson = JSON.stringify(indexKeys, null, 2);
94
+
95
+ for (const { indexName, localIndexComposer } of localKeyComposers) {
96
+ const sortKey = getLocalIndexSortKeyName(indexName);
97
+
98
+ const errorTemplate = `Sort key composer should be defined by "${indexName}.$PATH", index keys: ${indexKeysJson}`;
99
+ const skTemplate = got(localIndexComposer, 'sk', errorTemplate);
100
+ const skValue = composeSortKeyValue(attributes, skTemplate);
101
+
102
+ if (!skValue) {
103
+ continue;
104
+ }
105
+
106
+ composedKeys[sortKey] = skValue;
107
+ }
108
+
109
+ for (const { indexName, globalIndexComposer } of globalKeyComposers) {
110
+ const sortKey = getGlobalIndexSortKeyName(indexName);
111
+ const partitionKey = getGlobalIndexPartitionKeyName(indexName);
112
+
113
+ const skErrorTemplate = `Partition key composer should be defined by "${indexName}.${documentName}.$PATH", index keys: ${indexKeysJson}`;
114
+ const pkTemplate = got(globalIndexComposer, 'pk', skErrorTemplate) as string;
115
+ const pkValue = composePartitionKeyValue(idPrefix, attributes, pkTemplate);
116
+
117
+ if (!pkValue) {
118
+ continue;
119
+ }
120
+
121
+ composedKeys[partitionKey] = pkValue;
122
+
123
+ const pkErrorTemplate = `Sort key composer should be defined by "${indexName}.${documentName}.$PATH", index keys: ${indexKeysJson}`;
124
+ const skTemplate = got(globalIndexComposer, 'sk', pkErrorTemplate) as string;
125
+ const skValue = composeSortKeyValue(attributes, skTemplate);
126
+
127
+ if (!skValue) {
128
+ continue;
129
+ }
130
+
131
+ composedKeys[sortKey] = skValue;
132
+ }
133
+
134
+ return composedKeys;
135
+ };
136
+
137
+ export default composeIndexKeys;
@@ -0,0 +1,5 @@
1
+ import composeIndexKeys from './composeIndexKeys';
2
+
3
+ export {
4
+ composeIndexKeys,
5
+ };
@@ -0,0 +1,354 @@
1
+ import type { MutationMap, QueryMap } from '@kravc/dos';
2
+ import { DynamoDBClient, DeleteTableCommand, CreateTableCommand } from '@aws-sdk/client-dynamodb';
3
+
4
+ import {
5
+ type PutCommandInput,
6
+ type GetCommandInput,
7
+ type QueryCommandInput,
8
+ type QueryCommandOutput,
9
+ type DeleteCommandInput,
10
+ DynamoDBDocument,
11
+ UpdateCommandInput,
12
+ } from '@aws-sdk/lib-dynamodb';
13
+
14
+ import {
15
+ type TableOptions,
16
+ getTableOptions,
17
+ buildTableSchema,
18
+ getRawClientConfig,
19
+ buildUpdateExpression,
20
+ buildQueryCommandInput,
21
+ buildConditionExpression,
22
+ filterConditionExpression,
23
+ } from './helpers';
24
+
25
+ type Options = {
26
+ partitionKey: string;
27
+ sortKey: string;
28
+ }
29
+
30
+ export type ItemAttributes = Record<string, unknown>;
31
+
32
+ /** DynamoDB table class. */
33
+ class Table {
34
+ private _client: DynamoDBDocument;
35
+ private _rawClient: DynamoDBClient;
36
+ private _tableOptions: TableOptions;
37
+
38
+ /** Creates an instance of a table. */
39
+ constructor(options: Options) {
40
+ const { sortKey, partitionKey } = options;
41
+
42
+ const tableOptions = getTableOptions(partitionKey, sortKey);
43
+
44
+ const { region, profile } = tableOptions;
45
+ const config = getRawClientConfig(region, profile);
46
+
47
+ this._rawClient = new DynamoDBClient(config);
48
+ this._client = DynamoDBDocument.from(this._rawClient);
49
+
50
+ this._tableOptions = tableOptions;
51
+ }
52
+
53
+ /** Returns table options. */
54
+ get options() {
55
+ return this._tableOptions;
56
+ }
57
+
58
+ /** Creates a DynamoDB table. */
59
+ async create(): Promise<void> {
60
+ const tableSchema = buildTableSchema(this._tableOptions);
61
+ const command = new CreateTableCommand(tableSchema);
62
+
63
+ await this._rawClient.send(command);
64
+ }
65
+
66
+ /** Deletes a DynamoDB table. */
67
+ async delete(): Promise<void> {
68
+ const { name: TableName } = this._tableOptions;
69
+ const deleteTableCommand = new DeleteTableCommand({ TableName });
70
+
71
+ await this._client.send(deleteTableCommand);
72
+ }
73
+
74
+ /** Resets a DynamoDB table. */
75
+ async reset(): Promise<void> {
76
+ try {
77
+ await this.delete();
78
+
79
+ } catch (dynamoError: unknown) {
80
+ const { name: errorName } = dynamoError as { name: string };
81
+
82
+ /* istanbul ignore next */
83
+ if (errorName !== 'ResourceNotFoundException') {
84
+ throw dynamoError;
85
+ }
86
+ }
87
+
88
+ await this.create();
89
+ }
90
+
91
+ /** Returns key and conditionQuery from query. */
92
+ _parseItemQuery(query: QueryMap) {
93
+ const { primaryKey } = this._tableOptions;
94
+ const { sortKey, partitionKey } = primaryKey;
95
+
96
+ const {
97
+ [sortKey]: sortKeyValue,
98
+ [partitionKey]: partitionKeyValue,
99
+ ...conditionQuery
100
+ } = query;
101
+
102
+ const Key = {
103
+ [sortKey]: sortKeyValue,
104
+ [partitionKey]: partitionKeyValue,
105
+ };
106
+
107
+ return { Key, conditionQuery };
108
+ }
109
+
110
+ /** Creates a table item; returns false if item already exists. */
111
+ async createItem(attributes: ItemAttributes): Promise<boolean> {
112
+ const { name: TableName, primaryKey } = this._tableOptions;
113
+
114
+ const { sortKey } = primaryKey;
115
+
116
+ const ConditionExpression = `#${sortKey} <> :${sortKey}`;
117
+
118
+ const ExpressionAttributeNames = {
119
+ [`#${sortKey}`]: sortKey,
120
+ };
121
+
122
+ const ExpressionAttributeValues = {
123
+ [`:${sortKey}`]: attributes[sortKey],
124
+ };
125
+
126
+ const putCommandInput = {
127
+ TableName,
128
+ ConditionExpression,
129
+ ExpressionAttributeNames,
130
+ ExpressionAttributeValues,
131
+ Item: attributes,
132
+ } as PutCommandInput;
133
+
134
+ try {
135
+ await this._client.put(putCommandInput);
136
+
137
+ } catch (dynamoError: unknown) {
138
+ const { name: errorName } = dynamoError as { name: string };
139
+
140
+ if (errorName === 'ConditionalCheckFailedException') {
141
+ return false;
142
+ }
143
+
144
+ /* istanbul ignore next */
145
+ if (errorName !== 'ResourceNotFoundException') {
146
+ throw dynamoError;
147
+ }
148
+
149
+ throw new Error(`Table "${TableName}" does not exist`);
150
+ }
151
+
152
+ return true;
153
+ }
154
+
155
+ /** Returns a table item, or undefined if not found. */
156
+ async getItem(query: QueryMap): Promise<ItemAttributes | undefined> {
157
+ const { name: TableName } = this._tableOptions;
158
+ const { Key, conditionQuery } = this._parseItemQuery(query);
159
+
160
+ const getCommandInput = {
161
+ ConsistentRead: true,
162
+ TableName,
163
+ Key,
164
+ } as GetCommandInput;
165
+
166
+ let result;
167
+
168
+ try {
169
+ result = await this._client.get(getCommandInput);
170
+
171
+ } catch (dynamoError: unknown) {
172
+ const { name: errorName } = dynamoError as { name: string };
173
+
174
+ /* istanbul ignore next */
175
+ if (errorName !== 'ResourceNotFoundException') {
176
+ throw dynamoError;
177
+ }
178
+
179
+ throw new Error(`Table "${TableName}" does not exist`);
180
+ }
181
+
182
+ const { Item: itemAttributes } = result;
183
+
184
+ return filterConditionExpression(itemAttributes, conditionQuery);
185
+ }
186
+
187
+ /** Updates a table item with attributes. */
188
+ async updateItem(query: QueryMap, mutation: MutationMap): Promise<ItemAttributes | undefined> {
189
+ const { name: TableName, primaryKey } = this._tableOptions;
190
+ const { Key } = this._parseItemQuery(query);
191
+
192
+ const { partitionKey, sortKey } = primaryKey;
193
+
194
+ delete mutation[sortKey];
195
+ delete mutation[partitionKey];
196
+
197
+ const {
198
+ UpdateExpression,
199
+ ConditionExpression,
200
+ ExpressionAttributeNames,
201
+ ExpressionAttributeValues,
202
+ } = buildUpdateExpression(query, mutation);
203
+
204
+ const updateCommandInput = {
205
+ ReturnValues: 'ALL_NEW',
206
+ Key,
207
+ TableName,
208
+ UpdateExpression,
209
+ ConditionExpression,
210
+ ExpressionAttributeNames,
211
+ ExpressionAttributeValues,
212
+ } as UpdateCommandInput;
213
+
214
+ let result;
215
+
216
+ try {
217
+ result = await this._client.update(updateCommandInput);
218
+
219
+ } catch (dynamoError: unknown) {
220
+ const { name: errorName } = dynamoError as { name: string };
221
+
222
+ if (errorName === 'ConditionalCheckFailedException') {
223
+ return;
224
+ }
225
+
226
+ /* istanbul ignore next */
227
+ if (errorName !== 'ResourceNotFoundException') {
228
+ throw dynamoError;
229
+ }
230
+
231
+ throw new Error(`Table "${TableName}" does not exist`);
232
+ }
233
+
234
+ const { Attributes: attributes } = result;
235
+
236
+ return attributes;
237
+ }
238
+
239
+ /** Deletes a table item; returns false if item not found. */
240
+ async deleteItem(query: QueryMap): Promise<boolean> {
241
+ const { name: TableName } = this._tableOptions;
242
+ const { Key } = this._parseItemQuery(query);
243
+
244
+ const {
245
+ ConditionExpression,
246
+ ExpressionAttributeNames,
247
+ ExpressionAttributeValues,
248
+ } = buildConditionExpression(query);
249
+
250
+ const deleteCommandInput = {
251
+ TableName,
252
+ Key,
253
+ ConditionExpression,
254
+ ExpressionAttributeNames,
255
+ ExpressionAttributeValues
256
+ } as DeleteCommandInput;
257
+
258
+ try {
259
+ await this._client.delete(deleteCommandInput);
260
+
261
+ } catch (dynamoError: unknown) {
262
+ const { name: errorName } = dynamoError as { name: string };
263
+
264
+ if (errorName === 'ConditionalCheckFailedException') {
265
+ return false;
266
+ }
267
+
268
+ /* istanbul ignore next */
269
+ if (errorName !== 'ResourceNotFoundException') {
270
+ throw dynamoError;
271
+ }
272
+
273
+ throw new Error(`Table "${TableName}" does not exist`);
274
+ }
275
+
276
+ return true;
277
+ }
278
+
279
+ /** Returns item from the table using a query. */
280
+ async _queryItems(input: QueryCommandInput): Promise<QueryCommandOutput> {
281
+ let result: QueryCommandOutput;
282
+
283
+ try {
284
+ result = await this._client.query(input);
285
+
286
+ } catch (dynamoError: unknown) {
287
+ const { name: errorName } = dynamoError as { name: string };
288
+
289
+ /* istanbul ignore next */
290
+ if (errorName !== 'ResourceNotFoundException') {
291
+ throw dynamoError;
292
+ }
293
+
294
+ const { TableName } = input;
295
+
296
+ throw new Error(`Table "${TableName}" does not exist`);
297
+ }
298
+
299
+ return result;
300
+ }
301
+
302
+ /** Lists table items. */
303
+ async listItems(
304
+ query: QueryMap,
305
+ options: {
306
+ sort: string,
307
+ limit: number,
308
+ indexName?: string,
309
+ exclusiveStartKey?: string
310
+ }
311
+ ): Promise<{
312
+ count: number;
313
+ items: ItemAttributes[];
314
+ lastEvaluatedKey?: string;
315
+ }> {
316
+ const queryCommandInput = buildQueryCommandInput(this._tableOptions, query, options);
317
+
318
+ const { limit } = options;
319
+
320
+ let count = 0;
321
+ let items = [] as Record<string, unknown>[];
322
+ let lastEvaluatedKey;
323
+ let isNextChunkRequired = true;
324
+
325
+ do {
326
+ const {
327
+ Items: chunk,
328
+ LastEvaluatedKey
329
+ } = await this._queryItems(queryCommandInput);
330
+
331
+ const hasMoreItems = !!LastEvaluatedKey;
332
+
333
+ items = [ ...items, ...(chunk as ItemAttributes[]) ];
334
+ count = items.length;
335
+ lastEvaluatedKey = LastEvaluatedKey;
336
+ isNextChunkRequired = hasMoreItems && count < limit;
337
+
338
+ queryCommandInput.ExclusiveStartKey = LastEvaluatedKey;
339
+
340
+ } while (isNextChunkRequired);
341
+
342
+ if (lastEvaluatedKey) {
343
+ lastEvaluatedKey = encodeURIComponent(JSON.stringify(lastEvaluatedKey));
344
+ }
345
+
346
+ return {
347
+ count,
348
+ items,
349
+ lastEvaluatedKey
350
+ };
351
+ }
352
+ }
353
+
354
+ export default Table;
@@ -0,0 +1,64 @@
1
+ import Table from '../Table';
2
+
3
+ describe('Table', () => {
4
+ const table = new Table({
5
+ sortKey: 'id',
6
+ partitionKey: 'partition',
7
+ });
8
+
9
+ beforeAll(async () => {
10
+ try {
11
+ await table.delete();
12
+
13
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
14
+ } catch (_error) {
15
+ ;
16
+ }
17
+ });
18
+
19
+ describe('Table.createItem(attributes)', () => {
20
+ it('throws an exception if the table does not exist', async () => {
21
+ const attributes = {};
22
+ await expect(table.createItem(attributes))
23
+ .rejects
24
+ .toThrow('Table "kravc-dos-dynamodb" does not exist');
25
+ });
26
+ });
27
+
28
+ describe('Table.getItem(query)', () => {
29
+ it('throws an exception if the table does not exist', async () => {
30
+ const query = { partition: 'ORG_1', id: 'AST_BAD_ID' };
31
+ await expect(table.getItem(query))
32
+ .rejects
33
+ .toThrow('Table "kravc-dos-dynamodb" does not exist');
34
+ });
35
+ });
36
+
37
+ describe('Table.updateItem(query, mutation)', () => {
38
+ it('throws an exception if the table does not exist', async () => {
39
+ const query = { partition: 'ORG_1', id: 'AST_BAD_ID' };
40
+ const mutation = { name: 'Text Document' };
41
+ await expect(table.updateItem(query, mutation))
42
+ .rejects
43
+ .toThrow('Table "kravc-dos-dynamodb" does not exist');
44
+ });
45
+ });
46
+
47
+ describe('Table.deleteItem(query)', () => {
48
+ it('throws an exception if the table does not exist', async () => {
49
+ const query = { partition: 'ORG_1', id: 'AST_BAD_ID' };
50
+ await expect(table.deleteItem(query))
51
+ .rejects
52
+ .toThrow('Table "kravc-dos-dynamodb" does not exist');
53
+ });
54
+ });
55
+
56
+ describe('Table.listItems(query, options)', () => {
57
+ it('throws an exception if the table does not exist', async () => {
58
+ const query = { partition: 'ORG_1' };
59
+ await expect(table.listItems(query, { limit: 100, sort: 'desc' }))
60
+ .rejects
61
+ .toThrow('Table "kravc-dos-dynamodb" does not exist');
62
+ });
63
+ });
64
+ });