@electrolux-oss/plugin-infrawallet-backend 0.1.7 → 0.1.9

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/config.d.ts CHANGED
@@ -41,6 +41,43 @@ export interface Config {
41
41
  tags?: string[];
42
42
  },
43
43
  ];
44
+ mock?: [
45
+ {
46
+ name: string;
47
+ },
48
+ ];
49
+ };
50
+ metricProviders?: {
51
+ datadog?: [
52
+ {
53
+ name: string;
54
+ /**
55
+ * @visibility secret
56
+ */
57
+ apiKey: string;
58
+ /**
59
+ * @visibility secret
60
+ */
61
+ applicationKey: string;
62
+ ddSite: string;
63
+ },
64
+ ];
65
+ grafanaCloud?: [
66
+ {
67
+ name: string;
68
+ url: string;
69
+ datasourceUid: string;
70
+ /**
71
+ * @visibility secret
72
+ */
73
+ token: string;
74
+ },
75
+ ];
76
+ mock?: [
77
+ {
78
+ name: string;
79
+ },
80
+ ];
44
81
  };
45
82
  };
46
83
  };
package/dist/index.cjs.js CHANGED
@@ -14,39 +14,86 @@ var armCostmanagement = require('@azure/arm-costmanagement');
14
14
  var coreRestPipeline = require('@azure/core-rest-pipeline');
15
15
  var identity = require('@azure/identity');
16
16
  var bigquery = require('@google-cloud/bigquery');
17
+ var datadogApiClient = require('@datadog/datadog-api-client');
18
+ var fetch = require('node-fetch');
19
+ var fs = require('fs');
20
+ var upath = require('upath');
17
21
 
18
22
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
19
23
 
24
+ function _interopNamespaceCompat(e) {
25
+ if (e && typeof e === 'object' && 'default' in e) return e;
26
+ var n = Object.create(null);
27
+ if (e) {
28
+ Object.keys(e).forEach(function (k) {
29
+ if (k !== 'default') {
30
+ var d = Object.getOwnPropertyDescriptor(e, k);
31
+ Object.defineProperty(n, k, d.get ? d : {
32
+ enumerable: true,
33
+ get: function () { return e[k]; }
34
+ });
35
+ }
36
+ });
37
+ }
38
+ n.default = e;
39
+ return Object.freeze(n);
40
+ }
41
+
20
42
  var express__default = /*#__PURE__*/_interopDefaultCompat(express);
21
43
  var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
22
44
  var moment__default = /*#__PURE__*/_interopDefaultCompat(moment);
45
+ var fetch__default = /*#__PURE__*/_interopDefaultCompat(fetch);
46
+ var upath__namespace = /*#__PURE__*/_interopNamespaceCompat(upath);
47
+
48
+ async function getWallet(database, walletName) {
49
+ const client = await database.getClient();
50
+ const result = await client("wallets").where("name", walletName).first();
51
+ return result;
52
+ }
53
+ async function getWalletMetricSettings(database, walletName) {
54
+ const client = await database.getClient();
55
+ const metricSettings = await client.select("business_metrics.*").from("business_metrics").where("wallets.name", walletName).join("wallets", "business_metrics.wallet_id", "=", "wallets.id");
56
+ return metricSettings;
57
+ }
58
+ async function updateOrInsertWalletMetricSetting(database, walletSetting) {
59
+ const client = await database.getClient();
60
+ const result = await client("business_metrics").insert(walletSetting).onConflict("id").merge();
61
+ if (result[0] > 0) {
62
+ return true;
63
+ }
64
+ return false;
65
+ }
66
+ async function deleteWalletMetricSetting(database, walletSetting) {
67
+ const client = await database.getClient();
68
+ const result = await client("business_metrics").where("id", walletSetting.id).del();
69
+ if (result > 0) {
70
+ return true;
71
+ }
72
+ return false;
73
+ }
23
74
 
24
75
  async function getCategoryMappings(database, provider) {
25
76
  const result = {};
26
77
  const client = await database.getClient();
27
- const default_mappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
28
- default_mappings.forEach((mapping) => {
29
- if (typeof mapping.cloud_service_names === "string") {
30
- JSON.parse(mapping.cloud_service_names).forEach((service) => {
31
- result[service] = mapping.category;
32
- });
33
- } else {
34
- mapping.cloud_service_names.forEach((service) => {
35
- result[service] = mapping.category;
36
- });
78
+ const defaultMappings = await client.where({ provider: provider.toLowerCase() }).select().from("category_mappings_default");
79
+ defaultMappings.forEach((mapping) => {
80
+ let services = mapping.cloud_service_names;
81
+ if (typeof services === "string") {
82
+ services = JSON.parse(services);
37
83
  }
84
+ services.forEach((service) => {
85
+ result[service] = mapping.category;
86
+ });
38
87
  });
39
- const override_mappings = await client.where({ provider }).select().from("category_mappings_override");
40
- override_mappings.forEach((mapping) => {
41
- if (typeof mapping.cloud_service_names === "string") {
42
- JSON.parse(mapping.cloud_service_names).forEach((service) => {
43
- result[service] = mapping.category;
44
- });
45
- } else {
46
- mapping.cloud_service_names.forEach((service) => {
47
- result[service] = mapping.category;
48
- });
88
+ const overrideMappings = await client.where({ provider }).select().from("category_mappings_override");
89
+ overrideMappings.forEach((mapping) => {
90
+ let services = mapping.cloud_service_names;
91
+ if (typeof services === "string") {
92
+ services = JSON.parse(services);
49
93
  }
94
+ services.forEach((service) => {
95
+ result[service] = mapping.category;
96
+ });
50
97
  });
51
98
  return result;
52
99
  }
@@ -54,6 +101,14 @@ function getCategoryByServiceName(serviceName, categoryMappings) {
54
101
  if (serviceName in categoryMappings) {
55
102
  return categoryMappings[serviceName];
56
103
  }
104
+ for (const service in categoryMappings) {
105
+ if (Object.hasOwn(categoryMappings, service)) {
106
+ const regex = new RegExp(service);
107
+ if (regex.test(serviceName)) {
108
+ return categoryMappings[service];
109
+ }
110
+ }
111
+ }
57
112
  return "Uncategorized";
58
113
  }
59
114
  async function getReportsFromCache(cache, provider, configKey, query) {
@@ -69,6 +124,20 @@ async function getReportsFromCache(cache, provider, configKey, query) {
69
124
  const cachedCosts = await cache.get(cacheKey);
70
125
  return cachedCosts;
71
126
  }
127
+ async function getMetricsFromCache(cache, provider, configKey, query) {
128
+ const cacheKey = [
129
+ provider,
130
+ configKey,
131
+ query.name,
132
+ query.query,
133
+ query.granularity,
134
+ query.startTime,
135
+ query.endTime
136
+ ].join("_");
137
+ const crypto = require("crypto");
138
+ const cachedMetrics = await cache.get(crypto.createHash("md5").update(cacheKey).digest("hex"));
139
+ return cachedMetrics;
140
+ }
72
141
  async function setReportsToCache(cache, reports, provider, configKey, query, ttl) {
73
142
  const cacheKey = [
74
143
  provider,
@@ -83,6 +152,21 @@ async function setReportsToCache(cache, reports, provider, configKey, query, ttl
83
152
  ttl: ttl
84
153
  });
85
154
  }
155
+ async function setMetricsToCache(cache, metrics, provider, configKey, query, ttl) {
156
+ const cacheKey = [
157
+ provider,
158
+ configKey,
159
+ query.name,
160
+ query.query,
161
+ query.granularity,
162
+ query.startTime,
163
+ query.endTime
164
+ ].join("_");
165
+ const crypto = require("crypto");
166
+ await cache.set(crypto.createHash("md5").update(cacheKey).digest("hex"), metrics, {
167
+ ttl: ttl
168
+ });
169
+ }
86
170
 
87
171
  class InfraWalletClient {
88
172
  constructor(providerName, config, database, cache, logger) {
@@ -541,10 +625,305 @@ class GCPClient extends InfraWalletClient {
541
625
  }
542
626
  }
543
627
 
544
- const PROVIDER_CLIENT_MAPPINGS = {
628
+ class MetricProvider {
629
+ constructor(providerName, config, database, cache, logger) {
630
+ this.providerName = providerName;
631
+ this.config = config;
632
+ this.database = database;
633
+ this.cache = cache;
634
+ this.logger = logger;
635
+ }
636
+ async getMetrics(query) {
637
+ const conf = this.config.getOptionalConfigArray(
638
+ `backend.infraWallet.metricProviders.${this.providerName.toLowerCase()}`
639
+ );
640
+ if (!conf) {
641
+ return { metrics: [], errors: [] };
642
+ }
643
+ const promises = [];
644
+ const results = [];
645
+ const errors = [];
646
+ for (const c of conf) {
647
+ const configName = c.getString("name");
648
+ const client = await this.initProviderClient(c);
649
+ const dbClient = await this.database.getClient();
650
+ const metricSettings = await dbClient.where({
651
+ "wallets.name": query.walletName,
652
+ "business_metrics.metric_provider": this.providerName.toLowerCase(),
653
+ "business_metrics.config_name": configName
654
+ }).select("business_metrics.*").from("business_metrics").join("wallets", "business_metrics.wallet_id", "=", "wallets.id");
655
+ for (const metric of metricSettings || []) {
656
+ const promise = (async () => {
657
+ try {
658
+ const fullQuery = {
659
+ name: metric.metric_name,
660
+ query: metric.query,
661
+ ...query
662
+ };
663
+ const cachedMetrics = await getMetricsFromCache(this.cache, this.providerName, configName, fullQuery);
664
+ if (cachedMetrics) {
665
+ this.logger.debug(`${this.providerName}/${configName}/${fullQuery.name} metrics from cache`);
666
+ cachedMetrics.map((m) => {
667
+ results.push(m);
668
+ });
669
+ return;
670
+ }
671
+ const metricResponse = await this.fetchMetrics(c, client, fullQuery);
672
+ const transformedMetrics = await this.transformMetricData(c, fullQuery, metricResponse);
673
+ await setMetricsToCache(
674
+ this.cache,
675
+ transformedMetrics,
676
+ this.providerName,
677
+ configName,
678
+ fullQuery,
679
+ 60 * 60 * 2 * 1e3
680
+ );
681
+ transformedMetrics.map((value) => {
682
+ results.push(value);
683
+ });
684
+ } catch (e) {
685
+ this.logger.error(e);
686
+ errors.push({
687
+ provider: this.providerName,
688
+ name: `${this.providerName}/${configName}/${metric.getString("metricName")}`,
689
+ error: e.message
690
+ });
691
+ }
692
+ })();
693
+ promises.push(promise);
694
+ }
695
+ }
696
+ await Promise.all(promises);
697
+ return {
698
+ metrics: results,
699
+ errors
700
+ };
701
+ }
702
+ }
703
+
704
+ class DatadogProvider extends MetricProvider {
705
+ static create(config, database, cache, logger) {
706
+ return new DatadogProvider("Datadog", config, database, cache, logger);
707
+ }
708
+ async initProviderClient(config) {
709
+ const apiKey = config.getString("apiKey");
710
+ const applicationKey = config.getString("applicationKey");
711
+ const ddSite = config.getString("ddSite");
712
+ const configuration = datadogApiClient.client.createConfiguration({
713
+ baseServer: new datadogApiClient.client.BaseServerConfiguration(ddSite, {}),
714
+ authMethods: {
715
+ apiKeyAuth: apiKey,
716
+ appKeyAuth: applicationKey
717
+ }
718
+ });
719
+ const client = new datadogApiClient.v1.MetricsApi(configuration);
720
+ return client;
721
+ }
722
+ async fetchMetrics(_metricProviderConfig, client, query) {
723
+ var _a;
724
+ const params = {
725
+ from: parseInt(query.startTime, 10) / 1e3,
726
+ to: parseInt(query.endTime, 10) / 1e3,
727
+ query: (_a = query.query) == null ? void 0 : _a.replaceAll("IW_INTERVAL", query.granularity === "daily" ? "86400" : "2592000")
728
+ };
729
+ return client.queryMetrics(params).then((data) => {
730
+ if (data.status === "ok") {
731
+ return data;
732
+ }
733
+ throw new Error(data.error);
734
+ });
735
+ }
736
+ async transformMetricData(_metricProviderConfig, query, metricResponse) {
737
+ const transformedData = [];
738
+ for (const series of metricResponse.series) {
739
+ const metricName = query.name;
740
+ const tagSet = series.tagSet;
741
+ const metric = {
742
+ id: `${metricName} ${tagSet.length === 0 ? "" : tagSet}`,
743
+ provider: this.providerName,
744
+ name: metricName,
745
+ reports: {}
746
+ };
747
+ for (const point of series.pointlist) {
748
+ const period = moment__default.default(point[0]).format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
749
+ const value = point[1];
750
+ metric.reports[period] = value;
751
+ }
752
+ transformedData.push(metric);
753
+ }
754
+ return transformedData;
755
+ }
756
+ }
757
+
758
+ class GrafanaCloudProvider extends MetricProvider {
759
+ static create(config, database, cache, logger) {
760
+ return new GrafanaCloudProvider("GrafanaCloud", config, database, cache, logger);
761
+ }
762
+ async initProviderClient(_config) {
763
+ return null;
764
+ }
765
+ async fetchMetrics(metricProviderConfig, _client, query) {
766
+ var _a;
767
+ const url = metricProviderConfig.getString("url");
768
+ const datasourceUid = metricProviderConfig.getString("datasourceUid");
769
+ const token = metricProviderConfig.getString("token");
770
+ const headers = {
771
+ "Content-Type": "application/json",
772
+ Authorization: `Bearer ${token}`
773
+ };
774
+ const payload = {
775
+ queries: [
776
+ {
777
+ datasource: {
778
+ uid: datasourceUid
779
+ },
780
+ expr: (_a = query.query) == null ? void 0 : _a.replaceAll("IW_INTERVAL", query.granularity === "daily" ? "1d" : "30d"),
781
+ refId: "A"
782
+ }
783
+ ],
784
+ from: query.startTime,
785
+ to: query.endTime
786
+ };
787
+ const response = await fetch__default.default(`${url}/api/ds/query`, {
788
+ method: "post",
789
+ body: JSON.stringify(payload),
790
+ headers
791
+ });
792
+ const data = await response.json();
793
+ return data;
794
+ }
795
+ async transformMetricData(_metricProviderConfig, query, metricResponse) {
796
+ const transformedData = [];
797
+ const metricName = query.name;
798
+ const metric = {
799
+ id: metricName,
800
+ provider: this.providerName,
801
+ name: metricName,
802
+ reports: {}
803
+ };
804
+ const periods = metricResponse.results.A.frames[0].data.values[0];
805
+ const values = metricResponse.results.A.frames[0].data.values[1];
806
+ for (let i = 0; i < periods.length; i++) {
807
+ const period = moment__default.default(periods[i]).format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
808
+ const value = values[i];
809
+ metric.reports[period] = value;
810
+ }
811
+ transformedData.push(metric);
812
+ return transformedData;
813
+ }
814
+ }
815
+
816
+ class MockProvider extends MetricProvider {
817
+ static create(config, database, cache, logger) {
818
+ return new MockProvider("Mock", config, database, cache, logger);
819
+ }
820
+ async initProviderClient(_config) {
821
+ return null;
822
+ }
823
+ async fetchMetrics(_metricProviderConfig, _client, _query) {
824
+ return null;
825
+ }
826
+ async transformMetricData(_metricProviderConfig, query, _metricResponse) {
827
+ var _a, _b;
828
+ const transformedData = [];
829
+ const metricName = query.name;
830
+ let mockSettings = {};
831
+ try {
832
+ mockSettings = JSON.parse(query.query);
833
+ } catch (e) {
834
+ }
835
+ const minValue = (_a = mockSettings.min) != null ? _a : 0;
836
+ const maxValue = (_b = mockSettings.max) != null ? _b : 1e3;
837
+ const metric = {
838
+ id: metricName,
839
+ provider: this.providerName,
840
+ name: metricName,
841
+ reports: {}
842
+ };
843
+ let cursor = moment__default.default(parseInt(query.startTime, 10));
844
+ while (cursor <= moment__default.default(parseInt(query.endTime, 10))) {
845
+ const period = cursor.format(query.granularity === "daily" ? "YYYY-MM-DD" : "YYYY-MM");
846
+ metric.reports[period] = Math.floor(Math.random() * (maxValue - minValue) + minValue);
847
+ cursor = cursor.add(1, query.granularity === "daily" ? "days" : "months");
848
+ }
849
+ transformedData.push(metric);
850
+ return transformedData;
851
+ }
852
+ }
853
+
854
+ class MockClient extends InfraWalletClient {
855
+ static create(config, database, cache, logger) {
856
+ return new MockClient("mock", config, database, cache, logger);
857
+ }
858
+ async initCloudClient(config) {
859
+ this.logger.debug(`MockClient.initCloudClient called with config: ${JSON.stringify(config)}`);
860
+ return null;
861
+ }
862
+ async fetchCostsFromCloud(_subAccountConfig, _client, _query) {
863
+ return null;
864
+ }
865
+ async transformCostsData(_subAccountConfig, query, _costResponse, _categoryMappings) {
866
+ try {
867
+ const startD = moment__default.default.unix(Number(query.startTime) / 1e3);
868
+ let endD = moment__default.default.unix(Number(query.endTime) / 1e3);
869
+ const mockDir = backendPluginApi.resolvePackagePath("@electrolux-oss/plugin-infrawallet-backend", "mock");
870
+ const mockFilePath = upath__namespace.join(mockDir, "mock_response.json");
871
+ const data = await fs.promises.readFile(mockFilePath, "utf8");
872
+ const jsonData = JSON.parse(data);
873
+ const currentDate = moment__default.default();
874
+ if (endD.isAfter(currentDate)) {
875
+ this.logger.warn("End Date is in the future, adjusting to current date.");
876
+ endD = currentDate.clone();
877
+ endD.add(1, "day");
878
+ }
879
+ const processedData = await Promise.all(
880
+ jsonData.map(async (item) => {
881
+ item.reports = {};
882
+ const StartDate = moment__default.default(startD);
883
+ let step;
884
+ let dateFormat = "YYYY-MM";
885
+ if (query.granularity.toLowerCase() === "monthly") {
886
+ step = "months";
887
+ dateFormat = "YYYY-MM";
888
+ } else if (query.granularity.toLowerCase() === "daily") {
889
+ step = "days";
890
+ dateFormat = "YYYY-MM-DD";
891
+ }
892
+ while (StartDate.isBefore(endD)) {
893
+ const dateString = StartDate.format(dateFormat);
894
+ if (query.granularity.toLowerCase() === "monthly") {
895
+ item.reports[dateString] = this.getRandomValue(0.4 * 30, 33.3 * 30);
896
+ } else {
897
+ item.reports[dateString] = this.getRandomValue(0.4, 33.3);
898
+ }
899
+ StartDate.add(1, step);
900
+ }
901
+ return item;
902
+ })
903
+ );
904
+ return processedData;
905
+ } catch (err) {
906
+ this.logger.error("Error while reading a file", err);
907
+ throw err;
908
+ }
909
+ }
910
+ getRandomValue(min, max) {
911
+ const random = Math.random();
912
+ const amplifiedRandom = Math.pow(random, 3);
913
+ return amplifiedRandom * (max - min) + min;
914
+ }
915
+ }
916
+
917
+ const COST_CLIENT_MAPPINGS = {
545
918
  aws: AwsClient,
546
919
  azure: AzureClient,
547
- gcp: GCPClient
920
+ gcp: GCPClient,
921
+ mock: MockClient
922
+ };
923
+ const METRIC_PROVIDER_MAPPINGS = {
924
+ datadog: DatadogProvider,
925
+ grafanacloud: GrafanaCloudProvider,
926
+ mock: MockProvider
548
927
  };
549
928
 
550
929
  async function setUpDatabase(database) {
@@ -579,8 +958,8 @@ async function createRouter(options) {
579
958
  const errors = [];
580
959
  const conf = config.getConfig("backend.infraWallet.integrations");
581
960
  conf.keys().forEach((provider) => {
582
- if (provider in PROVIDER_CLIENT_MAPPINGS) {
583
- const client = PROVIDER_CLIENT_MAPPINGS[provider].create(config, database, cache, logger);
961
+ if (provider in COST_CLIENT_MAPPINGS) {
962
+ const client = COST_CLIENT_MAPPINGS[provider].create(config, database, cache, logger);
584
963
  const fetchCloudCosts = (async () => {
585
964
  try {
586
965
  const clientResponse = await client.getCostReports({
@@ -615,6 +994,98 @@ async function createRouter(options) {
615
994
  response.json({ data: results, errors, status: 200 });
616
995
  }
617
996
  });
997
+ router.get("/:walletName/metrics", async (request, response) => {
998
+ const walletName = request.params.walletName;
999
+ const granularity = request.query.granularity;
1000
+ const startTime = request.query.startTime;
1001
+ const endTime = request.query.endTime;
1002
+ const promises = [];
1003
+ const results = [];
1004
+ const errors = [];
1005
+ const conf = config.getConfig("backend.infraWallet.metricProviders");
1006
+ conf.keys().forEach((provider) => {
1007
+ if (provider in METRIC_PROVIDER_MAPPINGS) {
1008
+ const client = METRIC_PROVIDER_MAPPINGS[provider].create(config, database, cache, logger);
1009
+ const fetchMetrics = (async () => {
1010
+ try {
1011
+ const metricResponse = await client.getMetrics({
1012
+ walletName,
1013
+ granularity,
1014
+ startTime,
1015
+ endTime
1016
+ });
1017
+ metricResponse.errors.forEach((e) => {
1018
+ errors.push(e);
1019
+ });
1020
+ metricResponse.metrics.forEach((metric) => {
1021
+ results.push(metric);
1022
+ });
1023
+ } catch (e) {
1024
+ logger.error(e);
1025
+ errors.push({
1026
+ provider: client.constructor.name,
1027
+ name: client.constructor.name,
1028
+ error: e.message
1029
+ });
1030
+ }
1031
+ })();
1032
+ promises.push(fetchMetrics);
1033
+ }
1034
+ });
1035
+ await Promise.all(promises);
1036
+ if (errors.length > 0) {
1037
+ response.status(207).json({ data: results, errors, status: 207 });
1038
+ } else {
1039
+ response.json({ data: results, errors, status: 200 });
1040
+ }
1041
+ });
1042
+ router.get("/:walletName", async (request, response) => {
1043
+ const walletName = request.params.walletName;
1044
+ const wallet = await getWallet(database, walletName);
1045
+ if (wallet === void 0) {
1046
+ response.status(404).json({ error: "Wallet not found", status: 404 });
1047
+ return;
1048
+ }
1049
+ response.json({ data: wallet, status: 200 });
1050
+ });
1051
+ router.get("/:walletName/metrics_setting", async (request, response) => {
1052
+ const walletName = request.params.walletName;
1053
+ const metricSettings = await getWalletMetricSettings(database, walletName);
1054
+ response.json({ data: metricSettings, status: 200 });
1055
+ });
1056
+ router.get("/metric/metric_configs", async (_request, response) => {
1057
+ const conf = config.getConfig("backend.infraWallet.metricProviders");
1058
+ const configNames = [];
1059
+ conf.keys().forEach((provider) => {
1060
+ const configs = conf.getOptionalConfigArray(provider);
1061
+ if (configs) {
1062
+ configs.forEach((c) => {
1063
+ configNames.push({ metric_provider: provider, config_name: c.getString("name") });
1064
+ });
1065
+ }
1066
+ });
1067
+ response.json({ data: configNames, status: 200 });
1068
+ });
1069
+ router.put("/:walletName/metrics_setting", async (request, response) => {
1070
+ var _a;
1071
+ const readOnly = (_a = config.getOptionalBoolean("infraWallet.settings.readOnly")) != null ? _a : false;
1072
+ if (readOnly) {
1073
+ response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
1074
+ return;
1075
+ }
1076
+ const updatedMetricSetting = await updateOrInsertWalletMetricSetting(database, request.body);
1077
+ response.json({ updated: updatedMetricSetting, status: 200 });
1078
+ });
1079
+ router.delete("/:walletName/metrics_setting", async (request, response) => {
1080
+ var _a;
1081
+ const readOnly = (_a = config.getOptionalBoolean("infraWallet.settings.readOnly")) != null ? _a : false;
1082
+ if (readOnly) {
1083
+ response.status(403).json({ error: "API not enabled in read-only mode", status: 403 });
1084
+ return;
1085
+ }
1086
+ const deletedMetricSetting = await deleteWalletMetricSetting(database, request.body);
1087
+ response.json({ deleted: deletedMetricSetting, status: 200 });
1088
+ });
618
1089
  router.use(backendCommon.errorHandler());
619
1090
  return router;
620
1091
  }