@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,61 @@
|
|
|
1
|
+
import { DocumentWithHashId, ExpressionKey, SortExpressionKey } from '../src';
|
|
2
|
+
import { type QueryMap, type Context, type IndexAllOptions } from '@kravc/dos';
|
|
3
|
+
import { schema, OrganizationStatus, type OrganizationAttributes } from './OrganizationAttributes';
|
|
4
|
+
|
|
5
|
+
const PLATFORM_PARTITION = 'PLATFORM';
|
|
6
|
+
|
|
7
|
+
/** Organization document class. */
|
|
8
|
+
class Organization extends DocumentWithHashId<OrganizationAttributes> {
|
|
9
|
+
/** Returns a PLATFORM partition to save a document. */
|
|
10
|
+
static getPartition(): string {
|
|
11
|
+
return PLATFORM_PARTITION;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/** Returns active assets. */
|
|
15
|
+
static async listActiveOrganizations(
|
|
16
|
+
context: Context,
|
|
17
|
+
query: QueryMap = {},
|
|
18
|
+
options: IndexAllOptions = {}
|
|
19
|
+
) {
|
|
20
|
+
const status = OrganizationStatus.ACTIVE;
|
|
21
|
+
|
|
22
|
+
const {
|
|
23
|
+
indexName,
|
|
24
|
+
sortKeyName,
|
|
25
|
+
partitionKeyName,
|
|
26
|
+
} = this.getGlobalIndexProps('gsi1');
|
|
27
|
+
|
|
28
|
+
const indexQuery = {
|
|
29
|
+
[partitionKeyName]: PLATFORM_PARTITION,
|
|
30
|
+
[`${sortKeyName}:${SortExpressionKey.BW}`]: `#${status}`,
|
|
31
|
+
} as Record<string, string>;
|
|
32
|
+
|
|
33
|
+
return this.indexAll(context, { ...query, ...indexQuery }, { ...options, indexName });
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Disables an organization. */
|
|
37
|
+
static async disable(context: Context, query: QueryMap) {
|
|
38
|
+
query[`status:${ExpressionKey.NOT}`] = OrganizationStatus.DISABLED;
|
|
39
|
+
|
|
40
|
+
const mutation = {
|
|
41
|
+
status: OrganizationStatus.DISABLED,
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
return this.update(context, query, mutation);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Enables an organization. */
|
|
48
|
+
static async enable(context: Context, query: QueryMap) {
|
|
49
|
+
query[`status:${ExpressionKey.NOT}`] = OrganizationStatus.ACTIVE;
|
|
50
|
+
|
|
51
|
+
const mutation = {
|
|
52
|
+
status: OrganizationStatus.ACTIVE,
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return this.update(context, query, mutation);
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
Organization.schema = schema;
|
|
60
|
+
|
|
61
|
+
export default Organization;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { Schema } from '@kravc/schema';
|
|
2
|
+
import { DefaultAttributes } from '../src';
|
|
3
|
+
|
|
4
|
+
export enum OrganizationStatus {
|
|
5
|
+
ACTIVE = 'ACTIVE',
|
|
6
|
+
DISABLED = 'DISABLED'
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface OrganizationAttributes extends DefaultAttributes {
|
|
10
|
+
name: string;
|
|
11
|
+
status: OrganizationStatus;
|
|
12
|
+
number: number;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const schema = new Schema({
|
|
16
|
+
name: {
|
|
17
|
+
required: true
|
|
18
|
+
},
|
|
19
|
+
status: {
|
|
20
|
+
enum: [ OrganizationStatus.ACTIVE, OrganizationStatus.DISABLED ],
|
|
21
|
+
required: true,
|
|
22
|
+
},
|
|
23
|
+
number: {
|
|
24
|
+
type: 'integer',
|
|
25
|
+
minimum: 1,
|
|
26
|
+
required: true,
|
|
27
|
+
}
|
|
28
|
+
}, 'OrganizationBody');
|
package/example/index.ts
ADDED
package/jest.config.mjs
ADDED
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@kravc/dos-dynamodb",
|
|
3
|
+
"version": "1.0.0-alpha.0",
|
|
4
|
+
"description": "CRUD interface for DynamoDB table to be used with @kravc/dos service.",
|
|
5
|
+
"main": "dist/src/index.js",
|
|
6
|
+
"types": "dist/src/index.d.ts",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+ssh://git@github.com/alexkravets/dos-dynamodb.git"
|
|
10
|
+
},
|
|
11
|
+
"directories": {
|
|
12
|
+
"src": "src"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "eslint --fix src/ example/ && npm run db:up && sleep 2 && jest --coverage",
|
|
16
|
+
"prebuild": "rimraf dist",
|
|
17
|
+
"build": "tsc",
|
|
18
|
+
"prepare": "npm run build",
|
|
19
|
+
"prepublishOnly": "npm run build",
|
|
20
|
+
"db:up": "docker compose --project-name dos -f ./docker-compose.yaml up -d"
|
|
21
|
+
},
|
|
22
|
+
"bin": {
|
|
23
|
+
"table": "bin/table.js"
|
|
24
|
+
},
|
|
25
|
+
"author": "Oleksandr Kravets <alex@slatestudio.com>",
|
|
26
|
+
"license": "ISC",
|
|
27
|
+
"dependencies": {
|
|
28
|
+
"@aws-sdk/client-dynamodb": "^3.995.0",
|
|
29
|
+
"@aws-sdk/credential-providers": "^3.995.0",
|
|
30
|
+
"@aws-sdk/lib-dynamodb": "^3.995.0",
|
|
31
|
+
"@kravc/dos": "^2.0.0-alpha.12",
|
|
32
|
+
"config": "^4.3.0",
|
|
33
|
+
"hashids": "^2.3.0",
|
|
34
|
+
"lodash": "^4.17.23"
|
|
35
|
+
},
|
|
36
|
+
"devDependencies": {
|
|
37
|
+
"@eslint/js": "^10.0.1",
|
|
38
|
+
"@types/config": "^3.3.5",
|
|
39
|
+
"@types/jest": "^30.0.0",
|
|
40
|
+
"@types/lodash": "^4.17.23",
|
|
41
|
+
"eslint": "^10.0.1",
|
|
42
|
+
"eslint-plugin-jsdoc": "^62.7.0",
|
|
43
|
+
"globals": "^17.3.0",
|
|
44
|
+
"jest": "^30.2.0",
|
|
45
|
+
"rimraf": "^6.1.3",
|
|
46
|
+
"ts-jest": "^29.4.6",
|
|
47
|
+
"typescript": "^5.9.3",
|
|
48
|
+
"typescript-eslint": "^8.56.0"
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { DefaultAttributes as CommonAttributes } from '@kravc/dos';
|
|
2
|
+
|
|
3
|
+
export interface DefaultAttributes extends CommonAttributes {
|
|
4
|
+
_lsi1SortKey?: string;
|
|
5
|
+
_lsi2SortKey?: string;
|
|
6
|
+
_lsi3SortKey?: string;
|
|
7
|
+
|
|
8
|
+
_gsi1PartitionKey?: string;
|
|
9
|
+
_gsi1SortKey?: string;
|
|
10
|
+
|
|
11
|
+
_gsi2PartitionKey?: string;
|
|
12
|
+
_gsi2SortKey?: string;
|
|
13
|
+
|
|
14
|
+
_gsi3PartitionKey?: string;
|
|
15
|
+
_gsi3SortKey?: string;
|
|
16
|
+
}
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import config from 'config';
|
|
2
|
+
import { composeIndexKeys } from './helpers';
|
|
3
|
+
import { get, set, unset, cloneDeep } from 'lodash';
|
|
4
|
+
import Table, { type ItemAttributes, SortExpressionKey } from '../Table';
|
|
5
|
+
import {
|
|
6
|
+
type Context,
|
|
7
|
+
type QueryMap,
|
|
8
|
+
type MutationMap,
|
|
9
|
+
type IndexOptions,
|
|
10
|
+
type IndexAllOptions,
|
|
11
|
+
got,
|
|
12
|
+
MemoryDocument,
|
|
13
|
+
DocumentExistsError,
|
|
14
|
+
DocumentNotFoundError,
|
|
15
|
+
} from '@kravc/dos';
|
|
16
|
+
|
|
17
|
+
const DEFAULT_PARTITION_PATH = 'identity.organizationId';
|
|
18
|
+
|
|
19
|
+
const QUERY_ERROR_TEMPLATE = 'Query parameter "$PATH" is required';
|
|
20
|
+
const ATTRIBUTE_ERROR_TEMPLATE = 'Attribute "$PATH" is required';
|
|
21
|
+
|
|
22
|
+
const INDEX_ALL_LIMIT = 300;
|
|
23
|
+
const INDEX_LIMIT_MAX = 999;
|
|
24
|
+
|
|
25
|
+
/** Document class with DynamoDB database interface. */
|
|
26
|
+
class Document<T> extends MemoryDocument<T> {
|
|
27
|
+
/** Returns the table instance used to store a document. */
|
|
28
|
+
static get table(): Table {
|
|
29
|
+
return new Table({
|
|
30
|
+
sortKey: this.idKey,
|
|
31
|
+
partitionKey: this.partitionKey,
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Returns prefix for new document IDs. */
|
|
36
|
+
static get idPrefix(): string {
|
|
37
|
+
return config.get(`idPrefixes.${this.name}`);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Returns context path to get partition value from. */
|
|
41
|
+
static get partitionContextPath() {
|
|
42
|
+
return DEFAULT_PARTITION_PATH;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Returns the partition to save a document to. */
|
|
46
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
47
|
+
static getPartition(context: Context, _parameters: Record<string, unknown>): string {
|
|
48
|
+
const errorTemplate = 'Context is missing "$PATH", partition is not defined';
|
|
49
|
+
const partition = got(context, this.partitionContextPath, errorTemplate) as string;
|
|
50
|
+
|
|
51
|
+
return partition;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Defines limit maximum value for index action. */
|
|
55
|
+
static get indexLimitMax(): number {
|
|
56
|
+
return INDEX_LIMIT_MAX;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Returns sort key name for a local secondary index. */
|
|
60
|
+
static getLocalIndexSortKeyName(indexName: string): string {
|
|
61
|
+
return this.table.options.getLocalIndexSortKeyName(indexName);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/** Returns partition key name for a global secondary index. */
|
|
65
|
+
static getGlobalIndexPartitionKeyName(indexName: string): string {
|
|
66
|
+
return this.table.options.getGlobalIndexPartitionKeyName(indexName);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Returns sort key name for a global secondary index. */
|
|
70
|
+
static getGlobalIndexSortKeyName(indexName: string): string {
|
|
71
|
+
return this.table.options.getGlobalIndexSortKeyName(indexName);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Returns true if local index, false if global index. */
|
|
75
|
+
static isLocalSecondaryIndex(indexName: string): boolean {
|
|
76
|
+
return this.table.options.isLocalSecondaryIndex(indexName);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** Returns local index props. */
|
|
80
|
+
static getLocalIndexProps(indexName: string) {
|
|
81
|
+
return this.table.options.getLocalIndexProps(indexName);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/** Returns global index props. */
|
|
85
|
+
static getGlobalIndexProps(indexName: string) {
|
|
86
|
+
return this.table.options.getGlobalIndexProps(indexName);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Returns composed index keys. */
|
|
90
|
+
static composeIndexKeys(attributes: ItemAttributes) {
|
|
91
|
+
const idPrefix = this.idPrefix;
|
|
92
|
+
const documentName = this.name;
|
|
93
|
+
const tableOptions = this.table.options;
|
|
94
|
+
|
|
95
|
+
return composeIndexKeys({
|
|
96
|
+
idPrefix,
|
|
97
|
+
attributes,
|
|
98
|
+
documentName,
|
|
99
|
+
tableOptions,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/** Implements the interface to save a document, throws an error if document exists. */
|
|
104
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
105
|
+
static async _create<T>(attributes: T, _context: Context): Promise<void> {
|
|
106
|
+
got(attributes, this.idKey, ATTRIBUTE_ERROR_TEMPLATE);
|
|
107
|
+
got(attributes, this.partitionKey, ATTRIBUTE_ERROR_TEMPLATE);
|
|
108
|
+
|
|
109
|
+
const indexKeys = this.composeIndexKeys(attributes as ItemAttributes);
|
|
110
|
+
|
|
111
|
+
const itemAttributes = cloneDeep({ ...indexKeys, ...attributes }) as ItemAttributes;
|
|
112
|
+
|
|
113
|
+
const isCreated = await this.table.createItem(itemAttributes);
|
|
114
|
+
const isDocumentExistsError = !isCreated;
|
|
115
|
+
|
|
116
|
+
if (isDocumentExistsError) {
|
|
117
|
+
throw new DocumentExistsError(this, itemAttributes);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/** Implements the interface to read a document. */
|
|
122
|
+
static async _read<T>(query: QueryMap): Promise<T> {
|
|
123
|
+
got(query, this.idKey, QUERY_ERROR_TEMPLATE);
|
|
124
|
+
got(query, this.partitionKey, ATTRIBUTE_ERROR_TEMPLATE);
|
|
125
|
+
|
|
126
|
+
const item = await this.table.getItem(query) as T;
|
|
127
|
+
|
|
128
|
+
if (!item) {
|
|
129
|
+
throw new DocumentNotFoundError(this, query);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return cloneDeep(item);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Implements the interface to update a document. */
|
|
136
|
+
static async _update<T>(query: QueryMap, mutation: MutationMap, _context: Context, previousAttributes: T): Promise<T> {
|
|
137
|
+
got(query, this.idKey, QUERY_ERROR_TEMPLATE);
|
|
138
|
+
got(query, this.partitionKey, QUERY_ERROR_TEMPLATE);
|
|
139
|
+
|
|
140
|
+
const indexKeys = this.composeIndexKeys({ ...previousAttributes, ...mutation });
|
|
141
|
+
|
|
142
|
+
const item = await this.table.updateItem(query, { ...indexKeys, ...mutation });
|
|
143
|
+
|
|
144
|
+
if (!item) {
|
|
145
|
+
throw new DocumentNotFoundError(this, query);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
return cloneDeep(item) as T;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Implements the interface to delete a document. */
|
|
152
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
153
|
+
static async _delete<T>(query: QueryMap, _context: Context, _previousAttributes: T): Promise<void> {
|
|
154
|
+
got(query, this.idKey, QUERY_ERROR_TEMPLATE);
|
|
155
|
+
got(query, this.partitionKey, QUERY_ERROR_TEMPLATE);
|
|
156
|
+
|
|
157
|
+
const isDeleted = await this.table.deleteItem(query);
|
|
158
|
+
const isDocumentNotFoundError = !isDeleted;
|
|
159
|
+
|
|
160
|
+
if (isDocumentNotFoundError) {
|
|
161
|
+
throw new DocumentNotFoundError(this, query);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Returns query that ensures access to the documents collection only. */
|
|
166
|
+
static _getIndexQuery(query: QueryMap, { indexName }: { indexName?: string }): {
|
|
167
|
+
indexQuery: QueryMap,
|
|
168
|
+
indexName?: string
|
|
169
|
+
} {
|
|
170
|
+
const indexQuery = cloneDeep(query);
|
|
171
|
+
|
|
172
|
+
if (!indexName) {
|
|
173
|
+
indexQuery[`${this.idKey}:${SortExpressionKey.BW}`] = `${this.idPrefix}_`;
|
|
174
|
+
|
|
175
|
+
return { indexQuery, indexName };
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const isLocalSecondaryIndex = this.isLocalSecondaryIndex(indexName);
|
|
179
|
+
|
|
180
|
+
if (isLocalSecondaryIndex) {
|
|
181
|
+
return { indexQuery, indexName };
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const partitionKeyName = this.getGlobalIndexPartitionKeyName(indexName);
|
|
185
|
+
const partitionKey = get(indexQuery, partitionKeyName, QUERY_ERROR_TEMPLATE) as string;
|
|
186
|
+
|
|
187
|
+
set(indexQuery, partitionKeyName, `${this.idPrefix}#${partitionKey}`);
|
|
188
|
+
unset(indexQuery, this.partitionKey);
|
|
189
|
+
|
|
190
|
+
return { indexQuery, indexName };
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Implements the interface to get documents in batches. */
|
|
194
|
+
static async _index<T>(query: QueryMap, options: IndexOptions): Promise<{
|
|
195
|
+
limit: number;
|
|
196
|
+
count: number;
|
|
197
|
+
items: T[];
|
|
198
|
+
lastEvaluatedKey?: string;
|
|
199
|
+
}> {
|
|
200
|
+
const sort = get(options, 'sort', this.indexDefaultSort);
|
|
201
|
+
const limit = get(options, 'limit', this.indexDefaultLimit);
|
|
202
|
+
const exclusiveStartKey = get(options, 'exclusiveStartKey');
|
|
203
|
+
|
|
204
|
+
const { indexQuery, indexName } = this._getIndexQuery(query, options);
|
|
205
|
+
|
|
206
|
+
const { items, count, lastEvaluatedKey } = await this.table.listItems(indexQuery, {
|
|
207
|
+
sort,
|
|
208
|
+
limit,
|
|
209
|
+
indexName,
|
|
210
|
+
exclusiveStartKey,
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
return {
|
|
214
|
+
items: items as T[],
|
|
215
|
+
limit,
|
|
216
|
+
count,
|
|
217
|
+
lastEvaluatedKey,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Implements the interface to get all documents. */
|
|
222
|
+
static async _indexAll<T>(
|
|
223
|
+
query: QueryMap,
|
|
224
|
+
options: IndexAllOptions
|
|
225
|
+
): Promise<{
|
|
226
|
+
count: number;
|
|
227
|
+
items: T[];
|
|
228
|
+
}> {
|
|
229
|
+
let resultItems = [] as T[];
|
|
230
|
+
let exclusiveStartKey: string | undefined;
|
|
231
|
+
|
|
232
|
+
const limit = INDEX_ALL_LIMIT;
|
|
233
|
+
const indexOptions = { limit, ...options } as IndexOptions;
|
|
234
|
+
|
|
235
|
+
do {
|
|
236
|
+
if (exclusiveStartKey) {
|
|
237
|
+
indexOptions.exclusiveStartKey = exclusiveStartKey;
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const { items, lastEvaluatedKey: nextExclusiveStartKey } =
|
|
241
|
+
await this._index(query, indexOptions);
|
|
242
|
+
|
|
243
|
+
resultItems = [ ...resultItems, ...(items as T[]) ];
|
|
244
|
+
exclusiveStartKey = nextExclusiveStartKey;
|
|
245
|
+
|
|
246
|
+
} while (exclusiveStartKey);
|
|
247
|
+
|
|
248
|
+
const count = resultItems.length;
|
|
249
|
+
|
|
250
|
+
return {
|
|
251
|
+
items: resultItems,
|
|
252
|
+
count
|
|
253
|
+
};
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
export default Document;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import Hashids from 'hashids';
|
|
2
|
+
import Document from './Document';
|
|
3
|
+
import { get, set } from 'lodash';
|
|
4
|
+
import { wait, DocumentExistsError, type QueryMap, type Context } from '@kravc/dos';
|
|
5
|
+
|
|
6
|
+
const CHARACTER_SET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
|
|
7
|
+
const DEFAULT_INDEX_NAME = 'lsi1';
|
|
8
|
+
const DEFAULT_NUMBER_KEY = 'number';
|
|
9
|
+
|
|
10
|
+
/** Document class that uses hash ID as id key. */
|
|
11
|
+
class DocumentWithHashId<T> extends Document<T> {
|
|
12
|
+
/** Returns salt for hash ID method. */
|
|
13
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
14
|
+
static getSalt<T>(_context: Context, _attributes: T): string {
|
|
15
|
+
return this.name;
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
/** Returns number key. */
|
|
19
|
+
static get numberKey() {
|
|
20
|
+
return DEFAULT_NUMBER_KEY;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Ensures index query without index name uses lsi1 to sort documents. */
|
|
24
|
+
static _getIndexQuery(query: QueryMap, { indexName }: { indexName?: string }) {
|
|
25
|
+
return indexName
|
|
26
|
+
? super._getIndexQuery(query, { indexName })
|
|
27
|
+
: super._getIndexQuery(query, { indexName: DEFAULT_INDEX_NAME });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Returns number for the next document. */
|
|
31
|
+
static async _getNextDocumentNumber(context: Context) {
|
|
32
|
+
const query = {};
|
|
33
|
+
const options = { limit: 1, sort: this.indexDefaultSort };
|
|
34
|
+
|
|
35
|
+
const { objects: documents } = await this.index(context, query, options);
|
|
36
|
+
const [ previousDocument ] = documents;
|
|
37
|
+
|
|
38
|
+
if (!previousDocument) {
|
|
39
|
+
return 1;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const number = get(previousDocument.attributes, this.numberKey, 0 );
|
|
43
|
+
|
|
44
|
+
return number + 1;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Ensures creation of a document with generated ID. */
|
|
48
|
+
static async _create<T>(attributes: T, context: Context): Promise<void> {
|
|
49
|
+
let number = get(attributes, this.numberKey) as number | undefined;
|
|
50
|
+
|
|
51
|
+
if (!number) {
|
|
52
|
+
number = await this._getNextDocumentNumber(context);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const salt = this.getSalt(context, attributes);
|
|
56
|
+
const hashids = new Hashids(salt, 0, CHARACTER_SET);
|
|
57
|
+
const hashId = hashids.encode(number);
|
|
58
|
+
|
|
59
|
+
const id = `${this.idPrefix}${hashId}`;
|
|
60
|
+
|
|
61
|
+
set(attributes as Record<string, unknown>, this.numberKey, number);
|
|
62
|
+
set(attributes as Record<string, unknown>, this.idKey, id);
|
|
63
|
+
|
|
64
|
+
try {
|
|
65
|
+
await super._create(attributes, context);
|
|
66
|
+
return;
|
|
67
|
+
|
|
68
|
+
} catch (error) {
|
|
69
|
+
const isDocumentExistsError = error instanceof DocumentExistsError;
|
|
70
|
+
|
|
71
|
+
/* istanbul ignore next */
|
|
72
|
+
if (!isDocumentExistsError) {
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await wait(50);
|
|
78
|
+
|
|
79
|
+
set(attributes as Record<string, unknown>, this.numberKey, number! + 1);
|
|
80
|
+
|
|
81
|
+
return this._create(attributes, context);
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
export default DocumentWithHashId;
|