@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,14 @@
1
+ import getTableOptions from '../getTableOptions';
2
+ import buildQueryCommandInput from '../buildQueryCommandInput';
3
+
4
+ describe('buildQueryCommandInput(tableOptions, query, options)', () => {
5
+ it('throws an exception if the index is not defined in the table config', () => {
6
+ const tableOptions = getTableOptions('partition', 'id');
7
+
8
+ const query = {};
9
+ const options = { sort: 'asc', limit: 10, indexName: 'customGSI' };
10
+
11
+ expect(() => buildQueryCommandInput(tableOptions, query, options))
12
+ .toThrow('Index "customGSI" is not defined');
13
+ });
14
+ });
@@ -0,0 +1,19 @@
1
+ import getTableOptions from '../getTableOptions';
2
+ import buildTableSchema from '../buildTableSchema';
3
+
4
+ describe('buildTableSchema(tableOptions)', () => {
5
+ it('supports global secondary indexes without a sort key', () => {
6
+ const tableOptions = getTableOptions('partition', 'id');
7
+
8
+ tableOptions.globalSecondaryIndexes = [
9
+ {
10
+ name: 'gsiTest',
11
+ partitionKey: 'gsiPartitionKey',
12
+ }
13
+ ];
14
+
15
+ const tableSchema = buildTableSchema(tableOptions);
16
+
17
+ expect(tableSchema.GlobalSecondaryIndexes).toHaveLength(1);
18
+ });
19
+ });
@@ -0,0 +1,151 @@
1
+ import type { QueryMap } from '@kravc/dos';
2
+
3
+ export enum ExpressionKey {
4
+ LT = 'lt',
5
+ LE = 'le',
6
+ GT = 'gt',
7
+ GE = 'ge',
8
+ NOT = 'not',
9
+ CONTAIN = 'contains',
10
+ EXCLUDE = 'not_contains',
11
+ // TODO: Add support for:
12
+ // - between: lt and gt
13
+ // - begins with
14
+ }
15
+
16
+ /** Builds condition expression from a query for a table query, update and delete methods. */
17
+ const buildConditionExpression = (query: QueryMap) => {
18
+ // NOTE: https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Expressions.OperatorsAndFunctions.html
19
+ const ConditionExpressions = [] as string[];
20
+ const ExpressionAttributeNames = {} as Record<string, string>;
21
+ const ExpressionAttributeValues = {} as Record<string, unknown>;
22
+
23
+ for (let key in query) {
24
+ let path = '#Q_' + key.replace(/\./g, '.#Q_');
25
+
26
+ const valueKey = key.replace(/\.|:/g, '_');
27
+ const filterValue = query[key];
28
+
29
+ const isNot = key.endsWith(`:${ExpressionKey.NOT}`);
30
+ const isLessThan = key.endsWith(`:${ExpressionKey.LT}`);
31
+ const isLessThanOrEqual = key.endsWith(`:${ExpressionKey.LE}`);
32
+ const isGreaterThan = key.endsWith(`:${ExpressionKey.GT}`);
33
+ const isGreaterThanOrEqual = key.endsWith(`:${ExpressionKey.GE}`);
34
+ const isContains = key.endsWith(`:${ExpressionKey.CONTAIN}`);
35
+ const isNotContains = key.endsWith(`:${ExpressionKey.EXCLUDE}`);
36
+
37
+ const isFilterValueNull = filterValue === null;
38
+
39
+ if (isNot) {
40
+ key = key.replace(/:not/g, '');
41
+ }
42
+
43
+ if (isContains) {
44
+ key = key.replace(/:contains/g, '');
45
+ }
46
+
47
+ if (isNotContains) {
48
+ key = key.replace(/:not_contains/g, '');
49
+ }
50
+
51
+ if (isLessThan) {
52
+ key = key.replace(/:lt/g, '');
53
+ }
54
+
55
+ if (isLessThanOrEqual) {
56
+ key = key.replace(/:le/g, '');
57
+ }
58
+
59
+ if (isGreaterThan) {
60
+ key = key.replace(/:gt/g, '');
61
+ }
62
+
63
+ if (isGreaterThanOrEqual) {
64
+ key = key.replace(/:ge/g, '');
65
+ }
66
+
67
+ const pathKeys = key.split('.');
68
+
69
+ for (const pathKey of pathKeys) {
70
+ ExpressionAttributeNames[`#Q_${pathKey}`] = pathKey;
71
+ }
72
+
73
+ const isFilterValueArray = Array.isArray(filterValue);
74
+
75
+ if (isFilterValueArray) {
76
+ const filterValues = filterValue.entries();
77
+ const valueKeys = [];
78
+
79
+ for (const [ index, value ] of filterValues) {
80
+ ExpressionAttributeValues[`:Q_${valueKey}${index + 1}`] = value;
81
+ valueKeys.push(`:Q_${valueKey}${index + 1}`);
82
+ }
83
+
84
+ ConditionExpressions.push(`${path} in (${valueKeys})`);
85
+
86
+ } else {
87
+ ExpressionAttributeValues[`:Q_${valueKey}`] = filterValue;
88
+
89
+ if (isNot) {
90
+ path = path.replace(/:not/, '');
91
+ ConditionExpressions.push(`${path} <> :Q_${valueKey}`);
92
+
93
+ if (isFilterValueNull) {
94
+ ConditionExpressions.push(`attribute_exists(${path})`);
95
+ }
96
+
97
+ } else if (isContains) {
98
+ path = path.replace(/:contains/, '');
99
+ ConditionExpressions.push(`contains(${path}, :Q_${valueKey})`);
100
+
101
+ } else if (isNotContains) {
102
+ path = path.replace(/:not_contains/, '');
103
+ ConditionExpressions.push(`not contains(${path}, :Q_${valueKey})`);
104
+
105
+ } else if (isLessThan) {
106
+ path = path.replace(/:lt/, '');
107
+ ConditionExpressions.push(`${path} < :Q_${valueKey}`);
108
+
109
+ } else if (isLessThanOrEqual) {
110
+ path = path.replace(/:le/, '');
111
+ ConditionExpressions.push(`${path} <= :Q_${valueKey}`);
112
+
113
+ } else if (isGreaterThan) {
114
+ path = path.replace(/:gt/, '');
115
+ ConditionExpressions.push(`${path} > :Q_${valueKey}`);
116
+
117
+ } else if (isGreaterThanOrEqual) {
118
+ path = path.replace(/:ge/, '');
119
+ ConditionExpressions.push(`${path} >= :Q_${valueKey}`);
120
+
121
+ } else {
122
+ if (isFilterValueNull) {
123
+ ConditionExpressions.push(`(${path} = :Q_${valueKey} OR attribute_not_exists(${path}))`);
124
+
125
+ } else {
126
+ ConditionExpressions.push(`${path} = :Q_${valueKey}`);
127
+
128
+ }
129
+
130
+ }
131
+ }
132
+ }
133
+
134
+ const ConditionExpression = ConditionExpressions.join(' AND ');
135
+ const hasConditionExpression = ConditionExpression !== '';
136
+
137
+ if (!hasConditionExpression) {
138
+ return {
139
+ ExpressionAttributeNames,
140
+ ExpressionAttributeValues
141
+ };
142
+ }
143
+
144
+ return {
145
+ ConditionExpression,
146
+ ExpressionAttributeNames,
147
+ ExpressionAttributeValues,
148
+ };
149
+ };
150
+
151
+ export default buildConditionExpression;
@@ -0,0 +1,113 @@
1
+ import { TableOptions } from './getTableOptions';
2
+ import { got, type QueryMap } from '@kravc/dos';
3
+ import { type QueryCommandInput } from '@aws-sdk/lib-dynamodb';
4
+ import buildQueryConditionExpression from './buildQueryConditionExpression';
5
+
6
+ const QUERY_ERROR_TEMPLATE = 'Query parameter "$PATH" is required';
7
+
8
+ const SORT_ASC = 'asc';
9
+
10
+ /** Returns partitionKey and sortKey for a query. */
11
+ const getKeys = (
12
+ tableOptions: TableOptions,
13
+ indexName?: string
14
+ ): {
15
+ isGlobalSecondaryIndex: boolean;
16
+ partitionKey: string;
17
+ sortKey: string
18
+ } => {
19
+ const {
20
+ primaryKey,
21
+ localSecondaryIndexes,
22
+ globalSecondaryIndexes
23
+ } = tableOptions;
24
+
25
+ if (!indexName) {
26
+ return { ...primaryKey, isGlobalSecondaryIndex: false };
27
+ }
28
+
29
+ const lsi = localSecondaryIndexes.find(({ name }) => name === indexName);
30
+
31
+ if (lsi) {
32
+ const { sortKey } = lsi;
33
+
34
+ return { ...primaryKey, sortKey, isGlobalSecondaryIndex: false };
35
+ }
36
+
37
+ const gsi = globalSecondaryIndexes.find(({ name }) => name === indexName);
38
+
39
+ if (gsi) {
40
+ const { partitionKey, sortKey } = gsi;
41
+
42
+ return { partitionKey, sortKey, isGlobalSecondaryIndex: true };
43
+ }
44
+
45
+ const indexesJson = JSON.stringify({ localSecondaryIndexes, globalSecondaryIndexes }, null, 2);
46
+ throw new Error(`Index "${indexName}" is not defined, indexes: ${indexesJson}`);
47
+ };
48
+
49
+ /** Builds query command input. */
50
+ const buildQueryCommandInput = (
51
+ tableOptions: TableOptions,
52
+ query: QueryMap,
53
+ options: {
54
+ sort: string,
55
+ limit: number,
56
+ indexName?: string,
57
+ exclusiveStartKey?: string
58
+ }
59
+ ) => {
60
+ const { name: TableName } = tableOptions;
61
+
62
+ const {
63
+ sort,
64
+ limit,
65
+ indexName,
66
+ exclusiveStartKey
67
+ } = options;
68
+
69
+ const {
70
+ sortKey,
71
+ partitionKey,
72
+ isGlobalSecondaryIndex
73
+ } = getKeys(tableOptions, indexName);
74
+
75
+ got(query, partitionKey, QUERY_ERROR_TEMPLATE);
76
+
77
+ const {
78
+ ConditionExpression,
79
+ KeyConditionExpression,
80
+ ExpressionAttributeNames,
81
+ ExpressionAttributeValues
82
+ } = buildQueryConditionExpression(query, partitionKey, sortKey);
83
+
84
+ const Limit = limit;
85
+ const ConsistentRead = !isGlobalSecondaryIndex;
86
+ const ScanIndexForward = sort === SORT_ASC;
87
+
88
+ const input = {
89
+ TableName,
90
+ Limit,
91
+ ConsistentRead,
92
+ ScanIndexForward,
93
+ KeyConditionExpression,
94
+ ExpressionAttributeNames,
95
+ ExpressionAttributeValues
96
+ } as QueryCommandInput;
97
+
98
+ if (indexName) {
99
+ input.IndexName = indexName;
100
+ }
101
+
102
+ if (ConditionExpression) {
103
+ input.FilterExpression = ConditionExpression;
104
+ }
105
+
106
+ if (exclusiveStartKey) {
107
+ input.ExclusiveStartKey = JSON.parse(decodeURIComponent(exclusiveStartKey));
108
+ }
109
+
110
+ return input;
111
+ };
112
+
113
+ export default buildQueryCommandInput;
@@ -0,0 +1,109 @@
1
+ import type { QueryMap } from '@kravc/dos';
2
+ import buildConditionExpression from './buildConditionExpression';
3
+
4
+ export enum SortExpressionKey {
5
+ BW = 'bw',
6
+ LT = 'lt',
7
+ LE = 'le',
8
+ GT = 'gt',
9
+ GE = 'ge',
10
+ };
11
+
12
+ /** Builds condition expression for a table query. */
13
+ const buildQueryConditionExpression = (
14
+ query: QueryMap,
15
+ partitionKey: string,
16
+ sortKey: string,
17
+ ) => {
18
+ const {
19
+ [partitionKey]: partitionKeyValue,
20
+ [sortKey]: sortKeyValue,
21
+ [`${sortKey}:${SortExpressionKey.BW}`]: sortKeyBeginsWithValue,
22
+ [`${sortKey}:${SortExpressionKey.LT}`]: sortKeyLowerThanValue,
23
+ [`${sortKey}:${SortExpressionKey.LE}`]: sortKeyLowerThanOrEqualValue,
24
+ [`${sortKey}:${SortExpressionKey.GT}`]: sortKeyGreaterThanValue,
25
+ [`${sortKey}:${SortExpressionKey.GE}`]: sortKeyGreaterThanOrEqualValue,
26
+ ...conditionQuery
27
+ } = query;
28
+
29
+ const {
30
+ ConditionExpression,
31
+ ExpressionAttributeNames,
32
+ ExpressionAttributeValues,
33
+ } = buildConditionExpression(conditionQuery);
34
+
35
+ ExpressionAttributeNames[`#${partitionKey}`] = partitionKey;
36
+ ExpressionAttributeValues[`:${partitionKey}`] = partitionKeyValue;
37
+
38
+ const KeyConditionExpression = `#${partitionKey} = :${partitionKey}`;
39
+
40
+ const result = {
41
+ ConditionExpression,
42
+ KeyConditionExpression,
43
+ ExpressionAttributeNames,
44
+ ExpressionAttributeValues
45
+ };
46
+
47
+ const isBetweenValues = !!sortKeyLowerThanValue && !!sortKeyGreaterThanValue;
48
+
49
+ if (isBetweenValues) {
50
+ result.KeyConditionExpression += ` AND #${sortKey} BETWEEN :${sortKey}_gt AND :${sortKey}_lt`;
51
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
52
+ result.ExpressionAttributeValues[`:${sortKey}_lt`] = sortKeyLowerThanValue;
53
+ result.ExpressionAttributeValues[`:${sortKey}_gt`] = sortKeyGreaterThanValue;
54
+
55
+ return result;
56
+ }
57
+
58
+ if (sortKeyLowerThanValue) {
59
+ result.KeyConditionExpression += ` AND #${sortKey} < :${sortKey}_lt`;
60
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
61
+ result.ExpressionAttributeValues[`:${sortKey}_lt`] = sortKeyLowerThanValue;
62
+
63
+ return result;
64
+ }
65
+
66
+ if (sortKeyLowerThanOrEqualValue) {
67
+ result.KeyConditionExpression += ` AND #${sortKey} <= :${sortKey}_le`;
68
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
69
+ result.ExpressionAttributeValues[`:${sortKey}_le`] = sortKeyLowerThanOrEqualValue;
70
+
71
+ return result;
72
+ }
73
+
74
+ if (sortKeyGreaterThanValue) {
75
+ result.KeyConditionExpression += ` AND #${sortKey} > :${sortKey}_gt`;
76
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
77
+ result.ExpressionAttributeValues[`:${sortKey}_gt`] = sortKeyGreaterThanValue;
78
+
79
+ return result;
80
+ }
81
+
82
+ if (sortKeyGreaterThanOrEqualValue) {
83
+ result.KeyConditionExpression += ` AND #${sortKey} >= :${sortKey}_ge`;
84
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
85
+ result.ExpressionAttributeValues[`:${sortKey}_ge`] = sortKeyGreaterThanOrEqualValue;
86
+
87
+ return result;
88
+ }
89
+
90
+ if (sortKeyBeginsWithValue) {
91
+ result.KeyConditionExpression += ` AND begins_with(#${sortKey}, :${sortKey})`;
92
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
93
+ result.ExpressionAttributeValues[`:${sortKey}`] = sortKeyBeginsWithValue;
94
+
95
+ return result;
96
+ }
97
+
98
+ if (sortKeyValue) {
99
+ result.KeyConditionExpression += ` AND #${sortKey} = :${sortKey}`;
100
+ result.ExpressionAttributeNames[`#${sortKey}`] = sortKey;
101
+ result.ExpressionAttributeValues[`:${sortKey}`] = sortKeyValue;
102
+
103
+ return result;
104
+ }
105
+
106
+ return result;
107
+ };
108
+
109
+ export default buildQueryConditionExpression;
@@ -0,0 +1,151 @@
1
+ import { uniq } from 'lodash';
2
+ import type { TableOptions, PrimaryKey, LSI, GSI } from './getTableOptions';
3
+ import {
4
+ type CreateTableInput,
5
+ type LocalSecondaryIndex,
6
+ type GlobalSecondaryIndex
7
+ } from '@aws-sdk/client-dynamodb';
8
+
9
+ const ProvisionedThroughput = {
10
+ ReadCapacityUnits: 1,
11
+ WriteCapacityUnits: 1,
12
+ };
13
+
14
+ const Projection = {
15
+ ProjectionType: 'ALL'
16
+ };
17
+
18
+ /** Returns attribute definitions for table schema. */
19
+ const getAttributeDefinitions = (
20
+ primaryKey: PrimaryKey,
21
+ localSecondaryIndexes: LSI[],
22
+ globalSecondaryIndexes: GSI[]
23
+ ) => {
24
+ const { partitionKey, sortKey } = primaryKey;
25
+
26
+ let attributes = [ partitionKey, sortKey ];
27
+
28
+ for (const localIndexKey in localSecondaryIndexes) {
29
+ const { sortKey } = localSecondaryIndexes[localIndexKey];
30
+
31
+ attributes.push(sortKey);
32
+ }
33
+
34
+ for (const globalIndexKey in globalSecondaryIndexes) {
35
+ const { partitionKey, sortKey } = globalSecondaryIndexes[globalIndexKey];
36
+
37
+ attributes.push(partitionKey);
38
+
39
+ if (sortKey) {
40
+ attributes.push(sortKey);
41
+ }
42
+ }
43
+
44
+ attributes = uniq(attributes);
45
+
46
+ const AttributeDefinitions = attributes
47
+ .map(AttributeName => ({
48
+ // NOTE: Only composite keys are supported, no numbers.
49
+ AttributeType: 'S',
50
+ AttributeName,
51
+ }));
52
+
53
+ return AttributeDefinitions;
54
+ };
55
+
56
+ /** Returns secondary index. */
57
+ const getSecondaryIndex = (
58
+ IndexName: string,
59
+ partitionKey: string,
60
+ sortKey?: string
61
+ ) => {
62
+ const KeySchema = [
63
+ {
64
+ KeyType: 'HASH',
65
+ AttributeName: partitionKey
66
+ },
67
+ ];
68
+
69
+ if (sortKey) {
70
+ KeySchema.push({
71
+ KeyType: 'RANGE',
72
+ AttributeName: sortKey
73
+ });
74
+ }
75
+
76
+ const index = {
77
+ IndexName,
78
+ KeySchema,
79
+ Projection,
80
+ } as LocalSecondaryIndex | GlobalSecondaryIndex;
81
+
82
+ return index;
83
+ };
84
+
85
+ /** Returns schemas for local secondary indexes. */
86
+ const getLocalSecondaryIndexes = (
87
+ partitionKey: string,
88
+ localSecondaryIndexes: LSI[]
89
+ ): LocalSecondaryIndex[] => {
90
+ const indexes = localSecondaryIndexes
91
+ .map(({ name, sortKey }) => getSecondaryIndex(name, partitionKey, sortKey));
92
+
93
+ return indexes;
94
+ };
95
+
96
+ /** Returns schemas for global secondary indexes. */
97
+ const getGlobalSecondaryIndexes = (globalSecondaryIndexes: GSI[], isLocalhost: boolean) => {
98
+ const indexes = globalSecondaryIndexes
99
+ .map(({ name, partitionKey, sortKey }) =>
100
+ getSecondaryIndex(name, partitionKey, sortKey)
101
+ ) as GlobalSecondaryIndex[];
102
+
103
+ /* istanbul ignore next */
104
+ if (!isLocalhost) {
105
+ return indexes;
106
+ }
107
+
108
+ indexes.forEach(index => index.ProvisionedThroughput = ProvisionedThroughput);
109
+ return indexes;
110
+ };
111
+
112
+ /** Returns schema for a table from table options. */
113
+ const buildTableSchema = ({
114
+ name: TableName,
115
+ primaryKey,
116
+ isLocalhost,
117
+ localSecondaryIndexes,
118
+ globalSecondaryIndexes,
119
+ }: TableOptions): CreateTableInput => {
120
+ const { partitionKey, sortKey } = primaryKey;
121
+
122
+ const AttributeDefinitions = getAttributeDefinitions(primaryKey, localSecondaryIndexes, globalSecondaryIndexes);
123
+ const LocalSecondaryIndexes = getLocalSecondaryIndexes(partitionKey, localSecondaryIndexes);
124
+ const GlobalSecondaryIndexes = getGlobalSecondaryIndexes(globalSecondaryIndexes, isLocalhost);
125
+
126
+ const KeySchema = [
127
+ { KeyType: 'HASH', AttributeName: partitionKey },
128
+ { KeyType: 'RANGE', AttributeName: sortKey }
129
+ ];
130
+
131
+ const schema = {
132
+ TableName,
133
+ KeySchema,
134
+ AttributeDefinitions,
135
+ LocalSecondaryIndexes,
136
+ GlobalSecondaryIndexes,
137
+ } as CreateTableInput;
138
+
139
+ /* istanbul ignore else */
140
+ if (isLocalhost) {
141
+ schema.ProvisionedThroughput = ProvisionedThroughput;
142
+
143
+ } else {
144
+ schema.BillingMode = 'PAY_PER_REQUEST';
145
+
146
+ }
147
+
148
+ return schema;
149
+ };
150
+
151
+ export default buildTableSchema;
@@ -0,0 +1,95 @@
1
+ import type { QueryMap, MutationMap } from '@kravc/dos';
2
+ import buildConditionExpression from './buildConditionExpression';
3
+
4
+ /** Builds update expression from a query for a table item update method. */
5
+ const buildUpdateExpression = (query: QueryMap, mutation: MutationMap) => {
6
+ const UpdateExpressions = [] as string[];
7
+ const RemoveExpressions = [] as string[];
8
+
9
+ const {
10
+ ConditionExpression: _ConditionExpression,
11
+ ExpressionAttributeNames,
12
+ ExpressionAttributeValues,
13
+ } = buildConditionExpression(query);
14
+
15
+ let ConditionExpression = _ConditionExpression;
16
+
17
+ for (let name in mutation) {
18
+ const expressionValue = mutation[name];
19
+
20
+ const arrayItemIndex = name.match(/\[\d+\]/g);
21
+ name = name.replace(/\[\d+\]/g, '');
22
+
23
+ let path = '#' + name.replace(/\./g, '.#');
24
+
25
+ const valueKey = name.replace(/\.|:/g, '_');
26
+
27
+ const isAppend = name.endsWith(':append');
28
+ const isPrepend = name.endsWith(':prepend');
29
+
30
+ name = name.replace(/:append/, '');
31
+ name = name.replace(/:prepend/, '');
32
+
33
+ const pathKeys = name.split('.');
34
+
35
+ for (const pathKey of pathKeys) {
36
+ ExpressionAttributeNames[`#${pathKey}`] = pathKey;
37
+ }
38
+
39
+ const shouldRemoveAttribute = expressionValue === null;
40
+
41
+ if (shouldRemoveAttribute) {
42
+ RemoveExpressions.push(`${path}`);
43
+ continue;
44
+ }
45
+
46
+ if (isAppend || isPrepend) {
47
+ path = path.replace(/:append/, '');
48
+ path = path.replace(/:prepend/, '');
49
+
50
+ ExpressionAttributeValues[`:${valueKey}`] = [ expressionValue ];
51
+ ExpressionAttributeValues[`:${valueKey}_item`] = expressionValue;
52
+
53
+ ConditionExpression += ` AND not contains (${path}, :${valueKey}_item)`;
54
+
55
+ if (isAppend) {
56
+ UpdateExpressions.push(`${path} = list_append(#${name}, :${valueKey})`);
57
+
58
+ } else {
59
+ UpdateExpressions.push(`${path} = list_append(:${valueKey}, #${name})`);
60
+
61
+ }
62
+
63
+ } else {
64
+ ExpressionAttributeValues[`:${valueKey}`] = expressionValue;
65
+ path = `${path}${arrayItemIndex ? arrayItemIndex : ''}`;
66
+
67
+ UpdateExpressions.push(`${path} = :${valueKey}`);
68
+ }
69
+ }
70
+
71
+ const UpdatedExpressions = [];
72
+
73
+ const hasUpdateExpressions = UpdateExpressions.length > 0;
74
+
75
+ if (hasUpdateExpressions) {
76
+ UpdatedExpressions.push(`SET ${UpdateExpressions.join(', ')}`);
77
+ }
78
+
79
+ const hasRemoveAttributes = RemoveExpressions.length > 0;
80
+
81
+ if (hasRemoveAttributes) {
82
+ UpdatedExpressions.push(`REMOVE ${RemoveExpressions.join(', ')}`);
83
+ }
84
+
85
+ const UpdateExpression = UpdatedExpressions.join(' ');
86
+
87
+ return {
88
+ UpdateExpression,
89
+ ConditionExpression,
90
+ ExpressionAttributeNames,
91
+ ExpressionAttributeValues,
92
+ };
93
+ };
94
+
95
+ export default buildUpdateExpression;