@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,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');
@@ -0,0 +1,9 @@
1
+ import Asset from './Asset';
2
+ import Activity from './Activity';
3
+ import Organization from './Organization';
4
+
5
+ export {
6
+ Asset,
7
+ Activity,
8
+ Organization,
9
+ };
@@ -0,0 +1,10 @@
1
+ export default {
2
+ bail: true,
3
+ preset: 'ts-jest',
4
+ testMatch: ['<rootDir>/src/**/*.test.ts'],
5
+ maxWorkers: 1,
6
+ collectCoverageFrom: [
7
+ '<rootDir>/src/**/*.ts',
8
+ '<rootDir>/example/**/*.ts'
9
+ ],
10
+ };
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;