@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.
- package/README.md +30 -0
- package/bin/table.js +88 -0
- package/config/default.yaml +36 -0
- package/config/test.yaml +0 -0
- package/dist/example/Activity.d.ts +34 -0
- package/dist/example/Activity.d.ts.map +1 -0
- package/dist/example/Activity.js +66 -0
- package/dist/example/Activity.js.map +1 -0
- package/dist/example/ActivityAttributes.d.ts +23 -0
- package/dist/example/ActivityAttributes.d.ts.map +1 -0
- package/dist/example/ActivityAttributes.js +54 -0
- package/dist/example/ActivityAttributes.js.map +1 -0
- package/dist/example/Asset.d.ts +36 -0
- package/dist/example/Asset.d.ts.map +1 -0
- package/dist/example/Asset.js +56 -0
- package/dist/example/Asset.js.map +1 -0
- package/dist/example/AssetAttributes.d.ts +28 -0
- package/dist/example/AssetAttributes.d.ts.map +1 -0
- package/dist/example/AssetAttributes.js +72 -0
- package/dist/example/AssetAttributes.js.map +1 -0
- package/dist/example/Organization.d.ts +19 -0
- package/dist/example/Organization.d.ts.map +1 -0
- package/dist/example/Organization.js +42 -0
- package/dist/example/Organization.js.map +1 -0
- package/dist/example/OrganizationAttributes.d.ts +13 -0
- package/dist/example/OrganizationAttributes.d.ts.map +1 -0
- package/dist/example/OrganizationAttributes.js +24 -0
- package/dist/example/OrganizationAttributes.js.map +1 -0
- package/dist/example/index.d.ts +5 -0
- package/dist/example/index.d.ts.map +1 -0
- package/dist/example/index.js +13 -0
- package/dist/example/index.js.map +1 -0
- package/dist/src/Document/Document.d.ts +67 -0
- package/dist/src/Document/Document.d.ts.map +1 -0
- package/dist/src/Document/Document.js +216 -0
- package/dist/src/Document/Document.js.map +1 -0
- package/dist/src/Document/DocumentWithHashId.d.ts +22 -0
- package/dist/src/Document/DocumentWithHashId.d.ts.map +1 -0
- package/dist/src/Document/DocumentWithHashId.js +73 -0
- package/dist/src/Document/DocumentWithHashId.js.map +1 -0
- package/dist/src/Document/__tests__/__helpers.d.ts +21 -0
- package/dist/src/Document/__tests__/__helpers.d.ts.map +1 -0
- package/dist/src/Document/__tests__/__helpers.js +92 -0
- package/dist/src/Document/__tests__/__helpers.js.map +1 -0
- package/dist/src/Document/helpers/composeIndexKeys.d.ts +11 -0
- package/dist/src/Document/helpers/composeIndexKeys.d.ts.map +1 -0
- package/dist/src/Document/helpers/composeIndexKeys.js +81 -0
- package/dist/src/Document/helpers/composeIndexKeys.js.map +1 -0
- package/dist/src/Document/helpers/index.d.ts +3 -0
- package/dist/src/Document/helpers/index.d.ts.map +1 -0
- package/dist/src/Document/helpers/index.js +9 -0
- package/dist/src/Document/helpers/index.js.map +1 -0
- package/dist/src/Table/Table.d.ts +56 -0
- package/dist/src/Table/Table.d.ts.map +1 -0
- package/dist/src/Table/Table.js +228 -0
- package/dist/src/Table/Table.js.map +1 -0
- package/dist/src/Table/helpers/buildConditionExpression.d.ts +22 -0
- package/dist/src/Table/helpers/buildConditionExpression.d.ts.map +1 -0
- package/dist/src/Table/helpers/buildConditionExpression.js +128 -0
- package/dist/src/Table/helpers/buildConditionExpression.js.map +1 -0
- package/dist/src/Table/helpers/buildQueryCommandInput.d.ts +12 -0
- package/dist/src/Table/helpers/buildQueryCommandInput.d.ts.map +1 -0
- package/dist/src/Table/helpers/buildQueryCommandInput.js +60 -0
- package/dist/src/Table/helpers/buildQueryCommandInput.js.map +1 -0
- package/dist/src/Table/helpers/buildQueryConditionExpression.d.ts +17 -0
- package/dist/src/Table/helpers/buildQueryConditionExpression.d.ts.map +1 -0
- package/dist/src/Table/helpers/buildQueryConditionExpression.js +77 -0
- package/dist/src/Table/helpers/buildQueryConditionExpression.js.map +1 -0
- package/dist/src/Table/helpers/buildTableSchema.d.ts +6 -0
- package/dist/src/Table/helpers/buildTableSchema.d.ts.map +1 -0
- package/dist/src/Table/helpers/buildTableSchema.js +100 -0
- package/dist/src/Table/helpers/buildTableSchema.js.map +1 -0
- package/dist/src/Table/helpers/buildUpdateExpression.d.ts +10 -0
- package/dist/src/Table/helpers/buildUpdateExpression.d.ts.map +1 -0
- package/dist/src/Table/helpers/buildUpdateExpression.js +69 -0
- package/dist/src/Table/helpers/buildUpdateExpression.js.map +1 -0
- package/dist/src/Table/helpers/filterConditionExpression.d.ts +5 -0
- package/dist/src/Table/helpers/filterConditionExpression.d.ts.map +1 -0
- package/dist/src/Table/helpers/filterConditionExpression.js +68 -0
- package/dist/src/Table/helpers/filterConditionExpression.js.map +1 -0
- package/dist/src/Table/helpers/getRawClientConfig.d.ts +5 -0
- package/dist/src/Table/helpers/getRawClientConfig.d.ts.map +1 -0
- package/dist/src/Table/helpers/getRawClientConfig.js +29 -0
- package/dist/src/Table/helpers/getRawClientConfig.js.map +1 -0
- package/dist/src/Table/helpers/getTableOptions.d.ts +51 -0
- package/dist/src/Table/helpers/getTableOptions.d.ts.map +1 -0
- package/dist/src/Table/helpers/getTableOptions.js +144 -0
- package/dist/src/Table/helpers/getTableOptions.js.map +1 -0
- package/dist/src/Table/helpers/index.d.ts +10 -0
- package/dist/src/Table/helpers/index.d.ts.map +1 -0
- package/dist/src/Table/helpers/index.js +21 -0
- package/dist/src/Table/helpers/index.js.map +1 -0
- package/dist/src/Table/index.d.ts +8 -0
- package/dist/src/Table/index.d.ts.map +1 -0
- package/dist/src/Table/index.js +13 -0
- package/dist/src/Table/index.js.map +1 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -0
- package/dist/src/index.js +14 -0
- package/dist/src/index.js.map +1 -0
- package/docker-compose.yaml +10 -0
- package/eslint.config.mjs +35 -0
- package/example/Activity.ts +123 -0
- package/example/ActivityAttributes.ts +72 -0
- package/example/Asset.ts +78 -0
- package/example/AssetAttributes.ts +87 -0
- package/example/Organization.ts +61 -0
- package/example/OrganizationAttributes.ts +28 -0
- package/example/index.ts +9 -0
- package/jest.config.mjs +10 -0
- package/package.json +50 -0
- package/src/Document/DefaultAttributes.d.ts +16 -0
- package/src/Document/Document.ts +257 -0
- package/src/Document/DocumentWithHashId.ts +85 -0
- package/src/Document/__tests__/Document.test.ts +596 -0
- package/src/Document/__tests__/DocumentWithHashId.test.ts +81 -0
- package/src/Document/__tests__/__helpers.ts +115 -0
- package/src/Document/helpers/__tests__/composeIndexKeys.test.ts +40 -0
- package/src/Document/helpers/composeIndexKeys.ts +137 -0
- package/src/Document/helpers/index.ts +5 -0
- package/src/Table/Table.ts +354 -0
- package/src/Table/__tests__/Table.test.ts +64 -0
- package/src/Table/helpers/__tests__/buildQueryCommandInput.test.ts +14 -0
- package/src/Table/helpers/__tests__/buildTableSchema.test.ts +19 -0
- package/src/Table/helpers/buildConditionExpression.ts +151 -0
- package/src/Table/helpers/buildQueryCommandInput.ts +113 -0
- package/src/Table/helpers/buildQueryConditionExpression.ts +109 -0
- package/src/Table/helpers/buildTableSchema.ts +151 -0
- package/src/Table/helpers/buildUpdateExpression.ts +95 -0
- package/src/Table/helpers/filterConditionExpression.ts +87 -0
- package/src/Table/helpers/getRawClientConfig.ts +35 -0
- package/src/Table/helpers/getTableOptions.ts +228 -0
- package/src/Table/helpers/index.ts +21 -0
- package/src/Table/index.ts +18 -0
- package/src/index.ts +15 -0
- 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;
|