@certik/skynet 0.10.4 → 0.10.7
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/.editorconfig +5 -5
- package/.eslintrc.js +13 -13
- package/.prettierrc.js +3 -3
- package/CHANGELOG.md +372 -364
- package/README.md +23 -23
- package/abi.js +353 -353
- package/ably.js +29 -0
- package/address.js +18 -18
- package/api.js +105 -105
- package/app.js +709 -695
- package/availability.js +58 -58
- package/block.js +83 -83
- package/cli.js +53 -53
- package/const.js +92 -92
- package/deploy.js +676 -673
- package/dynamodb.js +444 -444
- package/env.js +90 -90
- package/examples/api +73 -73
- package/examples/consumer +47 -47
- package/examples/indexer +65 -65
- package/examples/mode-indexer +82 -82
- package/examples/producer +80 -80
- package/indexer.js +595 -595
- package/inquiry.js +14 -14
- package/kafka.js +443 -442
- package/labelling.js +90 -90
- package/log.js +29 -29
- package/metric.js +65 -65
- package/monitor.js +191 -192
- package/opsgenie.js +55 -60
- package/package.json +37 -34
- package/price.js +48 -48
- package/primitive.js +77 -77
- package/proxy.js +157 -157
- package/rateLimit.js +21 -21
- package/s3.js +122 -122
- package/scan.js +74 -74
- package/selector.js +53 -53
- package/slack.js +87 -87
- package/snowflake.js +36 -36
- package/sqs.js +12 -12
- package/token.js +46 -46
- package/transaction.js +41 -41
- package/util.js +67 -67
- package/web3.js +117 -117
package/dynamodb.js
CHANGED
|
@@ -1,444 +1,444 @@
|
|
|
1
|
-
const { DynamoDB } = require("aws-sdk");
|
|
2
|
-
const { getAWSAccessKeyId, getAWSSecretAccessKey, getAWSRegion } = require("./env");
|
|
3
|
-
const { wait } = require("./availability");
|
|
4
|
-
|
|
5
|
-
function getDynamoDB() {
|
|
6
|
-
return new DynamoDB({
|
|
7
|
-
accessKeyId: getAWSAccessKeyId(),
|
|
8
|
-
secretAccessKey: getAWSSecretAccessKey(),
|
|
9
|
-
region: getAWSRegion(),
|
|
10
|
-
});
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function getDocClient() {
|
|
14
|
-
return new DynamoDB.DocumentClient({
|
|
15
|
-
accessKeyId: getAWSAccessKeyId(),
|
|
16
|
-
secretAccessKey: getAWSSecretAccessKey(),
|
|
17
|
-
region: getAWSRegion(),
|
|
18
|
-
});
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
function mergeQueries(q1, q2) {
|
|
22
|
-
return {
|
|
23
|
-
Items: q1.Items.concat(q2.Items),
|
|
24
|
-
Count: q1.Count + q2.Count,
|
|
25
|
-
ScannedCount: q1.ScannedCount + q2.ScannedCount,
|
|
26
|
-
};
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
async function scanWholeTable(options) {
|
|
30
|
-
const dynamodb = getDocClient();
|
|
31
|
-
|
|
32
|
-
let items = {
|
|
33
|
-
Items: [],
|
|
34
|
-
Count: 0,
|
|
35
|
-
ScannedCount: 0,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
let data = await dynamodb.scan(options).promise();
|
|
39
|
-
|
|
40
|
-
while (data.LastEvaluatedKey) {
|
|
41
|
-
items = mergeQueries(items, data);
|
|
42
|
-
|
|
43
|
-
data = await dynamodb
|
|
44
|
-
.scan({
|
|
45
|
-
ExclusiveStartKey: data.LastEvaluatedKey,
|
|
46
|
-
...options,
|
|
47
|
-
})
|
|
48
|
-
.promise();
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
items = mergeQueries(items, data);
|
|
52
|
-
|
|
53
|
-
return items;
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function toSlices(items) {
|
|
57
|
-
let slices = [];
|
|
58
|
-
|
|
59
|
-
for (let start = 0; start < items.length; start += 25) {
|
|
60
|
-
slices.push(items.slice(start, start + 25));
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
return slices;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
async function batchCreateRecords(tableName, records, maxWritingCapacity, verbose = false) {
|
|
67
|
-
if (verbose) {
|
|
68
|
-
console.log(`creating ${records.length} items in ${tableName}`);
|
|
69
|
-
}
|
|
70
|
-
const docClient = getDocClient();
|
|
71
|
-
|
|
72
|
-
let remainingItems = records;
|
|
73
|
-
// an arbitrary number to ensure while first loop is entered
|
|
74
|
-
let prevRemainingCount = remainingItems.length + 1;
|
|
75
|
-
let factor = 1;
|
|
76
|
-
let rejection = undefined;
|
|
77
|
-
|
|
78
|
-
while (remainingItems.length > 0 && factor <= 128 && !rejection) {
|
|
79
|
-
if (prevRemainingCount === remainingItems.length) {
|
|
80
|
-
await wait(5000 * factor);
|
|
81
|
-
factor = factor * 2;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
if (factor >= 32) {
|
|
85
|
-
console.log(`WARNING: no progress for a long time for batchCreateRecords, please check`);
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const slices = toSlices(remainingItems.slice(0, maxWritingCapacity));
|
|
89
|
-
const results = await Promise.allSettled(
|
|
90
|
-
slices.map((rs) => {
|
|
91
|
-
const params = {
|
|
92
|
-
RequestItems: {
|
|
93
|
-
[tableName]: rs.map((record) => {
|
|
94
|
-
return { PutRequest: { Item: record } };
|
|
95
|
-
}),
|
|
96
|
-
},
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return docClient.batchWrite(params).promise();
|
|
100
|
-
})
|
|
101
|
-
);
|
|
102
|
-
|
|
103
|
-
prevRemainingCount = remainingItems.length;
|
|
104
|
-
|
|
105
|
-
remainingItems = results.reduce((acc, rs, idx) => {
|
|
106
|
-
if (rs.status === "rejected") {
|
|
107
|
-
// whole slice fails, redo whole slice
|
|
108
|
-
acc = acc.concat(slices[idx]);
|
|
109
|
-
rejection = rs;
|
|
110
|
-
} else if (Object.keys(rs.value.UnprocessedItems).length > 0) {
|
|
111
|
-
// partially fails, redo unprocessedItems
|
|
112
|
-
acc = acc.concat(rs.value.UnprocessedItems[tableName].map((it) => it.PutRequest.Item));
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
return acc;
|
|
116
|
-
}, remainingItems.slice(maxWritingCapacity));
|
|
117
|
-
|
|
118
|
-
if (verbose) {
|
|
119
|
-
console.log(`processed=${prevRemainingCount - remainingItems.length}, remaining=${remainingItems.length}`);
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
if (rejection) {
|
|
124
|
-
console.log("batchCreateRecords rejected", rejection);
|
|
125
|
-
|
|
126
|
-
throw new Error(`batchCreateRecords rejected, failed items=${remainingItems.length}`);
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
if (remainingItems.length > 0) {
|
|
130
|
-
console.log(`failed batchCreateRecords, failed items=${remainingItems.length}`);
|
|
131
|
-
|
|
132
|
-
throw new Error(`batchCreateRecords retry failed, failed items=${remainingItems.length}`);
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
async function createRecord(tableName, fields, verbose = false) {
|
|
137
|
-
if (verbose) {
|
|
138
|
-
console.log("creating", tableName, fields);
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
const docClient = getDocClient();
|
|
142
|
-
const params = {
|
|
143
|
-
TableName: tableName,
|
|
144
|
-
Item: fields,
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
return docClient.put(params).promise();
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
async function getRecordsByKey(tableName, keys, indexName) {
|
|
151
|
-
const docClient = getDocClient();
|
|
152
|
-
|
|
153
|
-
const keyNames = Object.keys(keys);
|
|
154
|
-
const conditionExpression = keyNames.map((key) => `#${key} = :${key}`).join(" and ");
|
|
155
|
-
|
|
156
|
-
const params = {
|
|
157
|
-
TableName: tableName,
|
|
158
|
-
KeyConditionExpression: conditionExpression,
|
|
159
|
-
ExpressionAttributeNames: generateExpressionNames(keyNames),
|
|
160
|
-
ExpressionAttributeValues: generateExpressionValues(keyNames, keys),
|
|
161
|
-
};
|
|
162
|
-
|
|
163
|
-
if (indexName) {
|
|
164
|
-
params.IndexName = indexName;
|
|
165
|
-
}
|
|
166
|
-
|
|
167
|
-
let data;
|
|
168
|
-
|
|
169
|
-
try {
|
|
170
|
-
data = await docClient.query(params).promise();
|
|
171
|
-
return data.Items;
|
|
172
|
-
} catch (err) {
|
|
173
|
-
if (err.statusCode && err.statusCode === 400) {
|
|
174
|
-
return null;
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
throw err;
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
async function getRecordByKey(tableName, keys, indexName) {
|
|
182
|
-
const records = await getRecordsByKey(tableName, keys, indexName);
|
|
183
|
-
|
|
184
|
-
if (records) {
|
|
185
|
-
return records[0];
|
|
186
|
-
} else {
|
|
187
|
-
return null;
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function generateExpressionNames(keys) {
|
|
192
|
-
return keys.reduce((acc, key) => {
|
|
193
|
-
acc[`#${key}`] = key;
|
|
194
|
-
return acc;
|
|
195
|
-
}, {});
|
|
196
|
-
}
|
|
197
|
-
|
|
198
|
-
function generateExpressionValues(keys, fields) {
|
|
199
|
-
return keys.reduce((acc, key) => {
|
|
200
|
-
acc[`:${key}`] = fields[key];
|
|
201
|
-
return acc;
|
|
202
|
-
}, {});
|
|
203
|
-
}
|
|
204
|
-
|
|
205
|
-
async function updateRecordByKey(tableName, idKey, fields, conditionExpressions = null, verbose = false) {
|
|
206
|
-
if (verbose) {
|
|
207
|
-
console.log("update", tableName, idKey, fields);
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
const docClient = getDocClient();
|
|
211
|
-
|
|
212
|
-
const idKeyNames = Object.keys(idKey);
|
|
213
|
-
const fieldsToDelete = Object.keys(fields).filter((f) => fields[f] === undefined);
|
|
214
|
-
const fieldsToUpdate = Object.keys(fields).filter((k) => !idKeyNames.includes(k) && !fieldsToDelete.includes(k));
|
|
215
|
-
|
|
216
|
-
let data;
|
|
217
|
-
|
|
218
|
-
if (fieldsToDelete.length > 0) {
|
|
219
|
-
if (verbose) {
|
|
220
|
-
console.log("delete fields", tableName, fieldsToDelete);
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
const deleteParams = {
|
|
224
|
-
TableName: tableName,
|
|
225
|
-
Key: idKey,
|
|
226
|
-
ExpressionAttributeNames: generateExpressionNames(fieldsToDelete),
|
|
227
|
-
UpdateExpression: `REMOVE ${fieldsToDelete.map((f) => `#${f}`).join(", ")}`,
|
|
228
|
-
ReturnValues: "ALL_NEW",
|
|
229
|
-
};
|
|
230
|
-
|
|
231
|
-
if (conditionExpressions) {
|
|
232
|
-
deleteParams.ConditionExpression = conditionExpressions;
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
data = await docClient.update(deleteParams).promise();
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
if (fieldsToUpdate.length > 0) {
|
|
239
|
-
if (verbose) {
|
|
240
|
-
console.log("update fields", tableName, fieldsToUpdate);
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
const updateExpressions = fieldsToUpdate.map((key) => `#${key} = :${key}`);
|
|
244
|
-
|
|
245
|
-
const params = {
|
|
246
|
-
TableName: tableName,
|
|
247
|
-
Key: idKey,
|
|
248
|
-
ExpressionAttributeNames: generateExpressionNames(fieldsToUpdate),
|
|
249
|
-
ExpressionAttributeValues: generateExpressionValues(fieldsToUpdate, fields),
|
|
250
|
-
UpdateExpression: `SET ${updateExpressions.join(", ")}`,
|
|
251
|
-
ReturnValues: "ALL_NEW",
|
|
252
|
-
};
|
|
253
|
-
|
|
254
|
-
if (conditionExpressions) {
|
|
255
|
-
params.ConditionExpression = conditionExpressions;
|
|
256
|
-
}
|
|
257
|
-
|
|
258
|
-
data = await docClient.update(params).promise();
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
return data && data.Attributes;
|
|
262
|
-
}
|
|
263
|
-
|
|
264
|
-
async function createTableIfNotExist(
|
|
265
|
-
tableName,
|
|
266
|
-
attributeDefinitions = [
|
|
267
|
-
{
|
|
268
|
-
AttributeName: "id",
|
|
269
|
-
AttributeType: "S",
|
|
270
|
-
},
|
|
271
|
-
],
|
|
272
|
-
keySchema = [
|
|
273
|
-
{
|
|
274
|
-
AttributeName: "id",
|
|
275
|
-
KeyType: "HASH",
|
|
276
|
-
},
|
|
277
|
-
],
|
|
278
|
-
otherOptions = {},
|
|
279
|
-
verbose = false
|
|
280
|
-
) {
|
|
281
|
-
const dynamodb = getDynamoDB();
|
|
282
|
-
|
|
283
|
-
try {
|
|
284
|
-
const table = await dynamodb.describeTable({ TableName: tableName }).promise();
|
|
285
|
-
|
|
286
|
-
if (verbose) {
|
|
287
|
-
console.log(`table ${tableName} already exist`, table);
|
|
288
|
-
}
|
|
289
|
-
return table;
|
|
290
|
-
} catch (_tableDoesNotExistErr) {
|
|
291
|
-
const createRequest = await dynamodb
|
|
292
|
-
.createTable({
|
|
293
|
-
TableName: tableName,
|
|
294
|
-
AttributeDefinitions: attributeDefinitions,
|
|
295
|
-
KeySchema: keySchema,
|
|
296
|
-
ProvisionedThroughput: {
|
|
297
|
-
ReadCapacityUnits: 5,
|
|
298
|
-
WriteCapacityUnits: 5,
|
|
299
|
-
},
|
|
300
|
-
...otherOptions,
|
|
301
|
-
})
|
|
302
|
-
.promise();
|
|
303
|
-
|
|
304
|
-
if (verbose) {
|
|
305
|
-
console.log(`table ${tableName} creating`);
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
let isCreated = false;
|
|
309
|
-
|
|
310
|
-
while (!isCreated) {
|
|
311
|
-
await wait(5000);
|
|
312
|
-
|
|
313
|
-
const t = await dynamodb.describeTable({ TableName: tableName }).promise();
|
|
314
|
-
|
|
315
|
-
console.log("waiting for table to become active");
|
|
316
|
-
|
|
317
|
-
isCreated = t.Table.TableStatus === "ACTIVE";
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
return createRequest;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
async function batchDeleteRecords(tableName, keys) {
|
|
325
|
-
const docClient = getDocClient();
|
|
326
|
-
|
|
327
|
-
for (let start = 0; start < keys.length; start += 25) {
|
|
328
|
-
const slice = keys.slice(start, start + 25);
|
|
329
|
-
|
|
330
|
-
await docClient
|
|
331
|
-
.batchWrite({
|
|
332
|
-
RequestItems: {
|
|
333
|
-
[tableName]: slice.map((key) => {
|
|
334
|
-
return { DeleteRequest: { Key: key } };
|
|
335
|
-
}),
|
|
336
|
-
},
|
|
337
|
-
})
|
|
338
|
-
.promise();
|
|
339
|
-
}
|
|
340
|
-
}
|
|
341
|
-
|
|
342
|
-
function getKeyName(keySchema, type) {
|
|
343
|
-
const key = keySchema.find((k) => k.KeyType === type);
|
|
344
|
-
|
|
345
|
-
return key && key.AttributeName;
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
function getIndexKeyName(globalSecondaryIndexes, indexName, type) {
|
|
349
|
-
const idx = globalSecondaryIndexes.find((i) => i.IndexName === indexName);
|
|
350
|
-
|
|
351
|
-
return idx && getKeyName(idx.KeySchema, type);
|
|
352
|
-
}
|
|
353
|
-
|
|
354
|
-
async function deleteRecordsByHashKey(tableName, indexName, hashKeyValue, verbose = false) {
|
|
355
|
-
const docClient = getDocClient();
|
|
356
|
-
|
|
357
|
-
const meta = await getDynamoDB().describeTable({ TableName: tableName }).promise();
|
|
358
|
-
|
|
359
|
-
const hashKeyName = indexName
|
|
360
|
-
? getIndexKeyName(meta.Table.GlobalSecondaryIndexes, indexName, "HASH")
|
|
361
|
-
: getKeyName(meta.Table.KeySchema, "HASH");
|
|
362
|
-
|
|
363
|
-
if (!hashKeyName) {
|
|
364
|
-
throw new Error(`cannot find corresponding index`, indexName);
|
|
365
|
-
}
|
|
366
|
-
|
|
367
|
-
const mainHashKeyName = getKeyName(meta.Table.KeySchema, "HASH");
|
|
368
|
-
const mainRangeKeyName = getKeyName(meta.Table.KeySchema, "RANGE");
|
|
369
|
-
|
|
370
|
-
let totalDeleted = 0;
|
|
371
|
-
|
|
372
|
-
let params = {
|
|
373
|
-
TableName: tableName,
|
|
374
|
-
KeyConditionExpression: "#hashKeyName = :hashKeyValue",
|
|
375
|
-
ExpressionAttributeNames: { "#hashKeyName": hashKeyName },
|
|
376
|
-
ExpressionAttributeValues: { ":hashKeyValue": hashKeyValue },
|
|
377
|
-
};
|
|
378
|
-
|
|
379
|
-
if (indexName) {
|
|
380
|
-
params.IndexName = indexName;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
let data = await docClient.query(params).promise();
|
|
384
|
-
|
|
385
|
-
await batchDeleteRecords(
|
|
386
|
-
tableName,
|
|
387
|
-
data.Items.map((item) =>
|
|
388
|
-
mainRangeKeyName
|
|
389
|
-
? {
|
|
390
|
-
[mainHashKeyName]: item[mainHashKeyName],
|
|
391
|
-
[mainRangeKeyName]: item[mainRangeKeyName],
|
|
392
|
-
}
|
|
393
|
-
: {
|
|
394
|
-
[mainHashKeyName]: item[mainHashKeyName],
|
|
395
|
-
}
|
|
396
|
-
)
|
|
397
|
-
);
|
|
398
|
-
|
|
399
|
-
totalDeleted += data.Items.length;
|
|
400
|
-
|
|
401
|
-
while (data.LastEvaluatedKey) {
|
|
402
|
-
data = await docClient
|
|
403
|
-
.query({
|
|
404
|
-
...params,
|
|
405
|
-
ExclusiveStartKey: data.LastEvaluatedKey,
|
|
406
|
-
})
|
|
407
|
-
.promise();
|
|
408
|
-
|
|
409
|
-
await batchDeleteRecords(
|
|
410
|
-
tableName,
|
|
411
|
-
data.Items.map((item) =>
|
|
412
|
-
mainRangeKeyName
|
|
413
|
-
? {
|
|
414
|
-
[mainHashKeyName]: item[mainHashKeyName],
|
|
415
|
-
[mainRangeKeyName]: item[mainRangeKeyName],
|
|
416
|
-
}
|
|
417
|
-
: {
|
|
418
|
-
[mainHashKeyName]: item[mainHashKeyName],
|
|
419
|
-
}
|
|
420
|
-
)
|
|
421
|
-
);
|
|
422
|
-
|
|
423
|
-
totalDeleted += data.Items.length;
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
if (verbose) {
|
|
427
|
-
console.log(`successfully delete ${totalDeleted} items`);
|
|
428
|
-
}
|
|
429
|
-
|
|
430
|
-
return totalDeleted;
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
module.exports = {
|
|
434
|
-
getDocClient,
|
|
435
|
-
scanWholeTable,
|
|
436
|
-
batchCreateRecords,
|
|
437
|
-
createRecord,
|
|
438
|
-
getRecordsByKey,
|
|
439
|
-
getRecordByKey,
|
|
440
|
-
updateRecordByKey,
|
|
441
|
-
batchDeleteRecords,
|
|
442
|
-
deleteRecordsByHashKey,
|
|
443
|
-
createTableIfNotExist,
|
|
444
|
-
};
|
|
1
|
+
const { DynamoDB } = require("aws-sdk");
|
|
2
|
+
const { getAWSAccessKeyId, getAWSSecretAccessKey, getAWSRegion } = require("./env");
|
|
3
|
+
const { wait } = require("./availability");
|
|
4
|
+
|
|
5
|
+
function getDynamoDB() {
|
|
6
|
+
return new DynamoDB({
|
|
7
|
+
accessKeyId: getAWSAccessKeyId(),
|
|
8
|
+
secretAccessKey: getAWSSecretAccessKey(),
|
|
9
|
+
region: getAWSRegion(),
|
|
10
|
+
});
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getDocClient() {
|
|
14
|
+
return new DynamoDB.DocumentClient({
|
|
15
|
+
accessKeyId: getAWSAccessKeyId(),
|
|
16
|
+
secretAccessKey: getAWSSecretAccessKey(),
|
|
17
|
+
region: getAWSRegion(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function mergeQueries(q1, q2) {
|
|
22
|
+
return {
|
|
23
|
+
Items: q1.Items.concat(q2.Items),
|
|
24
|
+
Count: q1.Count + q2.Count,
|
|
25
|
+
ScannedCount: q1.ScannedCount + q2.ScannedCount,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function scanWholeTable(options) {
|
|
30
|
+
const dynamodb = getDocClient();
|
|
31
|
+
|
|
32
|
+
let items = {
|
|
33
|
+
Items: [],
|
|
34
|
+
Count: 0,
|
|
35
|
+
ScannedCount: 0,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
let data = await dynamodb.scan(options).promise();
|
|
39
|
+
|
|
40
|
+
while (data.LastEvaluatedKey) {
|
|
41
|
+
items = mergeQueries(items, data);
|
|
42
|
+
|
|
43
|
+
data = await dynamodb
|
|
44
|
+
.scan({
|
|
45
|
+
ExclusiveStartKey: data.LastEvaluatedKey,
|
|
46
|
+
...options,
|
|
47
|
+
})
|
|
48
|
+
.promise();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
items = mergeQueries(items, data);
|
|
52
|
+
|
|
53
|
+
return items;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function toSlices(items) {
|
|
57
|
+
let slices = [];
|
|
58
|
+
|
|
59
|
+
for (let start = 0; start < items.length; start += 25) {
|
|
60
|
+
slices.push(items.slice(start, start + 25));
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return slices;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function batchCreateRecords(tableName, records, maxWritingCapacity, verbose = false) {
|
|
67
|
+
if (verbose) {
|
|
68
|
+
console.log(`creating ${records.length} items in ${tableName}`);
|
|
69
|
+
}
|
|
70
|
+
const docClient = getDocClient();
|
|
71
|
+
|
|
72
|
+
let remainingItems = records;
|
|
73
|
+
// an arbitrary number to ensure while first loop is entered
|
|
74
|
+
let prevRemainingCount = remainingItems.length + 1;
|
|
75
|
+
let factor = 1;
|
|
76
|
+
let rejection = undefined;
|
|
77
|
+
|
|
78
|
+
while (remainingItems.length > 0 && factor <= 128 && !rejection) {
|
|
79
|
+
if (prevRemainingCount === remainingItems.length) {
|
|
80
|
+
await wait(5000 * factor);
|
|
81
|
+
factor = factor * 2;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (factor >= 32) {
|
|
85
|
+
console.log(`WARNING: no progress for a long time for batchCreateRecords, please check`);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const slices = toSlices(remainingItems.slice(0, maxWritingCapacity));
|
|
89
|
+
const results = await Promise.allSettled(
|
|
90
|
+
slices.map((rs) => {
|
|
91
|
+
const params = {
|
|
92
|
+
RequestItems: {
|
|
93
|
+
[tableName]: rs.map((record) => {
|
|
94
|
+
return { PutRequest: { Item: record } };
|
|
95
|
+
}),
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
|
|
99
|
+
return docClient.batchWrite(params).promise();
|
|
100
|
+
})
|
|
101
|
+
);
|
|
102
|
+
|
|
103
|
+
prevRemainingCount = remainingItems.length;
|
|
104
|
+
|
|
105
|
+
remainingItems = results.reduce((acc, rs, idx) => {
|
|
106
|
+
if (rs.status === "rejected") {
|
|
107
|
+
// whole slice fails, redo whole slice
|
|
108
|
+
acc = acc.concat(slices[idx]);
|
|
109
|
+
rejection = rs;
|
|
110
|
+
} else if (Object.keys(rs.value.UnprocessedItems).length > 0) {
|
|
111
|
+
// partially fails, redo unprocessedItems
|
|
112
|
+
acc = acc.concat(rs.value.UnprocessedItems[tableName].map((it) => it.PutRequest.Item));
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return acc;
|
|
116
|
+
}, remainingItems.slice(maxWritingCapacity));
|
|
117
|
+
|
|
118
|
+
if (verbose) {
|
|
119
|
+
console.log(`processed=${prevRemainingCount - remainingItems.length}, remaining=${remainingItems.length}`);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (rejection) {
|
|
124
|
+
console.log("batchCreateRecords rejected", rejection);
|
|
125
|
+
|
|
126
|
+
throw new Error(`batchCreateRecords rejected, failed items=${remainingItems.length}`);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (remainingItems.length > 0) {
|
|
130
|
+
console.log(`failed batchCreateRecords, failed items=${remainingItems.length}`);
|
|
131
|
+
|
|
132
|
+
throw new Error(`batchCreateRecords retry failed, failed items=${remainingItems.length}`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function createRecord(tableName, fields, verbose = false) {
|
|
137
|
+
if (verbose) {
|
|
138
|
+
console.log("creating", tableName, fields);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const docClient = getDocClient();
|
|
142
|
+
const params = {
|
|
143
|
+
TableName: tableName,
|
|
144
|
+
Item: fields,
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
return docClient.put(params).promise();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
async function getRecordsByKey(tableName, keys, indexName) {
|
|
151
|
+
const docClient = getDocClient();
|
|
152
|
+
|
|
153
|
+
const keyNames = Object.keys(keys);
|
|
154
|
+
const conditionExpression = keyNames.map((key) => `#${key} = :${key}`).join(" and ");
|
|
155
|
+
|
|
156
|
+
const params = {
|
|
157
|
+
TableName: tableName,
|
|
158
|
+
KeyConditionExpression: conditionExpression,
|
|
159
|
+
ExpressionAttributeNames: generateExpressionNames(keyNames),
|
|
160
|
+
ExpressionAttributeValues: generateExpressionValues(keyNames, keys),
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
if (indexName) {
|
|
164
|
+
params.IndexName = indexName;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
let data;
|
|
168
|
+
|
|
169
|
+
try {
|
|
170
|
+
data = await docClient.query(params).promise();
|
|
171
|
+
return data.Items;
|
|
172
|
+
} catch (err) {
|
|
173
|
+
if (err.statusCode && err.statusCode === 400) {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
throw err;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function getRecordByKey(tableName, keys, indexName) {
|
|
182
|
+
const records = await getRecordsByKey(tableName, keys, indexName);
|
|
183
|
+
|
|
184
|
+
if (records) {
|
|
185
|
+
return records[0];
|
|
186
|
+
} else {
|
|
187
|
+
return null;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
function generateExpressionNames(keys) {
|
|
192
|
+
return keys.reduce((acc, key) => {
|
|
193
|
+
acc[`#${key}`] = key;
|
|
194
|
+
return acc;
|
|
195
|
+
}, {});
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function generateExpressionValues(keys, fields) {
|
|
199
|
+
return keys.reduce((acc, key) => {
|
|
200
|
+
acc[`:${key}`] = fields[key];
|
|
201
|
+
return acc;
|
|
202
|
+
}, {});
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
async function updateRecordByKey(tableName, idKey, fields, conditionExpressions = null, verbose = false) {
|
|
206
|
+
if (verbose) {
|
|
207
|
+
console.log("update", tableName, idKey, fields);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
const docClient = getDocClient();
|
|
211
|
+
|
|
212
|
+
const idKeyNames = Object.keys(idKey);
|
|
213
|
+
const fieldsToDelete = Object.keys(fields).filter((f) => fields[f] === undefined);
|
|
214
|
+
const fieldsToUpdate = Object.keys(fields).filter((k) => !idKeyNames.includes(k) && !fieldsToDelete.includes(k));
|
|
215
|
+
|
|
216
|
+
let data;
|
|
217
|
+
|
|
218
|
+
if (fieldsToDelete.length > 0) {
|
|
219
|
+
if (verbose) {
|
|
220
|
+
console.log("delete fields", tableName, fieldsToDelete);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const deleteParams = {
|
|
224
|
+
TableName: tableName,
|
|
225
|
+
Key: idKey,
|
|
226
|
+
ExpressionAttributeNames: generateExpressionNames(fieldsToDelete),
|
|
227
|
+
UpdateExpression: `REMOVE ${fieldsToDelete.map((f) => `#${f}`).join(", ")}`,
|
|
228
|
+
ReturnValues: "ALL_NEW",
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
if (conditionExpressions) {
|
|
232
|
+
deleteParams.ConditionExpression = conditionExpressions;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
data = await docClient.update(deleteParams).promise();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
if (fieldsToUpdate.length > 0) {
|
|
239
|
+
if (verbose) {
|
|
240
|
+
console.log("update fields", tableName, fieldsToUpdate);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
const updateExpressions = fieldsToUpdate.map((key) => `#${key} = :${key}`);
|
|
244
|
+
|
|
245
|
+
const params = {
|
|
246
|
+
TableName: tableName,
|
|
247
|
+
Key: idKey,
|
|
248
|
+
ExpressionAttributeNames: generateExpressionNames(fieldsToUpdate),
|
|
249
|
+
ExpressionAttributeValues: generateExpressionValues(fieldsToUpdate, fields),
|
|
250
|
+
UpdateExpression: `SET ${updateExpressions.join(", ")}`,
|
|
251
|
+
ReturnValues: "ALL_NEW",
|
|
252
|
+
};
|
|
253
|
+
|
|
254
|
+
if (conditionExpressions) {
|
|
255
|
+
params.ConditionExpression = conditionExpressions;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
data = await docClient.update(params).promise();
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return data && data.Attributes;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
async function createTableIfNotExist(
|
|
265
|
+
tableName,
|
|
266
|
+
attributeDefinitions = [
|
|
267
|
+
{
|
|
268
|
+
AttributeName: "id",
|
|
269
|
+
AttributeType: "S",
|
|
270
|
+
},
|
|
271
|
+
],
|
|
272
|
+
keySchema = [
|
|
273
|
+
{
|
|
274
|
+
AttributeName: "id",
|
|
275
|
+
KeyType: "HASH",
|
|
276
|
+
},
|
|
277
|
+
],
|
|
278
|
+
otherOptions = {},
|
|
279
|
+
verbose = false
|
|
280
|
+
) {
|
|
281
|
+
const dynamodb = getDynamoDB();
|
|
282
|
+
|
|
283
|
+
try {
|
|
284
|
+
const table = await dynamodb.describeTable({ TableName: tableName }).promise();
|
|
285
|
+
|
|
286
|
+
if (verbose) {
|
|
287
|
+
console.log(`table ${tableName} already exist`, table);
|
|
288
|
+
}
|
|
289
|
+
return table;
|
|
290
|
+
} catch (_tableDoesNotExistErr) {
|
|
291
|
+
const createRequest = await dynamodb
|
|
292
|
+
.createTable({
|
|
293
|
+
TableName: tableName,
|
|
294
|
+
AttributeDefinitions: attributeDefinitions,
|
|
295
|
+
KeySchema: keySchema,
|
|
296
|
+
ProvisionedThroughput: {
|
|
297
|
+
ReadCapacityUnits: 5,
|
|
298
|
+
WriteCapacityUnits: 5,
|
|
299
|
+
},
|
|
300
|
+
...otherOptions,
|
|
301
|
+
})
|
|
302
|
+
.promise();
|
|
303
|
+
|
|
304
|
+
if (verbose) {
|
|
305
|
+
console.log(`table ${tableName} creating`);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
let isCreated = false;
|
|
309
|
+
|
|
310
|
+
while (!isCreated) {
|
|
311
|
+
await wait(5000);
|
|
312
|
+
|
|
313
|
+
const t = await dynamodb.describeTable({ TableName: tableName }).promise();
|
|
314
|
+
|
|
315
|
+
console.log("waiting for table to become active");
|
|
316
|
+
|
|
317
|
+
isCreated = t.Table.TableStatus === "ACTIVE";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
return createRequest;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
async function batchDeleteRecords(tableName, keys) {
|
|
325
|
+
const docClient = getDocClient();
|
|
326
|
+
|
|
327
|
+
for (let start = 0; start < keys.length; start += 25) {
|
|
328
|
+
const slice = keys.slice(start, start + 25);
|
|
329
|
+
|
|
330
|
+
await docClient
|
|
331
|
+
.batchWrite({
|
|
332
|
+
RequestItems: {
|
|
333
|
+
[tableName]: slice.map((key) => {
|
|
334
|
+
return { DeleteRequest: { Key: key } };
|
|
335
|
+
}),
|
|
336
|
+
},
|
|
337
|
+
})
|
|
338
|
+
.promise();
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function getKeyName(keySchema, type) {
|
|
343
|
+
const key = keySchema.find((k) => k.KeyType === type);
|
|
344
|
+
|
|
345
|
+
return key && key.AttributeName;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
function getIndexKeyName(globalSecondaryIndexes, indexName, type) {
|
|
349
|
+
const idx = globalSecondaryIndexes.find((i) => i.IndexName === indexName);
|
|
350
|
+
|
|
351
|
+
return idx && getKeyName(idx.KeySchema, type);
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
async function deleteRecordsByHashKey(tableName, indexName, hashKeyValue, verbose = false) {
|
|
355
|
+
const docClient = getDocClient();
|
|
356
|
+
|
|
357
|
+
const meta = await getDynamoDB().describeTable({ TableName: tableName }).promise();
|
|
358
|
+
|
|
359
|
+
const hashKeyName = indexName
|
|
360
|
+
? getIndexKeyName(meta.Table.GlobalSecondaryIndexes, indexName, "HASH")
|
|
361
|
+
: getKeyName(meta.Table.KeySchema, "HASH");
|
|
362
|
+
|
|
363
|
+
if (!hashKeyName) {
|
|
364
|
+
throw new Error(`cannot find corresponding index`, indexName);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
const mainHashKeyName = getKeyName(meta.Table.KeySchema, "HASH");
|
|
368
|
+
const mainRangeKeyName = getKeyName(meta.Table.KeySchema, "RANGE");
|
|
369
|
+
|
|
370
|
+
let totalDeleted = 0;
|
|
371
|
+
|
|
372
|
+
let params = {
|
|
373
|
+
TableName: tableName,
|
|
374
|
+
KeyConditionExpression: "#hashKeyName = :hashKeyValue",
|
|
375
|
+
ExpressionAttributeNames: { "#hashKeyName": hashKeyName },
|
|
376
|
+
ExpressionAttributeValues: { ":hashKeyValue": hashKeyValue },
|
|
377
|
+
};
|
|
378
|
+
|
|
379
|
+
if (indexName) {
|
|
380
|
+
params.IndexName = indexName;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
let data = await docClient.query(params).promise();
|
|
384
|
+
|
|
385
|
+
await batchDeleteRecords(
|
|
386
|
+
tableName,
|
|
387
|
+
data.Items.map((item) =>
|
|
388
|
+
mainRangeKeyName
|
|
389
|
+
? {
|
|
390
|
+
[mainHashKeyName]: item[mainHashKeyName],
|
|
391
|
+
[mainRangeKeyName]: item[mainRangeKeyName],
|
|
392
|
+
}
|
|
393
|
+
: {
|
|
394
|
+
[mainHashKeyName]: item[mainHashKeyName],
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
);
|
|
398
|
+
|
|
399
|
+
totalDeleted += data.Items.length;
|
|
400
|
+
|
|
401
|
+
while (data.LastEvaluatedKey) {
|
|
402
|
+
data = await docClient
|
|
403
|
+
.query({
|
|
404
|
+
...params,
|
|
405
|
+
ExclusiveStartKey: data.LastEvaluatedKey,
|
|
406
|
+
})
|
|
407
|
+
.promise();
|
|
408
|
+
|
|
409
|
+
await batchDeleteRecords(
|
|
410
|
+
tableName,
|
|
411
|
+
data.Items.map((item) =>
|
|
412
|
+
mainRangeKeyName
|
|
413
|
+
? {
|
|
414
|
+
[mainHashKeyName]: item[mainHashKeyName],
|
|
415
|
+
[mainRangeKeyName]: item[mainRangeKeyName],
|
|
416
|
+
}
|
|
417
|
+
: {
|
|
418
|
+
[mainHashKeyName]: item[mainHashKeyName],
|
|
419
|
+
}
|
|
420
|
+
)
|
|
421
|
+
);
|
|
422
|
+
|
|
423
|
+
totalDeleted += data.Items.length;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
if (verbose) {
|
|
427
|
+
console.log(`successfully delete ${totalDeleted} items`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
return totalDeleted;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
module.exports = {
|
|
434
|
+
getDocClient,
|
|
435
|
+
scanWholeTable,
|
|
436
|
+
batchCreateRecords,
|
|
437
|
+
createRecord,
|
|
438
|
+
getRecordsByKey,
|
|
439
|
+
getRecordByKey,
|
|
440
|
+
updateRecordByKey,
|
|
441
|
+
batchDeleteRecords,
|
|
442
|
+
deleteRecordsByHashKey,
|
|
443
|
+
createTableIfNotExist,
|
|
444
|
+
};
|