triangle-utils 1.0.16 → 1.0.17

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/index.js CHANGED
@@ -1,16 +1,15 @@
1
+ import states from "./states.js"
1
2
 
3
+ import Utils_DynamoDB from "./utils/Utils_DynamoDB.js"
4
+ import Utils_S3 from "./utils/Utils_S3.js"
5
+ import Utils_Misc from "./utils/Utils_Misc.js"
2
6
 
3
- import * as utils_dynamodb from "./utils/dynamodb.js"
4
- import * as utils_misc from "./misc.js"
5
- import get_clients from "./clients.js"
6
-
7
- export default function get_utils(config) {
8
- const clients = get_clients(config)
9
- const utils = {}
10
- for (const utils_list of [utils_misc, utils_dynamodb]) {
11
- for (const [util_name, util] of Object.entries(utils_list)) {
12
- utils[util_name] = (...args) => util(clients, ...args)
13
- }
7
+ export default class Utils extends Utils_Misc {
8
+ constructor(config) {
9
+ super(config)
10
+ this.config = config
11
+ this.states = states
12
+ this.dynamodb = new Utils_DynamoDB(config)
13
+ this.s3 = new Utils_S3(config)
14
14
  }
15
- return utils
16
15
  }
package/package.json CHANGED
@@ -1,12 +1,14 @@
1
1
  {
2
2
  "name": "triangle-utils",
3
- "version": "1.0.16",
3
+ "version": "1.0.17",
4
4
  "main": "index.js",
5
5
  "author": "",
6
6
  "license": "ISC",
7
7
  "description": "",
8
8
  "type": "module",
9
9
  "dependencies": {
10
- "@aws-sdk/client-dynamodb": "^3.953.0"
10
+ "@aws-sdk/client-dynamodb": "^3.953.0",
11
+ "@aws-sdk/client-s3": "^3.953.0",
12
+ "nodemailer": "^7.0.11"
11
13
  }
12
14
  }
package/test.js CHANGED
@@ -0,0 +1,6 @@
1
+ import Utils from "./index.js";
2
+
3
+
4
+ const utils = new Utils({})
5
+
6
+ console.log(utils)
@@ -0,0 +1,291 @@
1
+ import { DynamoDB } from "@aws-sdk/client-dynamodb"
2
+
3
+ function convert_output(dynamoobject) {
4
+ if (dynamoobject.S !== undefined) {
5
+ return dynamoobject.S
6
+ } else if (dynamoobject.BOOL !== undefined) {
7
+ return dynamoobject.BOOL
8
+ } else if (dynamoobject.N !== undefined) {
9
+ return Number(dynamoobject.N)
10
+ } else if (dynamoobject.L !== undefined) {
11
+ return dynamoobject.L.map(a => convert_output(a))
12
+ } else if (dynamoobject.SS !== undefined) {
13
+ return new Set(dynamoobject.SS)
14
+ } else if (dynamoobject.M !== undefined) {
15
+ return Object.fromEntries(Object.entries(dynamoobject.M).map(([key, value]) => [key, convert_output(value)]))
16
+ }
17
+ return undefined
18
+ }
19
+
20
+ function convert_input(input) {
21
+ if (typeof input === typeof "") {
22
+ return { S : input }
23
+ } else if (typeof input === typeof true) {
24
+ return { BOOL : input }
25
+ } else if (typeof input === typeof 3.2) {
26
+ return { N : input.toString() }
27
+ } else if (Array.isArray(input)) {
28
+ return { L : input.map(a => convert_input(a)) }
29
+ } else if (input instanceof Set) {
30
+ return { SS : Array.from(input) }
31
+ } else if (typeof input === typeof {}) {
32
+ return {
33
+ M : Object.fromEntries(Object.entries(input)
34
+ .filter(([key, value]) => value !== undefined && value !== null && key !== "")
35
+ .map(([key, value]) => [key, convert_input(value)]))
36
+ }
37
+ }
38
+ return undefined
39
+ }
40
+
41
+ async function paginate(request, f) {
42
+ const items = []
43
+ let last_eval_key = undefined
44
+ while (true) {
45
+ const response = last_eval_key !== undefined ? (
46
+ await f({...request, ExclusiveStartKey : last_eval_key })
47
+ ) : (
48
+ await f(request)
49
+ )
50
+ const new_items = response.Items.map(item => convert_output({ M : item }))
51
+ items.push(...new_items)
52
+ if (response.LastEvaluatedKey === undefined) {
53
+ return items
54
+ }
55
+ last_eval_key = response.LastEvaluatedKey
56
+ }
57
+ }
58
+
59
+ export default class Utils_DynamoDB {
60
+
61
+ constructor(config) {
62
+ this.dynamodb = new DynamoDB({ region : config.region })
63
+ }
64
+
65
+ async scan(table, attribute_names = [], filters = {}) {
66
+ const request = Object.fromEntries(Object.entries({
67
+ TableName : table,
68
+ ExpressionAttributeNames : Object.fromEntries((Object.keys(filters).concat(attribute_names)).map(attribute_name => ["#" + attribute_name, attribute_name])),
69
+ ExpressionAttributeValues : Object.fromEntries(Object.entries(filters).map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)])),
70
+ FilterExpression : Object.keys(filters).map(attribute_name => "#" + attribute_name + " = :" + attribute_name).join(","),
71
+ ProjectionExpression : attribute_names.map(attribute_name => "#" + attribute_name).join(", ")
72
+ }).filter(([field, value]) => value !== undefined && Object.keys(value).length !== 0))
73
+ return await paginate(request, (request) => this.dynamodb.scan(request))
74
+ }
75
+
76
+ async get(table, key, consistent=false) {
77
+ const item = await this.dynamodb.getItem({
78
+ ConsistentRead : consistent,
79
+ TableName : table,
80
+ Key : convert_input(key).M
81
+ })
82
+ .then(response => response.Item)
83
+ if (item === undefined) {
84
+ return undefined
85
+ }
86
+ return convert_output({ M : item })
87
+ }
88
+
89
+ async get_max(table, primary_key) {
90
+ if (Object.keys(primary_key).length !== 1) {
91
+ return undefined
92
+ }
93
+ const request = {
94
+ TableName : table,
95
+ ExpressionAttributeNames: {
96
+ "#a": Object.keys(primary_key)[0]
97
+ },
98
+ ExpressionAttributeValues: {
99
+ ":a": convert_input(Object.values(primary_key)[0])
100
+ },
101
+ KeyConditionExpression: "#a = :a",
102
+ Limit : 1,
103
+ ScanIndexForward : false
104
+ }
105
+ const items = await this.dynamodb.query(request)
106
+ .then(response => response.Items)
107
+ if (items[0] === undefined) {
108
+ return undefined
109
+ }
110
+ return convert_output({ M : items[0] })
111
+ }
112
+
113
+
114
+
115
+ async query(table, primary_key, reverse=false) {
116
+ if (Object.keys(primary_key).length !== 1) {
117
+ return undefined
118
+ }
119
+ const request = {
120
+ TableName : table,
121
+ ExpressionAttributeNames: {
122
+ "#a": Object.keys(primary_key)[0]
123
+ },
124
+ ExpressionAttributeValues: {
125
+ ":a": convert_input(Object.values(primary_key)[0])
126
+ },
127
+ KeyConditionExpression: "#a = :a",
128
+ ScanIndexForward : !reverse
129
+ }
130
+ return await paginate(request, (request) => this.dynamodb.query(request))
131
+ }
132
+
133
+ async query_prefix(table, primary_key, secondary_key_prefix, reverse=false) {
134
+ if (Object.keys(primary_key).length !== 1 || Object.keys(secondary_key_prefix).length !== 1) {
135
+ return undefined
136
+ }
137
+ const request = {
138
+ TableName : table,
139
+ ExpressionAttributeNames: {
140
+ "#a": Object.keys(primary_key)[0],
141
+ "#b": Object.keys(secondary_key_prefix)[0]
142
+ },
143
+ ExpressionAttributeValues: {
144
+ ":a": convert_input(Object.values(primary_key)[0]),
145
+ ":b": convert_input(Object.values(secondary_key_prefix)[0])
146
+ },
147
+ KeyConditionExpression: "#a = :a AND begins_with(#b, :b)",
148
+ ScanIndexForward : !reverse
149
+ }
150
+ return await paginate(request, (request) => this.dynamodb.query(request))
151
+ }
152
+
153
+ async query_range(table, primary_key, secondary_key_range, reverse=false) {
154
+ if (Object.keys(primary_key).length !== 1 || Object.keys(secondary_key_range).length !== 1 || Object.values(secondary_key_range)[0].length !== 2) {
155
+ return undefined
156
+ }
157
+ const request = {
158
+ TableName : table,
159
+ ExpressionAttributeNames: {
160
+ "#a": Object.keys(primary_key)[0],
161
+ "#b": Object.keys(secondary_key_range)[0]
162
+ },
163
+ ExpressionAttributeValues: {
164
+ ":a": convert_input(Object.values(primary_key)[0]),
165
+ ":b1": convert_input(Object.values(secondary_key_range)[0][0]),
166
+ ":b2": convert_input(Object.values(secondary_key_range)[0][1])
167
+ },
168
+ KeyConditionExpression: "#a = :a AND (#b BETWEEN :b1 AND :b2)",
169
+ ScanIndexForward : !reverse
170
+ }
171
+ return await paginate(request, (request) => this.dynamodb.query(request))
172
+ }
173
+
174
+ async set(table, key, attributes) {
175
+ return await this.dynamodb.updateItem({
176
+ TableName : table,
177
+ Key : convert_input(key).M,
178
+ UpdateExpression: "set " + Object.keys(attributes)
179
+ .filter(attribute_name => !Object.keys(key).includes(attribute_name))
180
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
181
+ .map(attribute_name => "#" + attribute_name + " = :" + attribute_name
182
+ ).join(", "),
183
+ ExpressionAttributeNames: Object.fromEntries(Object.keys(attributes)
184
+ .filter(attribute_name => !Object.keys(key).includes(attribute_name))
185
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
186
+ .map(attribute_name => ["#" + attribute_name, attribute_name]
187
+ )),
188
+ ExpressionAttributeValues: Object.fromEntries(Object.entries(attributes)
189
+ .filter(([attribute_name, attribute_value]) => !Object.keys(key).includes(attribute_name))
190
+ .filter(([attribute_name, attribute_value]) => attribute_value !== undefined)
191
+ .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)]
192
+ ))
193
+ })
194
+ }
195
+
196
+ async append(table, key, attributes) {
197
+ return await this.dynamodb.updateItem({
198
+ TableName : table,
199
+ Key : convert_input(key).M,
200
+ UpdateExpression: "set " + Object.keys(attributes)
201
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
202
+ .map(attribute_name => "#" + attribute_name + " = list_append(#" + attribute_name + ", :" + attribute_name + ")").join(", "),
203
+ ExpressionAttributeNames: Object.fromEntries(Object.keys(attributes)
204
+ .filter(attribute_name => attributes[attribute_name] !== undefined)
205
+ .map(attribute_name => ["#" + attribute_name, attribute_name]
206
+ )),
207
+ ExpressionAttributeValues: Object.fromEntries(Object.entries(attributes)
208
+ .filter(([attribute_name, attribute_value]) => attribute_value !== undefined)
209
+ .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)]
210
+ ))
211
+ })
212
+ }
213
+
214
+ async add(table, key, attributes) {
215
+ const item = await this.get(table, key, true)
216
+ const new_attributes = {}
217
+ for (const [attribute, values] of Object.entries(attributes)) {
218
+ if (item[attribute] === undefined) {
219
+ continue
220
+ }
221
+ const new_values = values.filter(value => !item[attribute].includes(value))
222
+ if (new_values.length > 0) {
223
+ new_attributes[attribute] = new_values
224
+ }
225
+ }
226
+ if (Object.values(new_attributes).flat().length === 0) {
227
+ return undefined
228
+ }
229
+ return await this.append(table, key, attributes)
230
+ }
231
+
232
+ async remove(table, key, attributes) {
233
+ return await this.dynamodb.updateItem({
234
+ TableName : table,
235
+ Key : convert_input(key).M,
236
+ UpdateExpression: "remove " + attributes
237
+ .map(attribute_name => "#" + attribute_name).join(", "),
238
+ ExpressionAttributeNames: Object.fromEntries(attributes
239
+ .map(attribute_name => ["#" + attribute_name, attribute_name]
240
+ ))
241
+ })
242
+ }
243
+
244
+ async create(table, key, attributes = {}) {
245
+ const item = await this.get(table, key)
246
+ if (item !== undefined) {
247
+ return undefined
248
+ }
249
+ return await this.dynamodb.putItem({
250
+ TableName : table,
251
+ Item : { ...convert_input(key).M, ...convert_input(attributes).M }
252
+ })
253
+ }
254
+
255
+ async delete(table, key) {
256
+ return await this.dynamodb.deleteItem({
257
+ TableName: table,
258
+ Key: Object.fromEntries(Object.keys(key).map(key_name => [key_name, convert_input(key[key_name])]))
259
+ })
260
+ }
261
+
262
+ async duplicate_attribute(table, attribute_name, new_attribute_name) {
263
+ const table_key_names = await this.dynamodb.describeTable({
264
+ TableName : table
265
+ }).then(metadata => metadata.Table.KeySchema.map(key => key.AttributeName))
266
+ const items = await this.scan(table, table_key_names.concat([attribute_name, new_attribute_name]))
267
+ if (items.filter(item => item[new_attribute_name] !== undefined).length > 0) {
268
+ console.log("Cannot rename.", new_attribute_name, "is an existing item.")
269
+ return
270
+ }
271
+ for (const item of items) {
272
+ if (item[attribute_name] === undefined) {
273
+ continue
274
+ }
275
+ const key = Object.fromEntries(table_key_names.map(key_name => [key_name, item[key_name]]))
276
+ await this.set(table, key, { [new_attribute_name] : item[attribute_name] })
277
+ }
278
+ }
279
+
280
+ async remove_attribute(table, attribute_name) {
281
+ const table_key_names = await this.dynamodb.describeTable({
282
+ TableName : table
283
+ }).then(metadata => metadata.Table.KeySchema.map(key => key.AttributeName))
284
+ const items = await this.scan(table)
285
+ .then(items => items.filter(item => item[attribute_name] !== undefined))
286
+ for (const item of items) {
287
+ const key = Object.fromEntries(table_key_names.map(key_name => [key_name, item[key_name]]))
288
+ await this.remove(table, key, [attribute_name])
289
+ }
290
+ }
291
+ }
@@ -0,0 +1,114 @@
1
+ import nodemailer from "nodemailer"
2
+ import election_ids from "./election_ids.js"
3
+ import crypto from "crypto"
4
+
5
+ export default class Utils_Misc {
6
+
7
+ constructor(config) {
8
+ this.config = config
9
+ this.text_encoder = new TextEncoder()
10
+ if (config.google_email !== undefined && config.google_app_password !== undefined) {
11
+ this.transporter = nodemailer.createTransport({
12
+ service: "Gmail",
13
+ host: "smtp.gmail.com",
14
+ port: 465,
15
+ secure: true,
16
+ auth: {
17
+ user: config.google_email,
18
+ pass: config.google_app_password,
19
+ }
20
+ })
21
+ }
22
+ }
23
+
24
+ async wait(duration) {
25
+ return new Promise(resolve => setTimeout(resolve, duration))
26
+ }
27
+
28
+ get_current_time( milliseconds = false) {
29
+ if (milliseconds) {
30
+ return (new Date()).getTime()
31
+ }
32
+ return (new Date()).toISOString()
33
+ }
34
+
35
+ async send_email(recipient, subject, text) {
36
+ for (let i = 0; i < 3; i++) {
37
+ try {
38
+ await this.transporter.sendMail({
39
+ from: clients.config.alerts_email,
40
+ to: recipient,
41
+ subject: subject,
42
+ text: text
43
+ })
44
+ return
45
+ } catch (error) {
46
+ console.log("EMAIL ERROR", error)
47
+ }
48
+ }
49
+ }
50
+
51
+ async admin_alert(text) {
52
+ console.log("ADMIN ALERT:", text)
53
+ await send_email(this.config.google_email, "ADMIN ALERT", text)
54
+ }
55
+
56
+ async safe_run( f) {
57
+ try {
58
+ await f()
59
+ } catch (error) {
60
+ if (error.stack !== undefined) {
61
+ await admin_alert(error.stack.toString())
62
+ } else {
63
+ await admin_alert(error.toString())
64
+ }
65
+
66
+ }
67
+ }
68
+
69
+ async iterate(inputs, f, concurrency = 1, print_indices = false) {
70
+ let index = 0
71
+ let iterators = []
72
+ for (let i = 0; i < concurrency; i++) {
73
+ iterators.push((async () => {
74
+ while (index < inputs.length) {
75
+ index += 1
76
+ if (print_indices) {
77
+ console.log(i + ":" + (index - 1) + "/" + inputs.length)
78
+ }
79
+ await safe_run(() => f(inputs[index - 1]))
80
+ }
81
+ })())
82
+ }
83
+ await Promise.all(iterators)
84
+ }
85
+
86
+ async sha256(input) {
87
+ const input_buffer = text_encoder.encode(input);
88
+
89
+ const hash_buffer = await crypto.subtle.digest("SHA-256", input_buffer)
90
+
91
+ const hash_array = Array.from(new Uint8Array(hash_buffer))
92
+
93
+ const hash = hash_array.map(b => b.toString(16).padStart(2, "0")).join("")
94
+ return hash;
95
+ }
96
+
97
+ get_election_id(year, office, state, district) {
98
+ if (year === undefined || office === undefined || state === undefined || office === "P") {
99
+ return undefined
100
+ }
101
+ if (office === "H" && (district === undefined || district.length !== 2)) {
102
+ return undefined
103
+ }
104
+ if (year.length !== 4 || office.length !== 1 || state.length !== 2) {
105
+ return undefined
106
+ }
107
+ const office_name = { "H" : "house", "S" : "sen", "P" : "pres" }[office]
108
+ const election_id = year + "-" + office_name + "-" + state.toLowerCase() + (office === "H" ? ("-" + district.toLowerCase()) : "") + "-election"
109
+ if (!election_ids.has(election_id)) {
110
+ return undefined
111
+ }
112
+ return election_id
113
+ }
114
+ }
@@ -0,0 +1,55 @@
1
+ import { S3, NoSuchKey } from "@aws-sdk/client-s3"
2
+
3
+ export default class Utils_S3 {
4
+
5
+ constructor(config) {
6
+ this.s3 = new S3({ region : config.region })
7
+ }
8
+
9
+ async file_exists(bucket, filename) {
10
+ try {
11
+ const head = await s3.headObject({
12
+ Bucket : bucket,
13
+ Key : filename
14
+ })
15
+ return true
16
+ } catch (error) {
17
+ return false
18
+ }
19
+ }
20
+
21
+ async get_file(bucket, filename, json=false) {
22
+ try {
23
+ return await s3.getObject({
24
+ Bucket : bucket,
25
+ Key : filename
26
+ }).then(response => response.Body.transformToString("utf-8"))
27
+ .then(text => json ? JSON.parse(text) : text)
28
+ } catch (error) {
29
+ if (error instanceof NoSuchKey) {
30
+ return undefined
31
+ }
32
+ throw error
33
+ }
34
+ }
35
+
36
+ async create_file(bucket, filename, content) {
37
+ const exists = await file_exists(bucket, filename)
38
+ if (exists) {
39
+ return
40
+ }
41
+ return await s3.putObject({
42
+ Bucket : bucket,
43
+ Key : filename,
44
+ Body : content
45
+ })
46
+ }
47
+
48
+ async delete_file(bucket, filename) {
49
+ return await s3.deleteObject({
50
+ Bucket : bucket,
51
+ Key : filename
52
+ })
53
+ }
54
+
55
+ }
package/clients.js DELETED
@@ -1,24 +0,0 @@
1
-
2
- import nodemailer from "nodemailer"
3
- import { DynamoDB } from "@aws-sdk/client-dynamodb"
4
-
5
- export default function get_clients(config) {
6
- const clients = {}
7
- if (config.region !== undefined) {
8
- clients.dynamodb = new DynamoDB({ region : config.region })
9
- }
10
- if (config.google_email !== undefined && config.google_app_password !== undefined) {
11
- clients.transporter = nodemailer.createTransport({
12
- service: "Gmail",
13
- host: "smtp.gmail.com",
14
- port: 465,
15
- secure: true,
16
- auth: {
17
- user: config.google_email,
18
- pass: config.google_app_password,
19
- }
20
- })
21
- }
22
- clients.config = config
23
- return clients
24
- }
package/misc.js DELETED
@@ -1,95 +0,0 @@
1
- import election_ids from "./utils/election_ids.js"
2
- import crypto from "crypto"
3
-
4
- export async function wait(clients, duration) {
5
- return new Promise(resolve => setTimeout(resolve, duration))
6
- }
7
-
8
- export function get_current_time(clients, milliseconds = false) {
9
- if (milliseconds) {
10
- return (new Date()).getTime()
11
- }
12
- return (new Date()).toISOString()
13
- }
14
-
15
- export async function send_email(clients, recipient, subject, text) {
16
- for (let i = 0; i < 3; i++) {
17
- try {
18
- await clients.transporter.sendMail({
19
- from: clients.config.alerts_email,
20
- to: recipient,
21
- subject: subject,
22
- text: text
23
- })
24
- return
25
- } catch (error) {
26
- console.log("EMAIL ERROR", error)
27
- }
28
- }
29
- }
30
-
31
- export async function admin_alert(clients, text) {
32
- console.log("ADMIN ALERT:", text)
33
- await send_email(clients, clients.config.google_email, "ADMIN ALERT", text)
34
- }
35
-
36
- export async function safe_run(clients, f) {
37
- try {
38
- await f()
39
- } catch (error) {
40
- if (error.stack !== undefined) {
41
- await admin_alert(clients, error.stack.toString())
42
- } else {
43
- await admin_alert(clients, error.toString())
44
- }
45
-
46
- }
47
- }
48
-
49
- export async function iterate(clients, inputs, f, concurrency = 1, print_indices = false) {
50
- let index = 0
51
- let iterators = []
52
- for (let i = 0; i < concurrency; i++) {
53
- iterators.push((async () => {
54
- while (index < inputs.length) {
55
- index += 1
56
- if (print_indices) {
57
- console.log(i + ":" + (index - 1) + "/" + inputs.length)
58
- }
59
- await safe_run(clients, () => f(inputs[index - 1]))
60
- }
61
- })())
62
- }
63
- await Promise.all(iterators)
64
- }
65
-
66
- const text_encoder = new TextEncoder()
67
-
68
- export async function sha256(clients, input) {
69
- const input_buffer = text_encoder.encode(input);
70
-
71
- const hash_buffer = await crypto.subtle.digest("SHA-256", input_buffer)
72
-
73
- const hash_array = Array.from(new Uint8Array(hash_buffer))
74
-
75
- const hash = hash_array.map(b => b.toString(16).padStart(2, "0")).join("")
76
- return hash;
77
- }
78
-
79
- export function get_election_id(clients, year, office, state, district) {
80
- if (year === undefined || office === undefined || state === undefined || office === "P") {
81
- return undefined
82
- }
83
- if (office === "H" && (district === undefined || district.length !== 2)) {
84
- return undefined
85
- }
86
- if (year.length !== 4 || office.length !== 1 || state.length !== 2) {
87
- return undefined
88
- }
89
- const office_name = { "H" : "house", "S" : "sen", "P" : "pres" }[office]
90
- const election_id = year + "-" + office_name + "-" + state.toLowerCase() + (office === "H" ? ("-" + district.toLowerCase()) : "") + "-election"
91
- if (!election_ids.has(election_id)) {
92
- return undefined
93
- }
94
- return election_id
95
- }
package/utils/dynamodb.js DELETED
@@ -1,282 +0,0 @@
1
- function convert_output(dynamoobject) {
2
- if (dynamoobject.S !== undefined) {
3
- return dynamoobject.S
4
- } else if (dynamoobject.BOOL !== undefined) {
5
- return dynamoobject.BOOL
6
- } else if (dynamoobject.N !== undefined) {
7
- return Number(dynamoobject.N)
8
- } else if (dynamoobject.L !== undefined) {
9
- return dynamoobject.L.map(a => convert_output(a))
10
- } else if (dynamoobject.SS !== undefined) {
11
- return new Set(dynamoobject.SS)
12
- } else if (dynamoobject.M !== undefined) {
13
- return Object.fromEntries(Object.entries(dynamoobject.M).map(([key, value]) => [key, convert_output(value)]))
14
- }
15
- return undefined
16
- }
17
-
18
- function convert_input(input) {
19
- if (typeof input === typeof "") {
20
- return { S : input }
21
- } else if (typeof input === typeof true) {
22
- return { BOOL : input }
23
- } else if (typeof input === typeof 3.2) {
24
- return { N : input.toString() }
25
- } else if (Array.isArray(input)) {
26
- return { L : input.map(a => convert_input(a)) }
27
- } else if (input instanceof Set) {
28
- return { SS : Array.from(input) }
29
- } else if (typeof input === typeof {}) {
30
- return {
31
- M : Object.fromEntries(Object.entries(input)
32
- .filter(([key, value]) => value !== undefined && value !== null && key !== "")
33
- .map(([key, value]) => [key, convert_input(value)]))
34
- }
35
- }
36
- return undefined
37
- }
38
-
39
- async function paginate(request, f) {
40
- const items = []
41
- let last_eval_key = undefined
42
- while (true) {
43
- const response = last_eval_key !== undefined ? (
44
- await f({...request, ExclusiveStartKey : last_eval_key })
45
- ) : (
46
- await f(request)
47
- )
48
- const new_items = response.Items.map(item => convert_output({ M : item }))
49
- items.push(...new_items)
50
- if (response.LastEvaluatedKey === undefined) {
51
- return items
52
- }
53
- last_eval_key = response.LastEvaluatedKey
54
- }
55
- }
56
-
57
- export async function dynamodb_scan(clients, table, attribute_names = [], filters = {}) {
58
- const request = Object.fromEntries(Object.entries({
59
- TableName : table,
60
- ExpressionAttributeNames : Object.fromEntries((Object.keys(filters).concat(attribute_names)).map(attribute_name => ["#" + attribute_name, attribute_name])),
61
- ExpressionAttributeValues : Object.fromEntries(Object.entries(filters).map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)])),
62
- FilterExpression : Object.keys(filters).map(attribute_name => "#" + attribute_name + " = :" + attribute_name).join(","),
63
- ProjectionExpression : attribute_names.map(attribute_name => "#" + attribute_name).join(", ")
64
- }).filter(([field, value]) => value !== undefined && Object.keys(value).length !== 0))
65
- return await paginate(request, (request) => clients.dynamodb.scan(request))
66
- }
67
-
68
- export async function dynamodb_get(clients, table, key, consistent=false) {
69
- const item = await clients.dynamodb.getItem({
70
- ConsistentRead : consistent,
71
- TableName : table,
72
- Key : convert_input(key).M
73
- })
74
- .then(response => response.Item)
75
- if (item === undefined) {
76
- return undefined
77
- }
78
- return convert_output({ M : item })
79
- }
80
-
81
- export async function dynamodb_get_max(clients, table, primary_key) {
82
- if (Object.keys(primary_key).length !== 1) {
83
- return undefined
84
- }
85
- const request = {
86
- TableName : table,
87
- ExpressionAttributeNames: {
88
- "#a": Object.keys(primary_key)[0]
89
- },
90
- ExpressionAttributeValues: {
91
- ":a": convert_input(Object.values(primary_key)[0])
92
- },
93
- KeyConditionExpression: "#a = :a",
94
- Limit : 1,
95
- ScanIndexForward : false
96
- }
97
- const items = await clients.dynamodb.query(request)
98
- .then(response => response.Items)
99
- if (items[0] === undefined) {
100
- return undefined
101
- }
102
- return convert_output({ M : items[0] })
103
- }
104
-
105
-
106
-
107
- export async function dynamodb_query(clients, table, primary_key, reverse=false) {
108
- if (Object.keys(primary_key).length !== 1) {
109
- return undefined
110
- }
111
- const request = {
112
- TableName : table,
113
- ExpressionAttributeNames: {
114
- "#a": Object.keys(primary_key)[0]
115
- },
116
- ExpressionAttributeValues: {
117
- ":a": convert_input(Object.values(primary_key)[0])
118
- },
119
- KeyConditionExpression: "#a = :a",
120
- ScanIndexForward : !reverse
121
- }
122
- return await paginate(request, (request) => clients.dynamodb.query(request))
123
- }
124
-
125
- export async function dynamodb_query_prefix(clients, table, primary_key, secondary_key_prefix, reverse=false) {
126
- if (Object.keys(primary_key).length !== 1 || Object.keys(secondary_key_prefix).length !== 1) {
127
- return undefined
128
- }
129
- const request = {
130
- TableName : table,
131
- ExpressionAttributeNames: {
132
- "#a": Object.keys(primary_key)[0],
133
- "#b": Object.keys(secondary_key_prefix)[0]
134
- },
135
- ExpressionAttributeValues: {
136
- ":a": convert_input(Object.values(primary_key)[0]),
137
- ":b": convert_input(Object.values(secondary_key_prefix)[0])
138
- },
139
- KeyConditionExpression: "#a = :a AND begins_with(#b, :b)",
140
- ScanIndexForward : !reverse
141
- }
142
- return await paginate(request, (request) => clients.dynamodb.query(request))
143
- }
144
-
145
- export async function dynamodb_query_range(clients, table, primary_key, secondary_key_range, reverse=false) {
146
- if (Object.keys(primary_key).length !== 1 || Object.keys(secondary_key_range).length !== 1 || Object.values(secondary_key_range)[0].length !== 2) {
147
- return undefined
148
- }
149
- const request = {
150
- TableName : table,
151
- ExpressionAttributeNames: {
152
- "#a": Object.keys(primary_key)[0],
153
- "#b": Object.keys(secondary_key_range)[0]
154
- },
155
- ExpressionAttributeValues: {
156
- ":a": convert_input(Object.values(primary_key)[0]),
157
- ":b1": convert_input(Object.values(secondary_key_range)[0][0]),
158
- ":b2": convert_input(Object.values(secondary_key_range)[0][1])
159
- },
160
- KeyConditionExpression: "#a = :a AND (#b BETWEEN :b1 AND :b2)",
161
- ScanIndexForward : !reverse
162
- }
163
- return await paginate(request, (request) => clients.dynamodb.query(request))
164
- }
165
-
166
- export async function dynamodb_set(clients, table, key, attributes) {
167
- return await clients.dynamodb.updateItem({
168
- TableName : table,
169
- Key : convert_input(key).M,
170
- UpdateExpression: "set " + Object.keys(attributes)
171
- .filter(attribute_name => !Object.keys(key).includes(attribute_name))
172
- .filter(attribute_name => attributes[attribute_name] !== undefined)
173
- .map(attribute_name => "#" + attribute_name + " = :" + attribute_name
174
- ).join(", "),
175
- ExpressionAttributeNames: Object.fromEntries(Object.keys(attributes)
176
- .filter(attribute_name => !Object.keys(key).includes(attribute_name))
177
- .filter(attribute_name => attributes[attribute_name] !== undefined)
178
- .map(attribute_name => ["#" + attribute_name, attribute_name]
179
- )),
180
- ExpressionAttributeValues: Object.fromEntries(Object.entries(attributes)
181
- .filter(([attribute_name, attribute_value]) => !Object.keys(key).includes(attribute_name))
182
- .filter(([attribute_name, attribute_value]) => attribute_value !== undefined)
183
- .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)]
184
- ))
185
- })
186
- }
187
-
188
- export async function dynamodb_append(clients, table, key, attributes) {
189
- return await clients.dynamodb.updateItem({
190
- TableName : table,
191
- Key : convert_input(key).M,
192
- UpdateExpression: "set " + Object.keys(attributes)
193
- .filter(attribute_name => attributes[attribute_name] !== undefined)
194
- .map(attribute_name => "#" + attribute_name + " = list_append(#" + attribute_name + ", :" + attribute_name + ")").join(", "),
195
- ExpressionAttributeNames: Object.fromEntries(Object.keys(attributes)
196
- .filter(attribute_name => attributes[attribute_name] !== undefined)
197
- .map(attribute_name => ["#" + attribute_name, attribute_name]
198
- )),
199
- ExpressionAttributeValues: Object.fromEntries(Object.entries(attributes)
200
- .filter(([attribute_name, attribute_value]) => attribute_value !== undefined)
201
- .map(([attribute_name, attribute_value]) => [":" + attribute_name, convert_input(attribute_value)]
202
- ))
203
- })
204
- }
205
-
206
- export async function dynamodb_add(clients, table, key, attributes) {
207
- const item = await dynamodb_get(clients, table, key, true)
208
- const new_attributes = {}
209
- for (const [attribute, values] of Object.entries(attributes)) {
210
- if (item[attribute] === undefined) {
211
- continue
212
- }
213
- const new_values = values.filter(value => !item[attribute].includes(value))
214
- if (new_values.length > 0) {
215
- new_attributes[attribute] = new_values
216
- }
217
- }
218
- if (Object.values(new_attributes).flat().length === 0) {
219
- return undefined
220
- }
221
- return await dynamodb_append(clients, table, key, attributes)
222
- }
223
-
224
- export async function dynamodb_remove(clients, table, key, attributes) {
225
- return await clients.dynamodb.updateItem({
226
- TableName : table,
227
- Key : convert_input(key).M,
228
- UpdateExpression: "remove " + attributes
229
- .map(attribute_name => "#" + attribute_name).join(", "),
230
- ExpressionAttributeNames: Object.fromEntries(attributes
231
- .map(attribute_name => ["#" + attribute_name, attribute_name]
232
- ))
233
- })
234
- }
235
-
236
- export async function dynamodb_create(clients, table, key, attributes = {}) {
237
- const item = await dynamodb_get(clients, table, key)
238
- if (item !== undefined) {
239
- return undefined
240
- }
241
- return await clients.dynamodb.putItem({
242
- TableName : table,
243
- Item : { ...convert_input(key).M, ...convert_input(attributes).M }
244
- })
245
- }
246
-
247
- export async function dynamodb_delete(clients, table, key) {
248
- return await clients.dynamodb.deleteItem({
249
- TableName: table,
250
- Key: Object.fromEntries(Object.keys(key).map(key_name => [key_name, convert_input(key[key_name])]))
251
- })
252
- }
253
-
254
- export async function dynamodb_duplicate_attribute(clients, table, attribute_name, new_attribute_name) {
255
- const table_key_names = await clients.dynamodb.describeTable({
256
- TableName : table
257
- }).then(metadata => metadata.Table.KeySchema.map(key => key.AttributeName))
258
- const items = await dynamodb_scan(clients, table, table_key_names.concat([attribute_name, new_attribute_name]))
259
- if (items.filter(item => item[new_attribute_name] !== undefined).length > 0) {
260
- console.log("Cannot rename.", new_attribute_name, "is an existing item.")
261
- return
262
- }
263
- for (const item of items) {
264
- if (item[attribute_name] === undefined) {
265
- continue
266
- }
267
- const key = Object.fromEntries(table_key_names.map(key_name => [key_name, item[key_name]]))
268
- await dynamodb_set(clients, table, key, { [new_attribute_name] : item[attribute_name] })
269
- }
270
- }
271
-
272
- export async function dynamodb_remove_attribute(clients, table, attribute_name) {
273
- const table_key_names = await clients.dynamodb.describeTable({
274
- TableName : table
275
- }).then(metadata => metadata.Table.KeySchema.map(key => key.AttributeName))
276
- const items = await dynamodb_scan(clients, table)
277
- .then(items => items.filter(item => item[attribute_name] !== undefined))
278
- for (const item of items) {
279
- const key = Object.fromEntries(table_key_names.map(key_name => [key_name, item[key_name]]))
280
- await dynamodb_remove(clients, table, key, [attribute_name])
281
- }
282
- }