@e-mc/cloud 0.5.3 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/index.js +65 -113
- package/package.json +4 -4
- package/types/index.d.ts +2 -0
- package/util.d.ts +6 -0
- package/util.js +13 -1
package/index.js
CHANGED
|
@@ -5,7 +5,7 @@ const fs = require("fs");
|
|
|
5
5
|
const types_1 = require("../types");
|
|
6
6
|
const core_1 = require("../core");
|
|
7
7
|
const util_1 = require("./util");
|
|
8
|
-
const SERVICE_CLIENT =
|
|
8
|
+
const SERVICE_CLIENT = new Map();
|
|
9
9
|
const SERVICE_UPLOAD = {};
|
|
10
10
|
const SERVICE_DOWNLOAD = {};
|
|
11
11
|
function setUploadFilename(upload, filename) {
|
|
@@ -37,7 +37,7 @@ function getFiles(file, data) {
|
|
|
37
37
|
}
|
|
38
38
|
return [grouped];
|
|
39
39
|
}
|
|
40
|
-
const errorObject = (err, service, value) => err instanceof Error ? err : typeof err === 'string' ? new Error(err) : (0,
|
|
40
|
+
const errorObject = (err, service, value) => err instanceof Error ? err : typeof err === 'string' ? new Error(err) : (0, types_1.errorMessage)(service, value);
|
|
41
41
|
const assignFilename = (value) => (0, types_1.generateUUID)() + (path.extname(value) || '');
|
|
42
42
|
class Cloud extends core_1.ClientDb {
|
|
43
43
|
constructor() {
|
|
@@ -284,11 +284,12 @@ class Cloud extends core_1.ClientDb {
|
|
|
284
284
|
state.localStorage.set(file, upload);
|
|
285
285
|
}
|
|
286
286
|
const callback = async (value) => {
|
|
287
|
-
if (
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
287
|
+
if (ignoreProcess || !(0, types_1.isString)(value)) {
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
for (const { instance: document } of host.Document) {
|
|
291
|
+
if (document.cloudUpload && await document.cloudUpload(state, file, value, active)) {
|
|
292
|
+
return;
|
|
292
293
|
}
|
|
293
294
|
}
|
|
294
295
|
};
|
|
@@ -299,25 +300,22 @@ class Cloud extends core_1.ClientDb {
|
|
|
299
300
|
getFiles(file, upload).forEach((group, index) => {
|
|
300
301
|
let fileGroup;
|
|
301
302
|
if (index === 0 && group.length > 1) {
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (fs.existsSync(value)) {
|
|
313
|
-
fileGroup.push([fs.readFileSync(value), path.extname(value), value]);
|
|
314
|
-
}
|
|
315
|
-
}
|
|
316
|
-
catch (err) {
|
|
317
|
-
instance.writeFail(["Unable to read file" /* ERR_MESSAGE.READ_FILE */, path.basename(value)], err, { type: 32 /* LOG_TYPE.FILE */, fatal: false });
|
|
303
|
+
if (SERVICE_CLIENT.get(service)?.CLOUD_UPLOAD_FROMDISK) {
|
|
304
|
+
fileGroup = group.slice(1).filter(value => this.isPath(value)).map(value => [value, path.extname(value), value]);
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
fileGroup = [];
|
|
308
|
+
for (let i = 1; i < group.length; ++i) {
|
|
309
|
+
const value = group[i];
|
|
310
|
+
try {
|
|
311
|
+
if (fs.existsSync(value)) {
|
|
312
|
+
fileGroup.push([fs.readFileSync(value), path.extname(value), value]);
|
|
318
313
|
}
|
|
319
314
|
}
|
|
320
|
-
|
|
315
|
+
catch (err) {
|
|
316
|
+
instance.writeFail(["Unable to read file" /* ERR_MESSAGE.READ_FILE */, path.basename(value)], err, { type: 32 /* LOG_TYPE.FILE */, fatal: false });
|
|
317
|
+
}
|
|
318
|
+
}
|
|
321
319
|
}
|
|
322
320
|
group = [group[0]];
|
|
323
321
|
}
|
|
@@ -456,7 +454,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
456
454
|
return handler.call(this, credential, bucket, publicRead, options);
|
|
457
455
|
}
|
|
458
456
|
}
|
|
459
|
-
return Promise.reject((0,
|
|
457
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Create bucket not supported" /* ERR_CLOUD.CREATE_BUCKET_SUPPORT */));
|
|
460
458
|
}
|
|
461
459
|
catch (err) {
|
|
462
460
|
this.formatMessage(64 /* LOG_TYPE.CLOUD */, service, ["Unable to create bucket" /* ERR_CLOUD.CREATE_BUCKET */, bucket], err, { ...Cloud.LOG_CLOUD_WARN });
|
|
@@ -482,7 +480,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
482
480
|
return Promise.reject(err);
|
|
483
481
|
}
|
|
484
482
|
}
|
|
485
|
-
return Promise.reject((0,
|
|
483
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Bucket policy not supported" /* ERR_CLOUD.BUCKET_POLICY_SUPPORT */));
|
|
486
484
|
}
|
|
487
485
|
catch (err) {
|
|
488
486
|
return Promise.reject(err);
|
|
@@ -503,7 +501,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
503
501
|
return Promise.reject(err);
|
|
504
502
|
}
|
|
505
503
|
}
|
|
506
|
-
return Promise.reject((0,
|
|
504
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Set bucket website not supported" /* ERR_CLOUD.BUCKET_WEBSITE_SUPPORT */));
|
|
507
505
|
}
|
|
508
506
|
catch (err) {
|
|
509
507
|
return Promise.reject(err);
|
|
@@ -524,7 +522,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
524
522
|
if (handlerV1) {
|
|
525
523
|
return handlerV1.call(this, credential, bucket, service, undefined, recursive).catch(err => errorResponse(err));
|
|
526
524
|
}
|
|
527
|
-
return Promise.reject((0,
|
|
525
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Delete objects not supported" /* ERR_CLOUD.DELETE_OBJECTS_SUPPORT */));
|
|
528
526
|
}
|
|
529
527
|
catch (err) {
|
|
530
528
|
return Promise.reject(err);
|
|
@@ -556,7 +554,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
556
554
|
resolve(value);
|
|
557
555
|
}
|
|
558
556
|
else {
|
|
559
|
-
reject((0,
|
|
557
|
+
reject((0, types_1.errorMessage)(service, "Upload failed" /* ERR_CLOUD.UPLOAD_FAIL */));
|
|
560
558
|
}
|
|
561
559
|
});
|
|
562
560
|
}
|
|
@@ -600,7 +598,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
600
598
|
resolve(value);
|
|
601
599
|
}
|
|
602
600
|
else {
|
|
603
|
-
reject((0,
|
|
601
|
+
reject((0, types_1.errorMessage)(service, "Download failed" /* ERR_CLOUD.DOWNLOAD_FAIL */));
|
|
604
602
|
}
|
|
605
603
|
});
|
|
606
604
|
}
|
|
@@ -618,27 +616,27 @@ class Cloud extends core_1.ClientDb {
|
|
|
618
616
|
ignoreErrors = false;
|
|
619
617
|
}
|
|
620
618
|
const service = item.service;
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
if (
|
|
619
|
+
let client;
|
|
620
|
+
if (this.hasCredential('database', item) && (client = this.getClient(item.service))) {
|
|
621
|
+
if (client?.executeQuery) {
|
|
624
622
|
const credential = this.getCredential(item);
|
|
625
623
|
if (item.options && this.hasCoerce(service, 'options', null, credential)) {
|
|
626
624
|
(0, types_1.coerceObject)(item.options);
|
|
627
625
|
}
|
|
628
626
|
if (ignoreErrors) {
|
|
629
|
-
return
|
|
627
|
+
return client.executeQuery.call(this, credential, item, sessionKey);
|
|
630
628
|
}
|
|
631
629
|
try {
|
|
632
|
-
return await
|
|
630
|
+
return await client.executeQuery.call(this, credential, item, sessionKey);
|
|
633
631
|
}
|
|
634
632
|
catch (err) {
|
|
635
633
|
this.formatFail(64 /* LOG_TYPE.CLOUD */, service, "Unable to execute query" /* ERR_DB.EXEC_QUERY */, err, { ...Cloud.LOG_CLOUD_FAIL });
|
|
636
634
|
return Promise.reject(err);
|
|
637
635
|
}
|
|
638
636
|
}
|
|
639
|
-
return Promise.reject((0,
|
|
637
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Execute query not supported" /* ERR_CLOUD.EXECUTE_QUERY_SUPPORT */));
|
|
640
638
|
}
|
|
641
|
-
return Promise.reject((0,
|
|
639
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Invalid credentials" /* ERR_DB.CREDENTIALS */));
|
|
642
640
|
}
|
|
643
641
|
async getDatabaseBatchRows(batch, ignoreErrors, sessionKey) {
|
|
644
642
|
if (this.aborted) {
|
|
@@ -650,32 +648,33 @@ class Cloud extends core_1.ClientDb {
|
|
|
650
648
|
}
|
|
651
649
|
const data = batch[0];
|
|
652
650
|
const service = data.service;
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
if (
|
|
651
|
+
let client;
|
|
652
|
+
if (this.hasCredential('database', data) && (client = this.getClient(service))) {
|
|
653
|
+
if (client?.executeBatchQuery) {
|
|
656
654
|
const credential = this.getCredential(data);
|
|
657
655
|
if (this.hasCoerce(service, 'options', null, credential)) {
|
|
658
656
|
batch.forEach(item => item.options && (0, types_1.coerceObject)(item.options));
|
|
659
657
|
}
|
|
660
658
|
if (ignoreErrors) {
|
|
661
|
-
return
|
|
659
|
+
return client.executeBatchQuery.call(this, credential, batch, sessionKey);
|
|
662
660
|
}
|
|
663
661
|
try {
|
|
664
|
-
return await
|
|
662
|
+
return await client.executeBatchQuery.call(this, credential, batch, sessionKey);
|
|
665
663
|
}
|
|
666
664
|
catch (err) {
|
|
667
665
|
this.formatFail(64 /* LOG_TYPE.CLOUD */, service, "Unable to execute query" /* ERR_DB.EXEC_QUERY */, err, { ...Cloud.LOG_CLOUD_FAIL });
|
|
668
666
|
return Promise.reject(err);
|
|
669
667
|
}
|
|
670
668
|
}
|
|
671
|
-
return Promise.reject((0,
|
|
669
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Execute query not supported" /* ERR_CLOUD.EXECUTE_QUERY_SUPPORT */));
|
|
672
670
|
}
|
|
673
|
-
return Promise.reject((0,
|
|
671
|
+
return Promise.reject((0, types_1.errorMessage)(service, "Invalid credentials" /* ERR_DB.CREDENTIALS */));
|
|
674
672
|
}
|
|
675
673
|
getCredential(item, unused) {
|
|
674
|
+
const service = item.service;
|
|
676
675
|
let credential = item.credential, stored;
|
|
677
676
|
if (typeof credential === 'string') {
|
|
678
|
-
const settings = this.getSettings(
|
|
677
|
+
const settings = this.getSettings(service);
|
|
679
678
|
if ((0, types_1.isPlainObject)(settings)) {
|
|
680
679
|
credential = settings[credential];
|
|
681
680
|
stored = true;
|
|
@@ -683,7 +682,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
683
682
|
}
|
|
684
683
|
if ((0, types_1.isPlainObject)(credential)) {
|
|
685
684
|
item.credential = credential;
|
|
686
|
-
if (this.settingsOf(
|
|
685
|
+
if (this.settingsOf(service, 'coerce', 'credential') === true) {
|
|
687
686
|
(0, types_1.coerceObject)(credential, stored);
|
|
688
687
|
}
|
|
689
688
|
return unused ? credential : { ...credential };
|
|
@@ -691,19 +690,7 @@ class Cloud extends core_1.ClientDb {
|
|
|
691
690
|
return {};
|
|
692
691
|
}
|
|
693
692
|
getSettings(service) {
|
|
694
|
-
|
|
695
|
-
switch (service) {
|
|
696
|
-
case 'az':
|
|
697
|
-
return settings.az || settings.azure;
|
|
698
|
-
case 'azure':
|
|
699
|
-
return settings.azure || settings.az;
|
|
700
|
-
case 'gcp':
|
|
701
|
-
return settings.gcp || settings.gcloud;
|
|
702
|
-
case 'gcloud':
|
|
703
|
-
return settings.gcloud || settings.gcp;
|
|
704
|
-
default:
|
|
705
|
-
return settings[service];
|
|
706
|
-
}
|
|
693
|
+
return this.module[service];
|
|
707
694
|
}
|
|
708
695
|
getStorage(action, data) {
|
|
709
696
|
if ((0, types_1.isArray)(data)) {
|
|
@@ -731,9 +718,8 @@ class Cloud extends core_1.ClientDb {
|
|
|
731
718
|
return result && this.hasCredential('storage', storage) ? result : false;
|
|
732
719
|
}
|
|
733
720
|
hasCredential(feature, data, credential = this.getCredential(data, true)) {
|
|
734
|
-
var _a;
|
|
735
721
|
try {
|
|
736
|
-
const client =
|
|
722
|
+
const client = this.getClient(data.service);
|
|
737
723
|
switch (feature) {
|
|
738
724
|
case 'storage':
|
|
739
725
|
return typeof client.validateStorage === 'function' && client.validateStorage(credential, data);
|
|
@@ -754,60 +740,16 @@ class Cloud extends core_1.ClientDb {
|
|
|
754
740
|
}
|
|
755
741
|
resolveService(service, folder) {
|
|
756
742
|
let result;
|
|
757
|
-
if (service[0]
|
|
758
|
-
result = service;
|
|
759
|
-
if (!folder && !service.startsWith("@squared-functions/" /* NAMESPACE.SQUARED_FUNCTIONS */)) {
|
|
760
|
-
folder = 'client';
|
|
761
|
-
}
|
|
743
|
+
if (service[0] !== '@') {
|
|
744
|
+
result = this.settings.imports?.[service] || util_1.IMPORTS[service];
|
|
762
745
|
}
|
|
763
|
-
else {
|
|
764
|
-
|
|
765
|
-
case 'az':
|
|
766
|
-
service = 'azure';
|
|
767
|
-
break;
|
|
768
|
-
case 'gcloud':
|
|
769
|
-
service = 'gcp';
|
|
770
|
-
break;
|
|
771
|
-
}
|
|
772
|
-
switch (service) {
|
|
773
|
-
case 'atlas':
|
|
774
|
-
case 'aws':
|
|
775
|
-
case 'aws-v3':
|
|
776
|
-
case 'azure':
|
|
777
|
-
case 'gcp':
|
|
778
|
-
case 'ibm':
|
|
779
|
-
case 'minio':
|
|
780
|
-
case 'oci':
|
|
781
|
-
result = "@pi-r/" /* NAMESPACE.PIR */ + service;
|
|
782
|
-
break;
|
|
783
|
-
default:
|
|
784
|
-
result = service;
|
|
785
|
-
break;
|
|
786
|
-
}
|
|
746
|
+
else if (!folder && !service.startsWith("@squared-functions/" /* NAMESPACE.SQUARED_FUNCTIONS */)) {
|
|
747
|
+
folder = 'client';
|
|
787
748
|
}
|
|
788
|
-
return result + (folder ? '/' + folder : '');
|
|
749
|
+
return (result || service) + (folder ? '/' + folder : '');
|
|
789
750
|
}
|
|
790
751
|
settingsOf(service, name, component) {
|
|
791
|
-
const
|
|
792
|
-
let options;
|
|
793
|
-
switch (service) {
|
|
794
|
-
case 'az':
|
|
795
|
-
options = settings.az ?? settings.azure;
|
|
796
|
-
break;
|
|
797
|
-
case 'azure':
|
|
798
|
-
options = settings.azure ?? settings.az;
|
|
799
|
-
break;
|
|
800
|
-
case 'gcp':
|
|
801
|
-
options = settings.gcp ?? settings.gcloud;
|
|
802
|
-
break;
|
|
803
|
-
case 'gcloud':
|
|
804
|
-
options = settings.gcloud ?? settings.gcp;
|
|
805
|
-
break;
|
|
806
|
-
default:
|
|
807
|
-
options = settings[service];
|
|
808
|
-
break;
|
|
809
|
-
}
|
|
810
|
-
const result = options?.[name];
|
|
752
|
+
const result = this.settings[service]?.[name];
|
|
811
753
|
return component ? (0, types_1.isObject)(result) ? result[component] : undefined : result;
|
|
812
754
|
}
|
|
813
755
|
commit() {
|
|
@@ -821,14 +763,24 @@ class Cloud extends core_1.ClientDb {
|
|
|
821
763
|
return items.length === 0 ? Promise.resolve(false) : this.allSettled(items, ["Execute unassigned queries" /* VAL_DB.EXEC_QUERYUNASSIGNED */, this.moduleName]).then(result => result.length > 0).catch(() => false);
|
|
822
764
|
}
|
|
823
765
|
getClient(service) {
|
|
766
|
+
let client = SERVICE_CLIENT.get(service);
|
|
767
|
+
if (client) {
|
|
768
|
+
return client;
|
|
769
|
+
}
|
|
824
770
|
try {
|
|
825
|
-
|
|
771
|
+
if (client = require(this.resolveService(service))) {
|
|
772
|
+
client.CLOUD_SERVICE_NAME = service;
|
|
773
|
+
SERVICE_CLIENT.set(service, client);
|
|
774
|
+
return client;
|
|
775
|
+
}
|
|
826
776
|
}
|
|
827
777
|
catch {
|
|
828
|
-
throw (0, util_1.formatError)(service, "Cloud provider not found" /* ERR_CLOUD.PROVIDER_NOTFOUND */);
|
|
829
778
|
}
|
|
779
|
+
throw (0, types_1.errorMessage)(service, "Cloud provider not found" /* ERR_CLOUD.PROVIDER_NOTFOUND */);
|
|
830
780
|
}
|
|
831
781
|
}
|
|
782
|
+
Cloud.STORE_RESULT_PARTITION_SIZE = 16 /* CACHE_SIZE.CLOUD_PARTITION_SIZE */;
|
|
783
|
+
Cloud.STORE_RESULT_PARTITION_MULT = 2 /* CACHE_SIZE.CLOUD_PARTITION_MULT */;
|
|
832
784
|
Cloud.LOG_CLOUD_FAIL = core_1.ClientDb.LOG_STYLE_FAIL;
|
|
833
785
|
Cloud.LOG_CLOUD_COMMAND = Object.freeze({ titleColor: 'blue' });
|
|
834
786
|
Cloud.LOG_CLOUD_WARN = Object.freeze({ titleColor: 'yellow' });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e-mc/cloud",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.7.0",
|
|
4
4
|
"description": "Cloud constructor for E-mc.",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"license": "BSD 3-Clause",
|
|
21
21
|
"homepage": "https://github.com/anpham6/e-mc#readme",
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"@e-mc/core": "0.
|
|
24
|
-
"@e-mc/db": "0.
|
|
25
|
-
"@e-mc/types": "0.
|
|
23
|
+
"@e-mc/core": "0.7.0",
|
|
24
|
+
"@e-mc/db": "0.7.0",
|
|
25
|
+
"@e-mc/types": "0.7.0"
|
|
26
26
|
}
|
|
27
27
|
}
|
package/types/index.d.ts
CHANGED
|
@@ -3,6 +3,8 @@ import type { BucketWebsiteOptions, CloudAsset, CloudDatabase, CloudService, Clo
|
|
|
3
3
|
import type { BatchQueryResult, QueryResult } from '../../types/lib/db';
|
|
4
4
|
|
|
5
5
|
export interface ICloudServiceClient<T extends CloudDatabase = CloudDatabase> {
|
|
6
|
+
CLOUD_SERVICE_NAME: string;
|
|
7
|
+
CLOUD_UPLOAD_FROMDISK?: boolean;
|
|
6
8
|
validateStorage?(credential: unknown, data?: CloudService): boolean;
|
|
7
9
|
validateDatabase?(credential: unknown, data?: CloudService): boolean;
|
|
8
10
|
createStorageClient?<U>(this: IModule, credential: unknown, service?: string): U;
|
package/util.d.ts
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import type { Readable } from 'stream';
|
|
2
2
|
|
|
3
|
+
interface AuthValue {
|
|
4
|
+
username?: string;
|
|
5
|
+
password?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
3
8
|
declare namespace util {
|
|
9
|
+
const IMPORTS: Record<string, string | undefined>;
|
|
4
10
|
function readableAsBuffer(stream: Readable): Promise<Buffer | null>;
|
|
5
11
|
function generateFilename(filename: string): (i: number) => [string, boolean];
|
|
6
12
|
function getBasicAuth(auth: AuthValue): string;
|
package/util.js
CHANGED
|
@@ -1,11 +1,23 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.hasBasicAuth = exports.getBasicAuth = exports.formatError = exports.generateFilename = exports.readableAsBuffer = void 0;
|
|
3
|
+
exports.hasBasicAuth = exports.getBasicAuth = exports.formatError = exports.generateFilename = exports.readableAsBuffer = exports.IMPORTS = void 0;
|
|
4
4
|
const path = require("path");
|
|
5
5
|
const types_1 = require("../types");
|
|
6
6
|
const util_1 = require("../db/util");
|
|
7
7
|
Object.defineProperty(exports, "getBasicAuth", { enumerable: true, get: function () { return util_1.getBasicAuth; } });
|
|
8
8
|
Object.defineProperty(exports, "hasBasicAuth", { enumerable: true, get: function () { return util_1.hasBasicAuth; } });
|
|
9
|
+
exports.IMPORTS = {
|
|
10
|
+
"atlas": "@pi-r/atlas",
|
|
11
|
+
"aws": "@pi-r/aws",
|
|
12
|
+
"aws-v3": "@pi-r/aws-v3",
|
|
13
|
+
"az": "@pi-r/azure",
|
|
14
|
+
"azure": "@pi-r/azure",
|
|
15
|
+
"gcloud": "@pi-r/gcp",
|
|
16
|
+
"gcp": "@pi-r/gcp",
|
|
17
|
+
"ibm": "@pi-r/ibm",
|
|
18
|
+
"minio": "@pi-r/minio",
|
|
19
|
+
"oci": "@pi-r/oci"
|
|
20
|
+
};
|
|
9
21
|
function readableAsBuffer(stream) {
|
|
10
22
|
return new Promise((resolve, reject) => {
|
|
11
23
|
let result = null;
|