serverless-simple-middleware 0.0.62 → 0.0.63

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/src/aws/simple.ts CHANGED
@@ -1,686 +1,686 @@
1
- import {
2
- CloudfrontSignedCookiesOutput,
3
- getSignedCookies,
4
- } from '@aws-sdk/cloudfront-signer';
5
-
6
- import { envDefault as currentStage } from 'simple-staging';
7
-
8
- import * as fs from 'fs';
9
- import * as os from 'os';
10
- import { nanoid } from 'nanoid/non-secure';
11
-
12
- import { getLogger, stringifyError } from '../utils';
13
- import { SimpleAWSConfig } from './config';
14
-
15
- import { AWSComponent, SQSMessageBody } from './define';
16
- import { DynamoDB, DynamoDBClient } from '@aws-sdk/client-dynamodb';
17
- import {
18
- AbortMultipartUploadCommand,
19
- CompleteMultipartUploadCommand,
20
- CopyObjectCommand,
21
- CreateMultipartUploadCommand,
22
- DeleteObjectCommand,
23
- GetObjectCommand,
24
- HeadObjectCommand,
25
- ListObjectsV2Command,
26
- ListPartsCommand,
27
- PutObjectCommand,
28
- S3,
29
- Tag,
30
- UploadPartCommand,
31
- UploadPartCopyCommand,
32
- } from '@aws-sdk/client-s3';
33
- import { SQS } from '@aws-sdk/client-sqs';
34
- import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
35
- import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
36
- import { PresignerOptions } from '../internal/s3';
37
- import { Upload } from '@aws-sdk/lib-storage';
38
-
39
- const logger = getLogger(__filename);
40
-
41
- export class SimpleAWS {
42
- private queueUrls: { [queueName: string]: string } = {};
43
- private config: SimpleAWSConfig;
44
- private lazyS3: S3 | undefined;
45
- private lazySqs: SQS | undefined;
46
- private lazyDynamodb: DynamoDBDocument | undefined;
47
- private lazyDynamodbAdmin: DynamoDB | undefined;
48
- private static readonly stageTag: Tag = {
49
- Key: 'STAGE',
50
- Value: currentStage.level,
51
- };
52
- private static readonly stringifiedStageTag: string = `STAGE=${currentStage.level}`;
53
-
54
- constructor(config?: SimpleAWSConfig) {
55
- this.config = config || new SimpleAWSConfig();
56
- /**
57
- * The simple cache for { queueName: queueUrl }.
58
- * It can help in the only case of launching this project as offline.
59
- * @type { { [queueName: string]: string } }
60
- */
61
- this.queueUrls = {};
62
- }
63
-
64
- get s3() {
65
- if (this.lazyS3 === undefined) {
66
- this.lazyS3 = new S3(this.config.get(AWSComponent.s3) || {});
67
- }
68
- return this.lazyS3;
69
- }
70
-
71
- get sqs() {
72
- if (this.lazySqs === undefined) {
73
- this.lazySqs = new SQS(this.config.get(AWSComponent.sqs) || {});
74
- }
75
- return this.lazySqs;
76
- }
77
-
78
- get dynamodb() {
79
- if (this.lazyDynamodb === undefined) {
80
- this.lazyDynamodb = DynamoDBDocument.from(
81
- new DynamoDBClient(this.config.get(AWSComponent.dynamodb) || {}),
82
- {
83
- marshallOptions: {
84
- convertEmptyValues: true,
85
- removeUndefinedValues: true,
86
- },
87
- },
88
- );
89
- }
90
- return this.lazyDynamodb;
91
- }
92
-
93
- get dynamodbAdmin() {
94
- if (this.lazyDynamodbAdmin === undefined) {
95
- this.lazyDynamodbAdmin = new DynamoDB(
96
- this.config.get(AWSComponent.dynamodb) || {},
97
- );
98
- }
99
- return this.lazyDynamodbAdmin;
100
- }
101
-
102
- public getQueueUrl = async (queueName: string): Promise<string> => {
103
- if (this.queueUrls[queueName] !== undefined) {
104
- return this.queueUrls[queueName];
105
- }
106
- const urlResult = await this.sqs.getQueueUrl({
107
- QueueName: queueName,
108
- });
109
- logger.stupid(`urlResult`, urlResult);
110
- if (!urlResult.QueueUrl) {
111
- throw new Error(`No queue url with name[${queueName}]`);
112
- }
113
- return (this.queueUrls[queueName] = urlResult.QueueUrl);
114
- };
115
-
116
- public enqueue = async (queueName: string, data: any): Promise<number> => {
117
- logger.debug(`Send message[${data.key}] to queue.`);
118
- logger.stupid(`data`, data);
119
- const queueUrl = await this.getQueueUrl(queueName);
120
- const sendResult = await this.sqs.sendMessage({
121
- QueueUrl: queueUrl,
122
- MessageBody: JSON.stringify(data),
123
- DelaySeconds: 0,
124
- });
125
- logger.stupid(`sendResult`, sendResult);
126
-
127
- const attrResult = await this.sqs.getQueueAttributes({
128
- QueueUrl: queueUrl,
129
- AttributeNames: ['ApproximateNumberOfMessages'],
130
- });
131
- logger.stupid(`attrResult`, attrResult);
132
- if (!attrResult.Attributes) {
133
- return 0;
134
- }
135
- return +(attrResult.Attributes?.ApproximateNumberOfMessages || 0);
136
- };
137
-
138
- public dequeue = async <T>(
139
- queueName: string,
140
- fetchSize: number = 1,
141
- waitSeconds: number = 1,
142
- visibilityTimeout: number = 15,
143
- ): Promise<Array<SQSMessageBody<T>>> => {
144
- logger.debug(`Receive message from queue[${queueName}].`);
145
- const queueUrl = await this.getQueueUrl(queueName);
146
- const receiveResult = await this.sqs.receiveMessage({
147
- QueueUrl: queueUrl,
148
- MaxNumberOfMessages: fetchSize,
149
- WaitTimeSeconds: waitSeconds,
150
- VisibilityTimeout: visibilityTimeout,
151
- });
152
- logger.stupid(`receiveResult`, receiveResult);
153
- if (
154
- receiveResult.Messages === undefined ||
155
- receiveResult.Messages.length === 0
156
- ) {
157
- return [];
158
- }
159
- const data = [];
160
- for (const each of receiveResult.Messages) {
161
- if (!each.ReceiptHandle) {
162
- logger.warn(`No receipt handler: ${JSON.stringify(each)}`);
163
- continue;
164
- }
165
- const message: SQSMessageBody<T> = {
166
- handle: each.ReceiptHandle,
167
- body: each.Body ? (JSON.parse(each.Body) as T) : undefined,
168
- };
169
- data.push(message);
170
- }
171
- logger.verbose(`Receive a message[${JSON.stringify(data)}] from queue`);
172
- return data;
173
- };
174
-
175
- public dequeueAll = async <T>(
176
- queueName: string,
177
- limitSize: number = Number.MAX_VALUE,
178
- visibilityTimeout: number = 15,
179
- ): Promise<Array<SQSMessageBody<T>>> => {
180
- const messages = [];
181
- const maxFetchSize = 10; // This is max-value for fetching in each time.
182
- while (messages.length < limitSize) {
183
- const eachOfMessages: Array<SQSMessageBody<T>> = await this.dequeue<T>(
184
- queueName,
185
- Math.min(limitSize - messages.length, maxFetchSize),
186
- 0,
187
- visibilityTimeout,
188
- );
189
- if (!eachOfMessages || eachOfMessages.length === 0) {
190
- break;
191
- }
192
- for (const each of eachOfMessages) {
193
- messages.push(each);
194
- }
195
- }
196
- logger.stupid(`messages`, messages);
197
- return messages;
198
- };
199
-
200
- public retainMessage = async (
201
- queueName: string,
202
- handle: string,
203
- seconds: number,
204
- ): Promise<string> => {
205
- logger.debug(`Change visibilityTimeout of ${handle} to ${seconds}secs.`);
206
- const queueUrl = await this.getQueueUrl(queueName);
207
-
208
- await this.sqs.changeMessageVisibility({
209
- QueueUrl: queueUrl,
210
- ReceiptHandle: handle,
211
- VisibilityTimeout: seconds,
212
- });
213
-
214
- return handle;
215
- };
216
-
217
- public completeMessage = async (
218
- queueName: string,
219
- handle: string,
220
- ): Promise<string> => {
221
- logger.debug(`Complete a message with handle[${handle}]`);
222
- const queueUrl = await this.getQueueUrl(queueName);
223
- const deleteResult = await this.sqs.deleteMessage({
224
- QueueUrl: queueUrl,
225
- ReceiptHandle: handle,
226
- });
227
- logger.stupid(`deleteResult`, deleteResult);
228
- return handle;
229
- };
230
-
231
- public completeMessages = async (queueName: string, handles: string[]) => {
232
- logger.debug(`Complete a message with handle[${handles}]`);
233
- if (!handles) {
234
- return handles;
235
- }
236
-
237
- const chunkSize = 10;
238
- let index = 0;
239
- for (let start = 0; start < handles.length; start += chunkSize) {
240
- const end = Math.min(start + chunkSize, handles.length);
241
- const sublist = handles.slice(start, end);
242
- const queueUrl = await this.getQueueUrl(queueName);
243
- const deletesResult = await this.sqs.deleteMessageBatch({
244
- QueueUrl: queueUrl,
245
- Entries: sublist.map((handle) => ({
246
- Id: (++index).toString(),
247
- ReceiptHandle: handle,
248
- })),
249
- });
250
- logger.stupid(`deleteResult`, deletesResult);
251
- }
252
- return handles;
253
- };
254
-
255
- public download = async (
256
- bucket: string,
257
- key: string,
258
- localPath: string,
259
- ): Promise<string> => {
260
- logger.debug(`Get a stream of item[${key}] from bucket[${bucket}]`);
261
- const { Body } = await this.s3.getObject({ Bucket: bucket, Key: key });
262
-
263
- return new Promise<string>((resolve, reject) =>
264
- (Body as NodeJS.ReadableStream)
265
- .on('error', (error) => reject(error))
266
- .pipe(fs.createWriteStream(localPath))
267
- .on('finish', () => resolve(localPath))
268
- .on('error', (error) => reject(error)),
269
- );
270
- };
271
-
272
- public readFile = async (bucket: string, key: string): Promise<string> => {
273
- logger.debug(`Read item[${key}] from bucket[${bucket}]`);
274
- const tempFile = `${os.tmpdir()}/${nanoid()}`;
275
- try {
276
- await this.download(bucket, key, tempFile);
277
- const content = await fs.promises.readFile(tempFile, {
278
- encoding: 'utf-8',
279
- });
280
- return content;
281
- } finally {
282
- if (fs.existsSync(tempFile)) {
283
- try {
284
- await fs.promises.unlink(tempFile);
285
- } catch (error) {
286
- logger.error(
287
- `Failed to delete temp file ${tempFile}: ${stringifyError(error)}`,
288
- );
289
- }
290
- }
291
- }
292
- };
293
-
294
- public readFileBuffer = async (
295
- bucket: string,
296
- key: string,
297
- ): Promise<Buffer> => {
298
- logger.debug(`Read item[${key}] from bucket[${bucket}]`);
299
- const { Body } = await this.s3.getObject({ Bucket: bucket, Key: key });
300
-
301
- const buffer = await Body?.transformToByteArray();
302
- if (!buffer) {
303
- throw new Error(`Failed to read file ${key} from bucket ${bucket}`);
304
- }
305
- return Buffer.from(buffer);
306
- };
307
-
308
- public upload = async (
309
- bucket: string,
310
- localPath: string,
311
- key: string,
312
- tags?: Tag[],
313
- ): Promise<string> => {
314
- logger.debug(`Upload item[${key}] into bucket[${bucket}]`);
315
- const upload = new Upload({
316
- client: this.s3,
317
- params: {
318
- Bucket: bucket,
319
- Key: key,
320
- Body: fs.createReadStream(localPath),
321
- },
322
- partSize: 5 * 1024 * 1024, // 5MB
323
- queueSize: 4,
324
- tags: [SimpleAWS.stageTag, ...(tags || [])],
325
- });
326
-
327
- await upload.done();
328
- return key;
329
- };
330
-
331
- public uploadFromBuffer = async (
332
- bucket: string,
333
- key: string,
334
- buffer: Buffer,
335
- tags?: Tag[],
336
- ): Promise<string> => {
337
- logger.debug(`Upload item[${key}] into bucket[${bucket}]`);
338
- const upload = new Upload({
339
- client: this.s3,
340
- params: {
341
- Bucket: bucket,
342
- Key: key,
343
- Body: buffer,
344
- },
345
- partSize: 5 * 1024 * 1024, // 5MB
346
- queueSize: 4,
347
- tags: [SimpleAWS.stageTag, ...(tags || [])],
348
- });
349
- await upload.done();
350
- return key;
351
- };
352
-
353
- public writeFile = async (
354
- bucket: string,
355
- key: string,
356
- content: string,
357
- ): Promise<void> => {
358
- logger.debug(`Write item[${key}] into bucket[${bucket}]`);
359
- const tempFile = `${os.tmpdir()}/${nanoid()}`;
360
- try {
361
- await fs.promises.writeFile(tempFile, content, 'utf-8');
362
- await this.upload(bucket, tempFile, key);
363
- } finally {
364
- if (!fs.existsSync(tempFile)) {
365
- return;
366
- }
367
- try {
368
- await fs.promises.unlink(tempFile);
369
- } catch (error) {
370
- const msg = `Error during writeFile: unlink file ${tempFile}: ${stringifyError(
371
- error,
372
- )}`;
373
- logger.error(msg);
374
- }
375
- }
376
- };
377
-
378
- public async getSignedUrl(options: PresignerOptions): Promise<string> {
379
- const { expiresIn = 600, unhoistableHeaders } = options;
380
- switch (options.operation) {
381
- case 'putObject': {
382
- const tagging = options.params?.Tagging
383
- ? SimpleAWS.stringifiedStageTag + '&' + options.params.Tagging
384
- : SimpleAWS.stringifiedStageTag;
385
- const cmd = new PutObjectCommand({
386
- Bucket: options.bucket,
387
- Key: options.key,
388
- ...options.params,
389
- Tagging: tagging,
390
- });
391
- return getSignedUrl(this.s3, cmd, {
392
- expiresIn: expiresIn,
393
- unhoistableHeaders,
394
- });
395
- }
396
- case 'getObject': {
397
- const cmd = new GetObjectCommand({
398
- Bucket: options.bucket,
399
- Key: options.key,
400
- ...options.params,
401
- });
402
- return getSignedUrl(this.s3, cmd, {
403
- expiresIn: expiresIn,
404
- unhoistableHeaders,
405
- });
406
- }
407
- case 'deleteObject': {
408
- const cmd = new DeleteObjectCommand({
409
- Bucket: options.bucket,
410
- Key: options.key,
411
- ...options.params,
412
- });
413
- return getSignedUrl(this.s3, cmd, {
414
- expiresIn: expiresIn,
415
- unhoistableHeaders,
416
- });
417
- }
418
- case 'headObject': {
419
- const cmd = new HeadObjectCommand({
420
- Bucket: options.bucket,
421
- Key: options.key,
422
- ...options.params,
423
- });
424
- return getSignedUrl(this.s3, cmd, {
425
- expiresIn: expiresIn,
426
- unhoistableHeaders,
427
- });
428
- }
429
- case 'copyObject': {
430
- const cmd = new CopyObjectCommand({
431
- Bucket: options.bucket,
432
- Key: options.key,
433
- ...options.params,
434
- });
435
- return getSignedUrl(this.s3, cmd, {
436
- expiresIn: expiresIn,
437
- unhoistableHeaders,
438
- });
439
- }
440
- case 'uploadPart': {
441
- const cmd = new UploadPartCommand({
442
- Bucket: options.bucket,
443
- Key: options.key,
444
- ...options.params,
445
- });
446
- return getSignedUrl(this.s3, cmd, {
447
- expiresIn: expiresIn,
448
- unhoistableHeaders,
449
- });
450
- }
451
- case 'uploadPartCopy': {
452
- const cmd = new UploadPartCopyCommand({
453
- Bucket: options.bucket,
454
- Key: options.key,
455
- ...options.params,
456
- });
457
- return getSignedUrl(this.s3, cmd, {
458
- expiresIn: expiresIn,
459
- unhoistableHeaders,
460
- });
461
- }
462
- case 'listObjectsV2': {
463
- const cmd = new ListObjectsV2Command({
464
- Bucket: options.bucket,
465
- ...options.params,
466
- });
467
- return getSignedUrl(this.s3, cmd, {
468
- expiresIn: expiresIn,
469
- unhoistableHeaders,
470
- });
471
- }
472
- case 'createMultipartUpload': {
473
- const tagging = options.params?.Tagging
474
- ? SimpleAWS.stringifiedStageTag + '&' + options.params.Tagging
475
- : SimpleAWS.stringifiedStageTag;
476
- const cmd = new CreateMultipartUploadCommand({
477
- Bucket: options.bucket,
478
- Key: options.key,
479
- ...options.params,
480
- Tagging: tagging,
481
- });
482
- return getSignedUrl(this.s3, cmd, {
483
- expiresIn: expiresIn,
484
- unhoistableHeaders,
485
- });
486
- }
487
- case 'completeMultipartUpload': {
488
- const cmd = new CompleteMultipartUploadCommand({
489
- Bucket: options.bucket,
490
- Key: options.key,
491
- ...options.params,
492
- });
493
- return getSignedUrl(this.s3, cmd, {
494
- expiresIn: expiresIn,
495
- unhoistableHeaders,
496
- });
497
- }
498
- case 'abortMultipartUpload': {
499
- const cmd = new AbortMultipartUploadCommand({
500
- Bucket: options.bucket,
501
- Key: options.key,
502
- ...options.params,
503
- });
504
- return getSignedUrl(this.s3, cmd, {
505
- expiresIn: expiresIn,
506
- unhoistableHeaders,
507
- });
508
- }
509
- case 'listParts': {
510
- const cmd = new ListPartsCommand({
511
- Bucket: options.bucket,
512
- Key: options.key,
513
- ...options.params,
514
- });
515
- return getSignedUrl(this.s3, cmd, {
516
- expiresIn: expiresIn,
517
- unhoistableHeaders,
518
- });
519
- }
520
- }
521
- }
522
-
523
- public getSignedCookie = (
524
- keyPairId: string,
525
- privateKey: string,
526
- url: string,
527
- expires: number,
528
- ): CloudfrontSignedCookiesOutput => {
529
- const policy = JSON.stringify({
530
- Statement: [
531
- {
532
- Resource: url,
533
- Condition: {
534
- DateLessThan: { 'AWS:EpochTime': expires },
535
- },
536
- },
537
- ],
538
- });
539
-
540
- return getSignedCookies({
541
- keyPairId,
542
- privateKey,
543
- policy,
544
- });
545
- };
546
-
547
- public getDynamoDbItem = async <T>(
548
- tableName: string,
549
- key: { [keyColumn: string]: string },
550
- defaultValue?: T,
551
- ): Promise<T | undefined> => {
552
- logger.debug(
553
- `Read an item with key[${JSON.stringify(key)}] from ${tableName}.`,
554
- );
555
- const getResult = await this.dynamodb.get({
556
- TableName: tableName,
557
- Key: key,
558
- });
559
- logger.stupid(`getResult`, getResult);
560
- const item: T | undefined =
561
- getResult !== undefined && getResult.Item !== undefined
562
- ? (getResult.Item as any as T) // Casts forcefully.
563
- : defaultValue;
564
- logger.stupid(`item`, item);
565
- return item;
566
- };
567
-
568
- public updateDynamoDbItem = async (
569
- tableName: string,
570
- key: { [keyColumn: string]: string },
571
- columnValues: { [column: string]: any },
572
- ) => {
573
- logger.debug(
574
- `Update an item with key[${JSON.stringify(key)}] to ${tableName}`,
575
- );
576
- logger.stupid(`keyValues`, columnValues);
577
- const expressions = Object.keys(columnValues)
578
- .map((column) => `${column} = :${column}`)
579
- .join(', ');
580
- const attributeValues = Object.keys(columnValues)
581
- .map((column) => [`:${column}`, columnValues[column]])
582
- .reduce((obj, pair) => ({ ...obj, [pair[0]]: pair[1] }), {});
583
- logger.stupid(`expressions`, expressions);
584
- logger.stupid(`attributeValues`, attributeValues);
585
- const updateResult = await this.dynamodb.update({
586
- TableName: tableName,
587
- Key: key,
588
- UpdateExpression: `set ${expressions}`,
589
- ExpressionAttributeValues: attributeValues,
590
- });
591
- logger.stupid(`updateResult`, updateResult);
592
- return updateResult;
593
- };
594
-
595
- // Setup
596
-
597
- public setupQueue = async (queueName: string) => {
598
- try {
599
- const listResult = await this.sqs.listQueues({
600
- QueueNamePrefix: queueName,
601
- });
602
- if (listResult.QueueUrls) {
603
- for (const queueUrl of listResult.QueueUrls) {
604
- if (queueUrl.endsWith(queueName)) {
605
- logger.debug(`Queue[${queueName} => ${queueUrl}] already exists.`);
606
- return true;
607
- }
608
- }
609
- }
610
- } catch (error) {
611
- logger.debug(`No Queue[${queueName}] exists due to ${error}`);
612
- }
613
- logger.debug(`Create a queue[${queueName}] newly.`);
614
- const createResult = await this.sqs.createQueue({
615
- QueueName: queueName,
616
- });
617
- logger.stupid(`createResult`, createResult);
618
- return true;
619
- };
620
-
621
- public setupStorage = async (
622
- bucketName: string,
623
- cors: {
624
- methods: Array<'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD'>;
625
- origins: string[];
626
- },
627
- ) => {
628
- try {
629
- const listResult = await this.s3.listBuckets();
630
- if (
631
- listResult.Buckets &&
632
- listResult.Buckets.map((each) => each.Name).includes(bucketName)
633
- ) {
634
- logger.debug(`Bucket[${bucketName}] already exists.`);
635
- return true;
636
- }
637
- } catch (error) {
638
- logger.debug(`No bucket[${bucketName}] exists due to ${error}`);
639
- }
640
- logger.debug(`Create a bucket[${bucketName}] newly.`);
641
- const createResult = await this.s3.createBucket({
642
- Bucket: bucketName,
643
- });
644
- logger.stupid(`createResult`, createResult);
645
- if (cors) {
646
- const corsResult = await this.s3.putBucketCors({
647
- Bucket: bucketName,
648
- CORSConfiguration: {
649
- CORSRules: [
650
- {
651
- AllowedHeaders: ['*'],
652
- AllowedMethods: cors.methods,
653
- AllowedOrigins: cors.origins,
654
- },
655
- ],
656
- },
657
- });
658
- logger.stupid(`corsResult`, corsResult);
659
- }
660
- return true;
661
- };
662
-
663
- public setupDynamoDb = async (tableName: string, keyColumn: string) => {
664
- try {
665
- const listResult = await this.dynamodbAdmin.listTables();
666
- if (listResult.TableNames && listResult.TableNames.includes(tableName)) {
667
- logger.debug(`Table[${tableName}] already exists.`);
668
- return true;
669
- }
670
- } catch (error) {
671
- logger.debug(`No table[${tableName}] exists due to ${error}`);
672
- }
673
- logger.debug(`Create a table[${tableName}] newly.`);
674
- const createResult = await this.dynamodbAdmin.createTable({
675
- TableName: tableName,
676
- KeySchema: [{ AttributeName: keyColumn, KeyType: 'HASH' }],
677
- AttributeDefinitions: [{ AttributeName: keyColumn, AttributeType: 'S' }],
678
- ProvisionedThroughput: {
679
- ReadCapacityUnits: 30,
680
- WriteCapacityUnits: 10,
681
- },
682
- });
683
- logger.stupid(`createResult`, createResult);
684
- return true;
685
- };
686
- }
1
+ import {
2
+ CloudfrontSignedCookiesOutput,
3
+ getSignedCookies,
4
+ } from '@aws-sdk/cloudfront-signer';
5
+
6
+ import { envDefault as currentStage } from 'simple-staging';
7
+
8
+ import * as fs from 'fs';
9
+ import * as os from 'os';
10
+ import { nanoid } from 'nanoid/non-secure';
11
+
12
+ import { getLogger, stringifyError } from '../utils';
13
+ import { SimpleAWSConfig } from './config';
14
+
15
+ import { AWSComponent, SQSMessageBody } from './define';
16
+ import { DynamoDB, DynamoDBClient } from '@aws-sdk/client-dynamodb';
17
+ import {
18
+ AbortMultipartUploadCommand,
19
+ CompleteMultipartUploadCommand,
20
+ CopyObjectCommand,
21
+ CreateMultipartUploadCommand,
22
+ DeleteObjectCommand,
23
+ GetObjectCommand,
24
+ HeadObjectCommand,
25
+ ListObjectsV2Command,
26
+ ListPartsCommand,
27
+ PutObjectCommand,
28
+ S3,
29
+ Tag,
30
+ UploadPartCommand,
31
+ UploadPartCopyCommand,
32
+ } from '@aws-sdk/client-s3';
33
+ import { SQS } from '@aws-sdk/client-sqs';
34
+ import { DynamoDBDocument } from '@aws-sdk/lib-dynamodb';
35
+ import { getSignedUrl } from '@aws-sdk/s3-request-presigner';
36
+ import { PresignerOptions } from '../internal/s3';
37
+ import { Upload } from '@aws-sdk/lib-storage';
38
+
39
+ const logger = getLogger(__filename);
40
+
41
+ export class SimpleAWS {
42
+ private queueUrls: { [queueName: string]: string } = {};
43
+ private config: SimpleAWSConfig;
44
+ private lazyS3: S3 | undefined;
45
+ private lazySqs: SQS | undefined;
46
+ private lazyDynamodb: DynamoDBDocument | undefined;
47
+ private lazyDynamodbAdmin: DynamoDB | undefined;
48
+ private static readonly stageTag: Tag = {
49
+ Key: 'STAGE',
50
+ Value: currentStage.level,
51
+ };
52
+ private static readonly stringifiedStageTag: string = `STAGE=${currentStage.level}`;
53
+
54
+ constructor(config?: SimpleAWSConfig) {
55
+ this.config = config || new SimpleAWSConfig();
56
+ /**
57
+ * The simple cache for { queueName: queueUrl }.
58
+ * It can help in the only case of launching this project as offline.
59
+ * @type { { [queueName: string]: string } }
60
+ */
61
+ this.queueUrls = {};
62
+ }
63
+
64
+ get s3() {
65
+ if (this.lazyS3 === undefined) {
66
+ this.lazyS3 = new S3(this.config.get(AWSComponent.s3) || {});
67
+ }
68
+ return this.lazyS3;
69
+ }
70
+
71
+ get sqs() {
72
+ if (this.lazySqs === undefined) {
73
+ this.lazySqs = new SQS(this.config.get(AWSComponent.sqs) || {});
74
+ }
75
+ return this.lazySqs;
76
+ }
77
+
78
+ get dynamodb() {
79
+ if (this.lazyDynamodb === undefined) {
80
+ this.lazyDynamodb = DynamoDBDocument.from(
81
+ new DynamoDBClient(this.config.get(AWSComponent.dynamodb) || {}),
82
+ {
83
+ marshallOptions: {
84
+ convertEmptyValues: true,
85
+ removeUndefinedValues: true,
86
+ },
87
+ },
88
+ );
89
+ }
90
+ return this.lazyDynamodb;
91
+ }
92
+
93
+ get dynamodbAdmin() {
94
+ if (this.lazyDynamodbAdmin === undefined) {
95
+ this.lazyDynamodbAdmin = new DynamoDB(
96
+ this.config.get(AWSComponent.dynamodb) || {},
97
+ );
98
+ }
99
+ return this.lazyDynamodbAdmin;
100
+ }
101
+
102
+ public getQueueUrl = async (queueName: string): Promise<string> => {
103
+ if (this.queueUrls[queueName] !== undefined) {
104
+ return this.queueUrls[queueName];
105
+ }
106
+ const urlResult = await this.sqs.getQueueUrl({
107
+ QueueName: queueName,
108
+ });
109
+ logger.stupid(`urlResult`, urlResult);
110
+ if (!urlResult.QueueUrl) {
111
+ throw new Error(`No queue url with name[${queueName}]`);
112
+ }
113
+ return (this.queueUrls[queueName] = urlResult.QueueUrl);
114
+ };
115
+
116
+ public enqueue = async (queueName: string, data: any): Promise<number> => {
117
+ logger.debug(`Send message[${data.key}] to queue.`);
118
+ logger.stupid(`data`, data);
119
+ const queueUrl = await this.getQueueUrl(queueName);
120
+ const sendResult = await this.sqs.sendMessage({
121
+ QueueUrl: queueUrl,
122
+ MessageBody: JSON.stringify(data),
123
+ DelaySeconds: 0,
124
+ });
125
+ logger.stupid(`sendResult`, sendResult);
126
+
127
+ const attrResult = await this.sqs.getQueueAttributes({
128
+ QueueUrl: queueUrl,
129
+ AttributeNames: ['ApproximateNumberOfMessages'],
130
+ });
131
+ logger.stupid(`attrResult`, attrResult);
132
+ if (!attrResult.Attributes) {
133
+ return 0;
134
+ }
135
+ return +(attrResult.Attributes?.ApproximateNumberOfMessages || 0);
136
+ };
137
+
138
+ public dequeue = async <T>(
139
+ queueName: string,
140
+ fetchSize: number = 1,
141
+ waitSeconds: number = 1,
142
+ visibilityTimeout: number = 15,
143
+ ): Promise<Array<SQSMessageBody<T>>> => {
144
+ logger.debug(`Receive message from queue[${queueName}].`);
145
+ const queueUrl = await this.getQueueUrl(queueName);
146
+ const receiveResult = await this.sqs.receiveMessage({
147
+ QueueUrl: queueUrl,
148
+ MaxNumberOfMessages: fetchSize,
149
+ WaitTimeSeconds: waitSeconds,
150
+ VisibilityTimeout: visibilityTimeout,
151
+ });
152
+ logger.stupid(`receiveResult`, receiveResult);
153
+ if (
154
+ receiveResult.Messages === undefined ||
155
+ receiveResult.Messages.length === 0
156
+ ) {
157
+ return [];
158
+ }
159
+ const data = [];
160
+ for (const each of receiveResult.Messages) {
161
+ if (!each.ReceiptHandle) {
162
+ logger.warn(`No receipt handler: ${JSON.stringify(each)}`);
163
+ continue;
164
+ }
165
+ const message: SQSMessageBody<T> = {
166
+ handle: each.ReceiptHandle,
167
+ body: each.Body ? (JSON.parse(each.Body) as T) : undefined,
168
+ };
169
+ data.push(message);
170
+ }
171
+ logger.verbose(`Receive a message[${JSON.stringify(data)}] from queue`);
172
+ return data;
173
+ };
174
+
175
+ public dequeueAll = async <T>(
176
+ queueName: string,
177
+ limitSize: number = Number.MAX_VALUE,
178
+ visibilityTimeout: number = 15,
179
+ ): Promise<Array<SQSMessageBody<T>>> => {
180
+ const messages = [];
181
+ const maxFetchSize = 10; // This is max-value for fetching in each time.
182
+ while (messages.length < limitSize) {
183
+ const eachOfMessages: Array<SQSMessageBody<T>> = await this.dequeue<T>(
184
+ queueName,
185
+ Math.min(limitSize - messages.length, maxFetchSize),
186
+ 0,
187
+ visibilityTimeout,
188
+ );
189
+ if (!eachOfMessages || eachOfMessages.length === 0) {
190
+ break;
191
+ }
192
+ for (const each of eachOfMessages) {
193
+ messages.push(each);
194
+ }
195
+ }
196
+ logger.stupid(`messages`, messages);
197
+ return messages;
198
+ };
199
+
200
+ public retainMessage = async (
201
+ queueName: string,
202
+ handle: string,
203
+ seconds: number,
204
+ ): Promise<string> => {
205
+ logger.debug(`Change visibilityTimeout of ${handle} to ${seconds}secs.`);
206
+ const queueUrl = await this.getQueueUrl(queueName);
207
+
208
+ await this.sqs.changeMessageVisibility({
209
+ QueueUrl: queueUrl,
210
+ ReceiptHandle: handle,
211
+ VisibilityTimeout: seconds,
212
+ });
213
+
214
+ return handle;
215
+ };
216
+
217
+ public completeMessage = async (
218
+ queueName: string,
219
+ handle: string,
220
+ ): Promise<string> => {
221
+ logger.debug(`Complete a message with handle[${handle}]`);
222
+ const queueUrl = await this.getQueueUrl(queueName);
223
+ const deleteResult = await this.sqs.deleteMessage({
224
+ QueueUrl: queueUrl,
225
+ ReceiptHandle: handle,
226
+ });
227
+ logger.stupid(`deleteResult`, deleteResult);
228
+ return handle;
229
+ };
230
+
231
+ public completeMessages = async (queueName: string, handles: string[]) => {
232
+ logger.debug(`Complete a message with handle[${handles}]`);
233
+ if (!handles) {
234
+ return handles;
235
+ }
236
+
237
+ const chunkSize = 10;
238
+ let index = 0;
239
+ for (let start = 0; start < handles.length; start += chunkSize) {
240
+ const end = Math.min(start + chunkSize, handles.length);
241
+ const sublist = handles.slice(start, end);
242
+ const queueUrl = await this.getQueueUrl(queueName);
243
+ const deletesResult = await this.sqs.deleteMessageBatch({
244
+ QueueUrl: queueUrl,
245
+ Entries: sublist.map((handle) => ({
246
+ Id: (++index).toString(),
247
+ ReceiptHandle: handle,
248
+ })),
249
+ });
250
+ logger.stupid(`deleteResult`, deletesResult);
251
+ }
252
+ return handles;
253
+ };
254
+
255
+ public download = async (
256
+ bucket: string,
257
+ key: string,
258
+ localPath: string,
259
+ ): Promise<string> => {
260
+ logger.debug(`Get a stream of item[${key}] from bucket[${bucket}]`);
261
+ const { Body } = await this.s3.getObject({ Bucket: bucket, Key: key });
262
+
263
+ return new Promise<string>((resolve, reject) =>
264
+ (Body as NodeJS.ReadableStream)
265
+ .on('error', (error) => reject(error))
266
+ .pipe(fs.createWriteStream(localPath))
267
+ .on('finish', () => resolve(localPath))
268
+ .on('error', (error) => reject(error)),
269
+ );
270
+ };
271
+
272
+ public readFile = async (bucket: string, key: string): Promise<string> => {
273
+ logger.debug(`Read item[${key}] from bucket[${bucket}]`);
274
+ const tempFile = `${os.tmpdir()}/${nanoid()}`;
275
+ try {
276
+ await this.download(bucket, key, tempFile);
277
+ const content = await fs.promises.readFile(tempFile, {
278
+ encoding: 'utf-8',
279
+ });
280
+ return content;
281
+ } finally {
282
+ if (fs.existsSync(tempFile)) {
283
+ try {
284
+ await fs.promises.unlink(tempFile);
285
+ } catch (error) {
286
+ logger.error(
287
+ `Failed to delete temp file ${tempFile}: ${stringifyError(error)}`,
288
+ );
289
+ }
290
+ }
291
+ }
292
+ };
293
+
294
+ public readFileBuffer = async (
295
+ bucket: string,
296
+ key: string,
297
+ ): Promise<Buffer> => {
298
+ logger.debug(`Read item[${key}] from bucket[${bucket}]`);
299
+ const { Body } = await this.s3.getObject({ Bucket: bucket, Key: key });
300
+
301
+ const buffer = await Body?.transformToByteArray();
302
+ if (!buffer) {
303
+ throw new Error(`Failed to read file ${key} from bucket ${bucket}`);
304
+ }
305
+ return Buffer.from(buffer);
306
+ };
307
+
308
+ public upload = async (
309
+ bucket: string,
310
+ localPath: string,
311
+ key: string,
312
+ tags?: Tag[],
313
+ ): Promise<string> => {
314
+ logger.debug(`Upload item[${key}] into bucket[${bucket}]`);
315
+ const upload = new Upload({
316
+ client: this.s3,
317
+ params: {
318
+ Bucket: bucket,
319
+ Key: key,
320
+ Body: fs.createReadStream(localPath),
321
+ },
322
+ partSize: 5 * 1024 * 1024, // 5MB
323
+ queueSize: 4,
324
+ tags: [SimpleAWS.stageTag, ...(tags || [])],
325
+ });
326
+
327
+ await upload.done();
328
+ return key;
329
+ };
330
+
331
+ public uploadFromBuffer = async (
332
+ bucket: string,
333
+ key: string,
334
+ buffer: Buffer,
335
+ tags?: Tag[],
336
+ ): Promise<string> => {
337
+ logger.debug(`Upload item[${key}] into bucket[${bucket}]`);
338
+ const upload = new Upload({
339
+ client: this.s3,
340
+ params: {
341
+ Bucket: bucket,
342
+ Key: key,
343
+ Body: buffer,
344
+ },
345
+ partSize: 5 * 1024 * 1024, // 5MB
346
+ queueSize: 4,
347
+ tags: [SimpleAWS.stageTag, ...(tags || [])],
348
+ });
349
+ await upload.done();
350
+ return key;
351
+ };
352
+
353
+ public writeFile = async (
354
+ bucket: string,
355
+ key: string,
356
+ content: string,
357
+ ): Promise<void> => {
358
+ logger.debug(`Write item[${key}] into bucket[${bucket}]`);
359
+ const tempFile = `${os.tmpdir()}/${nanoid()}`;
360
+ try {
361
+ await fs.promises.writeFile(tempFile, content, 'utf-8');
362
+ await this.upload(bucket, tempFile, key);
363
+ } finally {
364
+ if (!fs.existsSync(tempFile)) {
365
+ return;
366
+ }
367
+ try {
368
+ await fs.promises.unlink(tempFile);
369
+ } catch (error) {
370
+ const msg = `Error during writeFile: unlink file ${tempFile}: ${stringifyError(
371
+ error,
372
+ )}`;
373
+ logger.error(msg);
374
+ }
375
+ }
376
+ };
377
+
378
+ public async getSignedUrl(options: PresignerOptions): Promise<string> {
379
+ const { expiresIn = 600, unhoistableHeaders } = options;
380
+ switch (options.operation) {
381
+ case 'putObject': {
382
+ const tagging = options.params?.Tagging
383
+ ? SimpleAWS.stringifiedStageTag + '&' + options.params.Tagging
384
+ : SimpleAWS.stringifiedStageTag;
385
+ const cmd = new PutObjectCommand({
386
+ Bucket: options.bucket,
387
+ Key: options.key,
388
+ ...options.params,
389
+ Tagging: tagging,
390
+ });
391
+ return getSignedUrl(this.s3, cmd, {
392
+ expiresIn: expiresIn,
393
+ unhoistableHeaders,
394
+ });
395
+ }
396
+ case 'getObject': {
397
+ const cmd = new GetObjectCommand({
398
+ Bucket: options.bucket,
399
+ Key: options.key,
400
+ ...options.params,
401
+ });
402
+ return getSignedUrl(this.s3, cmd, {
403
+ expiresIn: expiresIn,
404
+ unhoistableHeaders,
405
+ });
406
+ }
407
+ case 'deleteObject': {
408
+ const cmd = new DeleteObjectCommand({
409
+ Bucket: options.bucket,
410
+ Key: options.key,
411
+ ...options.params,
412
+ });
413
+ return getSignedUrl(this.s3, cmd, {
414
+ expiresIn: expiresIn,
415
+ unhoistableHeaders,
416
+ });
417
+ }
418
+ case 'headObject': {
419
+ const cmd = new HeadObjectCommand({
420
+ Bucket: options.bucket,
421
+ Key: options.key,
422
+ ...options.params,
423
+ });
424
+ return getSignedUrl(this.s3, cmd, {
425
+ expiresIn: expiresIn,
426
+ unhoistableHeaders,
427
+ });
428
+ }
429
+ case 'copyObject': {
430
+ const cmd = new CopyObjectCommand({
431
+ Bucket: options.bucket,
432
+ Key: options.key,
433
+ ...options.params,
434
+ });
435
+ return getSignedUrl(this.s3, cmd, {
436
+ expiresIn: expiresIn,
437
+ unhoistableHeaders,
438
+ });
439
+ }
440
+ case 'uploadPart': {
441
+ const cmd = new UploadPartCommand({
442
+ Bucket: options.bucket,
443
+ Key: options.key,
444
+ ...options.params,
445
+ });
446
+ return getSignedUrl(this.s3, cmd, {
447
+ expiresIn: expiresIn,
448
+ unhoistableHeaders,
449
+ });
450
+ }
451
+ case 'uploadPartCopy': {
452
+ const cmd = new UploadPartCopyCommand({
453
+ Bucket: options.bucket,
454
+ Key: options.key,
455
+ ...options.params,
456
+ });
457
+ return getSignedUrl(this.s3, cmd, {
458
+ expiresIn: expiresIn,
459
+ unhoistableHeaders,
460
+ });
461
+ }
462
+ case 'listObjectsV2': {
463
+ const cmd = new ListObjectsV2Command({
464
+ Bucket: options.bucket,
465
+ ...options.params,
466
+ });
467
+ return getSignedUrl(this.s3, cmd, {
468
+ expiresIn: expiresIn,
469
+ unhoistableHeaders,
470
+ });
471
+ }
472
+ case 'createMultipartUpload': {
473
+ const tagging = options.params?.Tagging
474
+ ? SimpleAWS.stringifiedStageTag + '&' + options.params.Tagging
475
+ : SimpleAWS.stringifiedStageTag;
476
+ const cmd = new CreateMultipartUploadCommand({
477
+ Bucket: options.bucket,
478
+ Key: options.key,
479
+ ...options.params,
480
+ Tagging: tagging,
481
+ });
482
+ return getSignedUrl(this.s3, cmd, {
483
+ expiresIn: expiresIn,
484
+ unhoistableHeaders,
485
+ });
486
+ }
487
+ case 'completeMultipartUpload': {
488
+ const cmd = new CompleteMultipartUploadCommand({
489
+ Bucket: options.bucket,
490
+ Key: options.key,
491
+ ...options.params,
492
+ });
493
+ return getSignedUrl(this.s3, cmd, {
494
+ expiresIn: expiresIn,
495
+ unhoistableHeaders,
496
+ });
497
+ }
498
+ case 'abortMultipartUpload': {
499
+ const cmd = new AbortMultipartUploadCommand({
500
+ Bucket: options.bucket,
501
+ Key: options.key,
502
+ ...options.params,
503
+ });
504
+ return getSignedUrl(this.s3, cmd, {
505
+ expiresIn: expiresIn,
506
+ unhoistableHeaders,
507
+ });
508
+ }
509
+ case 'listParts': {
510
+ const cmd = new ListPartsCommand({
511
+ Bucket: options.bucket,
512
+ Key: options.key,
513
+ ...options.params,
514
+ });
515
+ return getSignedUrl(this.s3, cmd, {
516
+ expiresIn: expiresIn,
517
+ unhoistableHeaders,
518
+ });
519
+ }
520
+ }
521
+ }
522
+
523
+ public getSignedCookie = (
524
+ keyPairId: string,
525
+ privateKey: string,
526
+ url: string,
527
+ expires: number,
528
+ ): CloudfrontSignedCookiesOutput => {
529
+ const policy = JSON.stringify({
530
+ Statement: [
531
+ {
532
+ Resource: url,
533
+ Condition: {
534
+ DateLessThan: { 'AWS:EpochTime': expires },
535
+ },
536
+ },
537
+ ],
538
+ });
539
+
540
+ return getSignedCookies({
541
+ keyPairId,
542
+ privateKey,
543
+ policy,
544
+ });
545
+ };
546
+
547
+ public getDynamoDbItem = async <T>(
548
+ tableName: string,
549
+ key: { [keyColumn: string]: string },
550
+ defaultValue?: T,
551
+ ): Promise<T | undefined> => {
552
+ logger.debug(
553
+ `Read an item with key[${JSON.stringify(key)}] from ${tableName}.`,
554
+ );
555
+ const getResult = await this.dynamodb.get({
556
+ TableName: tableName,
557
+ Key: key,
558
+ });
559
+ logger.stupid(`getResult`, getResult);
560
+ const item: T | undefined =
561
+ getResult !== undefined && getResult.Item !== undefined
562
+ ? (getResult.Item as any as T) // Casts forcefully.
563
+ : defaultValue;
564
+ logger.stupid(`item`, item);
565
+ return item;
566
+ };
567
+
568
+ public updateDynamoDbItem = async (
569
+ tableName: string,
570
+ key: { [keyColumn: string]: string },
571
+ columnValues: { [column: string]: any },
572
+ ) => {
573
+ logger.debug(
574
+ `Update an item with key[${JSON.stringify(key)}] to ${tableName}`,
575
+ );
576
+ logger.stupid(`keyValues`, columnValues);
577
+ const expressions = Object.keys(columnValues)
578
+ .map((column) => `${column} = :${column}`)
579
+ .join(', ');
580
+ const attributeValues = Object.keys(columnValues)
581
+ .map((column) => [`:${column}`, columnValues[column]])
582
+ .reduce((obj, pair) => ({ ...obj, [pair[0]]: pair[1] }), {});
583
+ logger.stupid(`expressions`, expressions);
584
+ logger.stupid(`attributeValues`, attributeValues);
585
+ const updateResult = await this.dynamodb.update({
586
+ TableName: tableName,
587
+ Key: key,
588
+ UpdateExpression: `set ${expressions}`,
589
+ ExpressionAttributeValues: attributeValues,
590
+ });
591
+ logger.stupid(`updateResult`, updateResult);
592
+ return updateResult;
593
+ };
594
+
595
+ // Setup
596
+
597
+ public setupQueue = async (queueName: string) => {
598
+ try {
599
+ const listResult = await this.sqs.listQueues({
600
+ QueueNamePrefix: queueName,
601
+ });
602
+ if (listResult.QueueUrls) {
603
+ for (const queueUrl of listResult.QueueUrls) {
604
+ if (queueUrl.endsWith(queueName)) {
605
+ logger.debug(`Queue[${queueName} => ${queueUrl}] already exists.`);
606
+ return true;
607
+ }
608
+ }
609
+ }
610
+ } catch (error) {
611
+ logger.debug(`No Queue[${queueName}] exists due to ${error}`);
612
+ }
613
+ logger.debug(`Create a queue[${queueName}] newly.`);
614
+ const createResult = await this.sqs.createQueue({
615
+ QueueName: queueName,
616
+ });
617
+ logger.stupid(`createResult`, createResult);
618
+ return true;
619
+ };
620
+
621
+ public setupStorage = async (
622
+ bucketName: string,
623
+ cors: {
624
+ methods: Array<'GET' | 'POST' | 'PUT' | 'DELETE' | 'HEAD'>;
625
+ origins: string[];
626
+ },
627
+ ) => {
628
+ try {
629
+ const listResult = await this.s3.listBuckets();
630
+ if (
631
+ listResult.Buckets &&
632
+ listResult.Buckets.map((each) => each.Name).includes(bucketName)
633
+ ) {
634
+ logger.debug(`Bucket[${bucketName}] already exists.`);
635
+ return true;
636
+ }
637
+ } catch (error) {
638
+ logger.debug(`No bucket[${bucketName}] exists due to ${error}`);
639
+ }
640
+ logger.debug(`Create a bucket[${bucketName}] newly.`);
641
+ const createResult = await this.s3.createBucket({
642
+ Bucket: bucketName,
643
+ });
644
+ logger.stupid(`createResult`, createResult);
645
+ if (cors) {
646
+ const corsResult = await this.s3.putBucketCors({
647
+ Bucket: bucketName,
648
+ CORSConfiguration: {
649
+ CORSRules: [
650
+ {
651
+ AllowedHeaders: ['*'],
652
+ AllowedMethods: cors.methods,
653
+ AllowedOrigins: cors.origins,
654
+ },
655
+ ],
656
+ },
657
+ });
658
+ logger.stupid(`corsResult`, corsResult);
659
+ }
660
+ return true;
661
+ };
662
+
663
+ public setupDynamoDb = async (tableName: string, keyColumn: string) => {
664
+ try {
665
+ const listResult = await this.dynamodbAdmin.listTables();
666
+ if (listResult.TableNames && listResult.TableNames.includes(tableName)) {
667
+ logger.debug(`Table[${tableName}] already exists.`);
668
+ return true;
669
+ }
670
+ } catch (error) {
671
+ logger.debug(`No table[${tableName}] exists due to ${error}`);
672
+ }
673
+ logger.debug(`Create a table[${tableName}] newly.`);
674
+ const createResult = await this.dynamodbAdmin.createTable({
675
+ TableName: tableName,
676
+ KeySchema: [{ AttributeName: keyColumn, KeyType: 'HASH' }],
677
+ AttributeDefinitions: [{ AttributeName: keyColumn, AttributeType: 'S' }],
678
+ ProvisionedThroughput: {
679
+ ReadCapacityUnits: 30,
680
+ WriteCapacityUnits: 10,
681
+ },
682
+ });
683
+ logger.stupid(`createResult`, createResult);
684
+ return true;
685
+ };
686
+ }