triangle-utils 1.2.12 → 1.3.1

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.
@@ -0,0 +1,448 @@
1
+ import { AttributeValue, DynamoDB, QueryCommandInput, QueryCommandOutput, ScanCommandInput, ScanCommandOutput, UpdateItemCommandInput } from "@aws-sdk/client-dynamodb"
2
+ import { Config } from "./types/Config"
3
+
4
+ function convert_output(dynamoobject : AttributeValue) : string | boolean | number | any[] | Set<any> | Record<string, any> | undefined {
5
+ if (dynamoobject.S !== undefined) {
6
+ return dynamoobject.S
7
+ } else if (dynamoobject.N !== undefined) {
8
+ return Number(dynamoobject.N)
9
+ } else if (dynamoobject.L !== undefined) {
10
+ return dynamoobject.L.map((a : AttributeValue) => convert_output(a))
11
+ } else if (dynamoobject.SS !== undefined) {
12
+ return new Set(dynamoobject.SS)
13
+ } else if (dynamoobject.M !== undefined) {
14
+ return Object.fromEntries(Object.entries(dynamoobject.M).map(([key, value]) => [key, convert_output(value)]))
15
+ } else if (dynamoobject.BOOL !== undefined) {
16
+ return dynamoobject.BOOL
17
+ }
18
+ return undefined
19
+ }
20
+
21
+ function not_undefined<T>(x : T | undefined): x is T {
22
+ return x !== undefined
23
+ }
24
+
25
+ function is_item(x : any): x is Record<string, any> {
26
+ return typeof x === "object" && Object.keys(x).filter(key => typeof key !== "string").length === 0 && !Array.isArray(x)
27
+ }
28
+
29
+ function convert_input(input : string | boolean | number | any[] | Set<any> | Record<string, any>) : AttributeValue | undefined {
30
+ if (typeof input === "string") {
31
+ return { S : input }
32
+ } else if (typeof input === "boolean") {
33
+ return { BOOL : input }
34
+ } else if (typeof input === "number") {
35
+ return { N : input.toString() }
36
+ } else if (Array.isArray(input)) {
37
+ const converted_list : AttributeValue[] = input.map((a : any) => convert_input(a))
38
+ .filter(converted_input => converted_input !== undefined)
39
+ if (converted_list.length !== input.length) {
40
+ return undefined
41
+ }
42
+ return { L : converted_list }
43
+ } else if (input instanceof Set) {
44
+ const converted_list : string[] = Array.from(input)
45
+ .filter(converted_input => typeof converted_input === "string")
46
+ if (converted_list.length !== input.size) {
47
+ return undefined
48
+ }
49
+ return { SS : converted_list }
50
+ } else {
51
+ const converted_inputs : Record<string, AttributeValue> = Object.fromEntries(Object.entries(input)
52
+ .filter(([key, value]) => value !== undefined && value !== null && key !== "")
53
+ .map(([key, value]) => [key, convert_input(value)])
54
+ .filter(([key, value]) => not_undefined(value))
55
+ )
56
+ return {
57
+ M : converted_inputs
58
+ }
59
+ }
60
+ }
61
+
62
+ async function compile_pages(
63
+ request : ScanCommandInput|QueryCommandInput,
64
+ f : (request : ScanCommandInput|QueryCommandInput) => Promise<ScanCommandOutput|QueryCommandOutput>,
65
+ compile : boolean = true
66
+ ) : Promise<any[]> {
67
+ const items = []
68
+ let last_eval_key : Record<string, AttributeValue> | undefined = undefined
69
+ while (true) {
70
+ const request_page : ScanCommandInput|QueryCommandInput = {
71
+ ...request,
72
+ ExclusiveStartKey : last_eval_key
73
+ }
74
+ const response = await f(request_page)
75
+ if (response.Items === undefined) {
76
+ return []
77
+ }
78
+ const new_items = response.Items.map(item => convert_output({ M : item }))
79
+ items.push(...new_items)
80
+ if (response.LastEvaluatedKey === undefined || !compile) {
81
+ return items
82
+ }
83
+ last_eval_key = response.LastEvaluatedKey
84
+ }
85
+ }
86
+
87
+ export class Utils_DynamoDB {
88
+
89
+ readonly dynamodb : DynamoDB
90
+
91
+ constructor(config : Config) {
92
+ this.dynamodb = new DynamoDB({ region : config.region })
93
+ }
94
+
95
+ async scan(
96
+ table : string,
97
+ options : {
98
+ filters? : Record<string, any>,
99
+ undefined_attribute_names? : string[],
100
+ defined_attribute_names? : string[],
101
+ attribute_names? : string[],
102
+ concurrency? : number
103
+ } = {}
104
+ ) : Promise<Record<string, any>[]> {
105
+ const filters = options.filters !== undefined ? options.filters : {}
106
+ const undefined_attribute_names : string[] = options.undefined_attribute_names !== undefined ? options.undefined_attribute_names : []
107
+ const defined_attribute_names : string[] = options.defined_attribute_names !== undefined ? options.defined_attribute_names : []
108
+ const concurrency : number = options.concurrency !== undefined ? options.concurrency : 1
109
+ const attribute_names : string[] = options.attribute_names !== undefined ? options.attribute_names : []
110
+ const iterators = []
111
+ for (let i = 0; i < concurrency; i++) {
112
+ const expression_attribute_names = Object.fromEntries(
113
+ [...Object.keys(filters), ...attribute_names, ...undefined_attribute_names, ...defined_attribute_names]
114
+ .map(attribute_name => ["#" + attribute_name, attribute_name])
115
+ )
116
+ const expression_attribute_values = Object.fromEntries(
117
+ Object.entries(filters)
118
+ .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)])
119
+ .filter(([attribute_name, attribute_value]) => not_undefined(attribute_value))
120
+ )
121
+ const filter_expression = [
122
+ ...Object.keys(filters).map(attribute_name => "#" + attribute_name + " = :" + attribute_name),
123
+ ...undefined_attribute_names.map(attribute_name => "attribute_not_exists(#" + attribute_name + ")"),
124
+ ...defined_attribute_names.map(attribute_name => "attribute_exists(#" + attribute_name + ")")
125
+ ].join(" AND ")
126
+ const projection_expression = attribute_names.map(attribute_name => "#" + attribute_name).join(", ")
127
+ const request : ScanCommandInput = {
128
+ TableName : table,
129
+ ExpressionAttributeNames : Object.keys(expression_attribute_names).length > 0 ? expression_attribute_names : undefined,
130
+ ExpressionAttributeValues : Object.keys(expression_attribute_values).length > 0 ? expression_attribute_values : undefined,
131
+ FilterExpression : filter_expression.length > 0 ? filter_expression : undefined,
132
+ ProjectionExpression : projection_expression.length > 0 ? projection_expression : undefined,
133
+ Segment : i,
134
+ TotalSegments : concurrency
135
+ }
136
+ iterators.push(compile_pages(request, (request : ScanCommandInput) => this.dynamodb.scan(request)))
137
+ }
138
+ const segments = await Promise.all(iterators)
139
+ const items = segments.flat().filter(is_item)
140
+ return items
141
+ }
142
+
143
+ async get(
144
+ table : string,
145
+ key : Record<string, any>,
146
+ consistent : boolean = false
147
+ ) : Promise<Record<string, any> | undefined> {
148
+ const converted_key = convert_input(key)?.M
149
+ if (converted_key === undefined) {
150
+ return undefined
151
+ }
152
+ const item = await this.dynamodb.getItem({
153
+ ConsistentRead : consistent,
154
+ TableName : table,
155
+ Key : converted_key
156
+ })
157
+ .then(response => response.Item)
158
+ if (item === undefined) {
159
+ return undefined
160
+ }
161
+ const converted_output = convert_output({ M : item })
162
+ if (!is_item(converted_output)) {
163
+ return undefined
164
+ }
165
+ return converted_output
166
+ }
167
+
168
+ async get_max(table : string, primary_key : Record<string, any>) : Promise<Record<string, any> | undefined> {
169
+ if (Object.keys(primary_key).length !== 1) {
170
+ return undefined
171
+ }
172
+ const key = Object.keys(primary_key)[0]
173
+ const value = convert_input(Object.values(primary_key)[0])
174
+ if (value === undefined) {
175
+ return undefined
176
+ }
177
+ const request : QueryCommandInput = {
178
+ TableName : table,
179
+ ExpressionAttributeNames: {
180
+ "#a": key
181
+ },
182
+ ExpressionAttributeValues: {
183
+ ":a": value
184
+ },
185
+ KeyConditionExpression: "#a = :a",
186
+ Limit : 1,
187
+ ScanIndexForward : false
188
+ }
189
+ const items = await this.dynamodb.query(request)
190
+ .then(response => response.Items)
191
+ if (items === undefined || items[0] === undefined) {
192
+ return undefined
193
+ }
194
+ const converted_output = convert_output({ M : items[0] })
195
+ if (!is_item(converted_output)) {
196
+ return undefined
197
+ }
198
+ return converted_output
199
+ }
200
+
201
+ async query(
202
+ table : string,
203
+ primary_key : Record<string, any>,
204
+ options : {
205
+ reverse? : boolean,
206
+ compile? : boolean
207
+ } = {}
208
+ ) : Promise<Record<string, any>[] | undefined>{
209
+ const reverse = options.reverse !== undefined ? options.reverse : false
210
+ const compile = options.compile !== undefined ? options.compile : true
211
+ if (Object.keys(primary_key).length !== 1) {
212
+ return undefined
213
+ }
214
+ const key = Object.keys(primary_key)[0]
215
+ const value = convert_input(Object.values(primary_key)[0])
216
+ if (value === undefined) {
217
+ return undefined
218
+ }
219
+ const request : QueryCommandInput = {
220
+ TableName : table,
221
+ ExpressionAttributeNames: {
222
+ "#a": key
223
+ },
224
+ ExpressionAttributeValues: {
225
+ ":a": value
226
+ },
227
+ KeyConditionExpression: "#a = :a",
228
+ ScanIndexForward : !reverse
229
+ }
230
+ return await compile_pages(request, (request) => this.dynamodb.query(request), compile)
231
+ }
232
+
233
+ async query_prefix(
234
+ table : string,
235
+ primary_key : Record<string, any>,
236
+ secondary_key_prefix : Record<string, string>,
237
+ options : {
238
+ reverse? : boolean,
239
+ compile? : boolean
240
+ } = {}
241
+ ) : Promise<Record<string, any>[] | undefined>{
242
+ const reverse = options.reverse !== undefined ? options.reverse : false
243
+ const compile = options.compile !== undefined ? options.compile : true
244
+ if (Object.keys(primary_key).length !== 1 || Object.keys(secondary_key_prefix).length !== 1) {
245
+ return undefined
246
+ }
247
+ const converted_primary_value = convert_input(Object.values(primary_key)[0])
248
+ const converted_secondary_prefix_value = convert_input(Object.values(secondary_key_prefix)[0])
249
+ if (converted_primary_value === undefined || converted_secondary_prefix_value === undefined) {
250
+ return undefined
251
+ }
252
+ const request : QueryCommandInput = {
253
+ TableName : table,
254
+ ExpressionAttributeNames: {
255
+ "#a": Object.keys(primary_key)[0],
256
+ "#b": Object.keys(secondary_key_prefix)[0]
257
+ },
258
+ ExpressionAttributeValues: {
259
+ ":a": converted_primary_value,
260
+ ":b": converted_secondary_prefix_value
261
+ },
262
+ KeyConditionExpression: "#a = :a AND begins_with(#b, :b)",
263
+ ScanIndexForward : !reverse
264
+ }
265
+ return await compile_pages(request, (request) => this.dynamodb.query(request), compile)
266
+ }
267
+
268
+ async query_range(
269
+ table : string,
270
+ primary_key : Record<string, any>,
271
+ secondary_key_range : Record<string, string|number[]>,
272
+ options : {
273
+ reverse? : boolean,
274
+ compile? : boolean
275
+ } = {}
276
+ ) : Promise<Record<string, any>[] | undefined>{
277
+ const reverse = options.reverse !== undefined ? options.reverse : false
278
+ const compile = options.compile !== undefined ? options.compile : true
279
+ if (Object.keys(primary_key).length !== 1 || Object.keys(secondary_key_range).length !== 1 || Object.values(secondary_key_range)[0].length !== 2) {
280
+ return undefined
281
+ }
282
+ const converted_primary_value = convert_input(Object.values(primary_key)[0])
283
+ const converted_secondary_range_start_value = convert_input(Object.values(secondary_key_range)[0][0])
284
+ const converted_secondary_range_end_value = convert_input(Object.values(secondary_key_range)[0][1])
285
+ if (converted_primary_value === undefined || converted_secondary_range_start_value === undefined || converted_secondary_range_end_value === undefined) {
286
+ return undefined
287
+ }
288
+ const request : QueryCommandInput = {
289
+ TableName : table,
290
+ ExpressionAttributeNames: {
291
+ "#a": Object.keys(primary_key)[0],
292
+ "#b": Object.keys(secondary_key_range)[0]
293
+ },
294
+ ExpressionAttributeValues: {
295
+ ":a": converted_primary_value,
296
+ ":b1": converted_secondary_range_start_value,
297
+ ":b2": converted_secondary_range_end_value
298
+ },
299
+ KeyConditionExpression: "#a = :a AND (#b BETWEEN :b1 AND :b2)",
300
+ ScanIndexForward : !reverse
301
+ }
302
+ return await compile_pages(request, (request) => this.dynamodb.query(request), compile)
303
+ }
304
+
305
+ async set(table : string, key : Record<string, any>, attributes : Record<string, any>) : Promise<void> {
306
+ const converted_key = convert_input(key)?.M
307
+
308
+ const request : UpdateItemCommandInput = {
309
+ TableName : table,
310
+ Key : converted_key,
311
+ UpdateExpression: "set " + Object.keys(attributes)
312
+ .filter(attribute_name => !Object.keys(key).includes(attribute_name))
313
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
314
+ .map(attribute_name => "#" + attribute_name + " = :" + attribute_name
315
+ ).join(", "),
316
+ ExpressionAttributeNames: Object.fromEntries(Object.keys(attributes)
317
+ .filter(attribute_name => !Object.keys(key).includes(attribute_name))
318
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
319
+ .map(attribute_name => ["#" + attribute_name, attribute_name]
320
+ )),
321
+ ExpressionAttributeValues: Object.fromEntries(Object.entries(attributes)
322
+ .filter(([attribute_name, attribute_value]) => !Object.keys(key).includes(attribute_name) && not_undefined(attribute_value))
323
+ .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)])
324
+ .filter(([attribute_name, attribute_value]) => not_undefined(attribute_value))
325
+ )
326
+ }
327
+ await this.dynamodb.updateItem(request)
328
+ }
329
+
330
+ async append(table : string, key : Record<string, any>, attributes : Record<string, any[]>) : Promise<void>{
331
+ const converted_key = convert_input(key)?.M
332
+
333
+ const request : UpdateItemCommandInput = {
334
+ TableName : table,
335
+ Key : converted_key,
336
+ UpdateExpression: "set " + Object.keys(attributes)
337
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
338
+ .map(attribute_name => "#" + attribute_name + " = list_append(#" + attribute_name + ", :" + attribute_name + ")").join(", "),
339
+ ExpressionAttributeNames: Object.fromEntries(Object.keys(attributes)
340
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
341
+ .map(attribute_name => ["#" + attribute_name, attribute_name]
342
+ )),
343
+ ExpressionAttributeValues: Object.fromEntries(Object.entries(attributes)
344
+ .filter(([attribute_name, attribute_value]) => not_undefined(attribute_value))
345
+ .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)])
346
+ .filter(([attribute_name, attribute_value]) => not_undefined(attribute_value))
347
+ )
348
+ }
349
+ this.dynamodb.updateItem(request)
350
+ }
351
+
352
+ async add(table : string, key : Record<string, any>, attributes : Record<string, any[]>) : Promise<void> {
353
+ const item = await this.get(table, key, true)
354
+ if (item === undefined) {
355
+ return
356
+ }
357
+ const new_attributes : Record<string, any[]> = {}
358
+ for (const [attribute, values] of Object.entries(attributes)) {
359
+ if (item[attribute] === undefined) {
360
+ continue
361
+ }
362
+ const new_values = values.filter(value => !item[attribute].includes(value))
363
+ if (new_values.length > 0) {
364
+ new_attributes[attribute] = new_values
365
+ }
366
+ }
367
+ if (Object.values(new_attributes).flat().length === 0) {
368
+ return undefined
369
+ }
370
+ return await this.append(table, key, attributes)
371
+ }
372
+
373
+ async remove(table : string, key : Record<string, any>, attributes : string[]) : Promise<void> {
374
+ const converted_key = convert_input(key)?.M
375
+
376
+ const request : UpdateItemCommandInput = {
377
+ TableName : table,
378
+ Key : converted_key,
379
+ UpdateExpression: "remove " + attributes
380
+ .map(attribute_name => "#" + attribute_name).join(", "),
381
+ ExpressionAttributeNames: Object.fromEntries(attributes
382
+ .map(attribute_name => ["#" + attribute_name, attribute_name]
383
+ ))
384
+ }
385
+ await this.dynamodb.updateItem(request)
386
+ }
387
+
388
+ async create(table : string, key : Record<string, any>, attributes : Record<string, any[]> = {}) : Promise<void> {
389
+ const item = await this.get(table, key, true)
390
+ if (item !== undefined) {
391
+ return
392
+ }
393
+ const converted_key = convert_input(key)?.M
394
+ const converted_attributes = convert_input(attributes)?.M
395
+ await this.dynamodb.putItem({
396
+ TableName : table,
397
+ Item : { ...converted_key, ...converted_attributes }
398
+ })
399
+ }
400
+
401
+ async delete(table : string, key : Record<string, any>) : Promise<void> {
402
+ const converted_key = convert_input(key)?.M
403
+ await this.dynamodb.deleteItem({
404
+ TableName: table,
405
+ Key: converted_key
406
+ })
407
+ }
408
+
409
+ async duplicate_attribute(table : string, attribute_name : string, new_attribute_name : string) : Promise<void>{
410
+ const table_metadata = await this.dynamodb.describeTable({
411
+ TableName : table
412
+ })
413
+ if (table_metadata.Table === undefined || table_metadata.Table.KeySchema === undefined) {
414
+ return
415
+ }
416
+ const table_key_names = table_metadata.Table.KeySchema.map(key => key.AttributeName)
417
+ .filter(table_key_name => table_key_name !== undefined)
418
+ const items = await this.scan(table, { attribute_names : table_key_names.concat([attribute_name, new_attribute_name]) })
419
+ if (items.filter(item => item[new_attribute_name] !== undefined).length > 0) {
420
+ console.log("Cannot rename.", new_attribute_name, "is an existing item.")
421
+ return
422
+ }
423
+ for (const item of items) {
424
+ if (item[attribute_name] === undefined) {
425
+ continue
426
+ }
427
+ const key = Object.fromEntries(table_key_names.map(key_name => [key_name, item[key_name]]))
428
+ await this.set(table, key, { [new_attribute_name] : item[attribute_name] })
429
+ }
430
+ }
431
+
432
+ async remove_attribute(table : string, attribute_name : string) {
433
+ const table_metadata = await this.dynamodb.describeTable({
434
+ TableName : table
435
+ })
436
+ if (table_metadata.Table === undefined || table_metadata.Table.KeySchema === undefined) {
437
+ return
438
+ }
439
+ const table_key_names = table_metadata.Table.KeySchema.map(key => key.AttributeName)
440
+ .filter(table_key_name => table_key_name !== undefined)
441
+ const items = await this.scan(table)
442
+ .then(items => items.filter(item => item[attribute_name] !== undefined))
443
+ for (const item of items) {
444
+ const key = Object.fromEntries(table_key_names.map(key_name => [key_name, item[key_name]]))
445
+ await this.remove(table, key, [attribute_name])
446
+ }
447
+ }
448
+ }