@iswesolutions/plugin-monitoring-backend 0.0.0-stage → 0.1.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/README.md CHANGED
@@ -1,3 +1,41 @@
1
- # Temporary Holding Version
1
+ # Monitoring backend plugin
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ The backend of [`@iswesolutions/plugin-monitoring`](../monitoring): the
4
+ **shared settings** of its Settings tab (cost rates, electricity tariff, PUE,
5
+ CO₂ factor ...), kept in Backstage's database. A saved setting overrides the
6
+ frontend's app-config (`monitoring.<key>`); removing it goes back to
7
+ app-config.
8
+
9
+ | Route | Who | Does |
10
+ | ------------------------------ | ---------------------------- | ------------------------------------------------------------------------------------------------------- |
11
+ | `GET /api/monitoring/settings` | every signed-in user | `{ settings: { key: { value, updatedBy, updatedAt } }, keys, canEdit }` |
12
+ | `PUT /api/monitoring/settings` | `monitoring.settings.update` | `{ changes: { key: value \| null } }`: saves, or removes with `null`; validated (ranges, currency code) |
13
+
14
+ Settings: `costs.currency`, `costs.monthlyInfrastructureCost`,
15
+ `costs.split.{cpu,memory,storage,network,backup}` (0 to 1),
16
+ `costs.rates.{cpuCoreMonth,memoryGbMonth,storageGbMonth,networkGb,backupGbMonth,guestMonth}`,
17
+ `costs.electricity.tariffPerKwh`, `costs.electricity.pue` (1 to 3),
18
+ `costs.electricity.co2KgPerKwh`.
19
+
20
+ ## Installation
21
+
22
+ ```sh
23
+ yarn --cwd packages/backend add @iswesolutions/plugin-monitoring-backend
24
+ ```
25
+
26
+ ```ts
27
+ // packages/backend/src/index.ts
28
+ backend.add(import('@iswesolutions/plugin-monitoring-backend'));
29
+ ```
30
+
31
+ The plugin registers the `monitoring.settings.update` permission. With the
32
+ community RBAC plugin, list it so roles can be given it:
33
+
34
+ ```yaml
35
+ permission:
36
+ rbac:
37
+ pluginsWithPermission: [monitoring]
38
+ ```
39
+
40
+ Super users have it already. The table `monitoring_settings` is created on
41
+ first start.
@@ -0,0 +1,14 @@
1
+ 'use strict';
2
+
3
+ Object.defineProperty(exports, '__esModule', { value: true });
4
+
5
+ var plugin = require('./plugin.cjs.js');
6
+ var settings = require('./settings.cjs.js');
7
+
8
+
9
+
10
+ exports.default = plugin.monitoringBackend;
11
+ exports.SETTINGS = settings.SETTINGS;
12
+ exports.monitoringPermissions = settings.monitoringPermissions;
13
+ exports.monitoringSettingsUpdatePermission = settings.monitoringSettingsUpdatePermission;
14
+ //# sourceMappingURL=index.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs.js","sources":[],"sourcesContent":[],"names":[],"mappings":";;;;;;;;;;;;"}
@@ -0,0 +1,30 @@
1
+ import * as _backstage_backend_plugin_api from '@backstage/backend-plugin-api';
2
+ import * as _backstage_plugin_permission_common from '@backstage/plugin-permission-common';
3
+
4
+ /**
5
+ * Backend for @iswesolutions/plugin-monitoring: the shared settings edited on its Settings tab
6
+ * (cost rates, electricity tariff ...), kept in Backstage's database. Changing them needs
7
+ * monitoring.settings.update.
8
+ */
9
+ declare const monitoringBackend: _backstage_backend_plugin_api.BackendFeature;
10
+
11
+ /**
12
+ * Change the Monitoring plugin's shared settings (Settings tab). Reading them is open to every
13
+ * signed-in user. Grant it with a role on the RBAC Administration page, or to super users.
14
+ */
15
+ declare const monitoringSettingsUpdatePermission: _backstage_plugin_permission_common.BasicPermission;
16
+ declare const monitoringPermissions: _backstage_plugin_permission_common.BasicPermission[];
17
+ type Rule = {
18
+ type: 'number';
19
+ min: number;
20
+ max?: number;
21
+ } | {
22
+ type: 'currency';
23
+ };
24
+ /**
25
+ * The settings that can be changed, by their app-config key under `monitoring.`. A saved value
26
+ * overrides app-config; removing it falls back to app-config again.
27
+ */
28
+ declare const SETTINGS: Record<string, Rule>;
29
+
30
+ export { SETTINGS, monitoringBackend as default, monitoringPermissions, monitoringSettingsUpdatePermission };
@@ -0,0 +1,40 @@
1
+ 'use strict';
2
+
3
+ var backendPluginApi = require('@backstage/backend-plugin-api');
4
+ var router = require('./router.cjs.js');
5
+ var settings = require('./settings.cjs.js');
6
+ var store = require('./store.cjs.js');
7
+
8
+ const monitoringBackend = backendPluginApi.createBackendPlugin({
9
+ pluginId: "monitoring",
10
+ register(env) {
11
+ env.registerInit({
12
+ deps: {
13
+ logger: backendPluginApi.coreServices.logger,
14
+ httpRouter: backendPluginApi.coreServices.httpRouter,
15
+ httpAuth: backendPluginApi.coreServices.httpAuth,
16
+ permissions: backendPluginApi.coreServices.permissions,
17
+ permissionsRegistry: backendPluginApi.coreServices.permissionsRegistry,
18
+ database: backendPluginApi.coreServices.database
19
+ },
20
+ async init({
21
+ logger,
22
+ httpRouter,
23
+ httpAuth,
24
+ permissions,
25
+ permissionsRegistry,
26
+ database
27
+ }) {
28
+ permissionsRegistry.addPermissions(settings.monitoringPermissions);
29
+ const store$1 = await store.createSettingsStore(database);
30
+ httpRouter.use(
31
+ await router.createRouter({ logger, httpAuth, permissions, store: store$1 })
32
+ );
33
+ logger.info("monitoring: settings backend initialised");
34
+ }
35
+ });
36
+ }
37
+ });
38
+
39
+ exports.monitoringBackend = monitoringBackend;
40
+ //# sourceMappingURL=plugin.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"plugin.cjs.js","sources":["../src/plugin.ts"],"sourcesContent":["import {\n coreServices,\n createBackendPlugin,\n} from '@backstage/backend-plugin-api';\nimport { createRouter } from './router';\nimport { monitoringPermissions } from './settings';\nimport { createSettingsStore } from './store';\n\n/**\n * Backend for @iswesolutions/plugin-monitoring: the shared settings edited on its Settings tab\n * (cost rates, electricity tariff ...), kept in Backstage's database. Changing them needs\n * monitoring.settings.update.\n */\nexport const monitoringBackend = createBackendPlugin({\n pluginId: 'monitoring',\n register(env) {\n env.registerInit({\n deps: {\n logger: coreServices.logger,\n httpRouter: coreServices.httpRouter,\n httpAuth: coreServices.httpAuth,\n permissions: coreServices.permissions,\n permissionsRegistry: coreServices.permissionsRegistry,\n database: coreServices.database,\n },\n async init({\n logger,\n httpRouter,\n httpAuth,\n permissions,\n permissionsRegistry,\n database,\n }) {\n permissionsRegistry.addPermissions(monitoringPermissions);\n const store = await createSettingsStore(database);\n httpRouter.use(\n await createRouter({ logger, httpAuth, permissions, store }),\n );\n logger.info('monitoring: settings backend initialised');\n },\n });\n },\n});\n"],"names":["createBackendPlugin","coreServices","monitoringPermissions","store","createSettingsStore","createRouter"],"mappings":";;;;;;;AAaO,MAAM,oBAAoBA,oCAAA,CAAoB;AAAA,EACnD,QAAA,EAAU,YAAA;AAAA,EACV,SAAS,GAAA,EAAK;AACZ,IAAA,GAAA,CAAI,YAAA,CAAa;AAAA,MACf,IAAA,EAAM;AAAA,QACJ,QAAQC,6BAAA,CAAa,MAAA;AAAA,QACrB,YAAYA,6BAAA,CAAa,UAAA;AAAA,QACzB,UAAUA,6BAAA,CAAa,QAAA;AAAA,QACvB,aAAaA,6BAAA,CAAa,WAAA;AAAA,QAC1B,qBAAqBA,6BAAA,CAAa,mBAAA;AAAA,QAClC,UAAUA,6BAAA,CAAa;AAAA,OACzB;AAAA,MACA,MAAM,IAAA,CAAK;AAAA,QACT,MAAA;AAAA,QACA,UAAA;AAAA,QACA,QAAA;AAAA,QACA,WAAA;AAAA,QACA,mBAAA;AAAA,QACA;AAAA,OACF,EAAG;AACD,QAAA,mBAAA,CAAoB,eAAeC,8BAAqB,CAAA;AACxD,QAAA,MAAMC,OAAA,GAAQ,MAAMC,yBAAA,CAAoB,QAAQ,CAAA;AAChD,QAAA,UAAA,CAAW,GAAA;AAAA,UACT,MAAMC,mBAAA,CAAa,EAAE,QAAQ,QAAA,EAAU,WAAA,SAAaF,SAAO;AAAA,SAC7D;AACA,QAAA,MAAA,CAAO,KAAK,0CAA0C,CAAA;AAAA,MACxD;AAAA,KACD,CAAA;AAAA,EACH;AACF,CAAC;;;;"}
@@ -0,0 +1,56 @@
1
+ 'use strict';
2
+
3
+ var express = require('express');
4
+ var Router = require('express-promise-router');
5
+ var errors = require('@backstage/errors');
6
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
7
+ var settings = require('./settings.cjs.js');
8
+
9
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
10
+
11
+ var express__default = /*#__PURE__*/_interopDefaultCompat(express);
12
+ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
13
+
14
+ async function createRouter(options) {
15
+ const { logger, httpAuth, permissions, store } = options;
16
+ const canEdit = async (req) => {
17
+ const credentials = await httpAuth.credentials(req, { allow: ["user"] });
18
+ const [decision] = await permissions.authorize(
19
+ [{ permission: settings.monitoringSettingsUpdatePermission }],
20
+ { credentials }
21
+ );
22
+ return { credentials, allowed: decision.result === pluginPermissionCommon.AuthorizeResult.ALLOW };
23
+ };
24
+ const answer = async (allowed) => ({
25
+ settings: await store.all(),
26
+ keys: Object.keys(settings.SETTINGS),
27
+ canEdit: allowed
28
+ });
29
+ const router = Router__default.default();
30
+ router.use(express__default.default.json());
31
+ router.get("/settings", async (req, res) => {
32
+ const { allowed } = await canEdit(req);
33
+ res.json(await answer(allowed));
34
+ });
35
+ router.put("/settings", async (req, res) => {
36
+ const { credentials, allowed } = await canEdit(req);
37
+ if (!allowed) {
38
+ throw new errors.NotAllowedError(
39
+ "changing Monitoring settings needs monitoring.settings.update"
40
+ );
41
+ }
42
+ const changes = req.body?.changes;
43
+ const problems = settings.validate(changes);
44
+ if (problems.length) throw new errors.InputError(problems.join("; "));
45
+ const by = credentials.principal.userEntityRef;
46
+ await store.apply(changes, by);
47
+ logger.info(
48
+ `monitoring settings changed by ${by}: ${JSON.stringify(changes)}`
49
+ );
50
+ res.json(await answer(true));
51
+ });
52
+ return router;
53
+ }
54
+
55
+ exports.createRouter = createRouter;
56
+ //# sourceMappingURL=router.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"router.cjs.js","sources":["../src/router.ts"],"sourcesContent":["import express from 'express';\nimport Router from 'express-promise-router';\nimport {\n HttpAuthService,\n LoggerService,\n PermissionsService,\n} from '@backstage/backend-plugin-api';\nimport { InputError, NotAllowedError } from '@backstage/errors';\nimport { AuthorizeResult } from '@backstage/plugin-permission-common';\nimport {\n monitoringSettingsUpdatePermission,\n SETTINGS,\n SettingValue,\n validate,\n} from './settings';\nimport { SettingsStore } from './store';\n\n/**\n * GET /settings every signed-in user: { settings: { key: { value, updatedBy, updatedAt } },\n * keys: [...], canEdit }\n * PUT /settings with monitoring.settings.update: { changes: { key: value | null } } (null removes\n * a setting, so app-config applies again); answers like GET\n */\nexport async function createRouter(options: {\n logger: LoggerService;\n httpAuth: HttpAuthService;\n permissions: PermissionsService;\n store: SettingsStore;\n}): Promise<express.Router> {\n const { logger, httpAuth, permissions, store } = options;\n\n const canEdit = async (req: express.Request) => {\n const credentials = await httpAuth.credentials(req, { allow: ['user'] });\n const [decision] = await permissions.authorize(\n [{ permission: monitoringSettingsUpdatePermission }],\n { credentials },\n );\n return { credentials, allowed: decision.result === AuthorizeResult.ALLOW };\n };\n const answer = async (allowed: boolean) => ({\n settings: await store.all(),\n keys: Object.keys(SETTINGS),\n canEdit: allowed,\n });\n\n const router = Router();\n router.use(express.json());\n\n router.get('/settings', async (req, res) => {\n const { allowed } = await canEdit(req);\n res.json(await answer(allowed));\n });\n\n router.put('/settings', async (req, res) => {\n const { credentials, allowed } = await canEdit(req);\n if (!allowed) {\n throw new NotAllowedError(\n 'changing Monitoring settings needs monitoring.settings.update',\n );\n }\n const changes = req.body?.changes;\n const problems = validate(changes);\n if (problems.length) throw new InputError(problems.join('; '));\n const by = credentials.principal.userEntityRef;\n await store.apply(changes as Record<string, SettingValue | null>, by);\n logger.info(\n `monitoring settings changed by ${by}: ${JSON.stringify(changes)}`,\n );\n res.json(await answer(true));\n });\n\n return router;\n}\n"],"names":["monitoringSettingsUpdatePermission","AuthorizeResult","SETTINGS","Router","express","NotAllowedError","validate","InputError"],"mappings":";;;;;;;;;;;;;AAuBA,eAAsB,aAAa,OAAA,EAKP;AAC1B,EAAA,MAAM,EAAE,MAAA,EAAQ,QAAA,EAAU,WAAA,EAAa,OAAM,GAAI,OAAA;AAEjD,EAAA,MAAM,OAAA,GAAU,OAAO,GAAA,KAAyB;AAC9C,IAAA,MAAM,WAAA,GAAc,MAAM,QAAA,CAAS,WAAA,CAAY,GAAA,EAAK,EAAE,KAAA,EAAO,CAAC,MAAM,CAAA,EAAG,CAAA;AACvE,IAAA,MAAM,CAAC,QAAQ,CAAA,GAAI,MAAM,WAAA,CAAY,SAAA;AAAA,MACnC,CAAC,EAAE,UAAA,EAAYA,2CAAA,EAAoC,CAAA;AAAA,MACnD,EAAE,WAAA;AAAY,KAChB;AACA,IAAA,OAAO,EAAE,WAAA,EAAa,OAAA,EAAS,QAAA,CAAS,MAAA,KAAWC,uCAAgB,KAAA,EAAM;AAAA,EAC3E,CAAA;AACA,EAAA,MAAM,MAAA,GAAS,OAAO,OAAA,MAAsB;AAAA,IAC1C,QAAA,EAAU,MAAM,KAAA,CAAM,GAAA,EAAI;AAAA,IAC1B,IAAA,EAAM,MAAA,CAAO,IAAA,CAAKC,iBAAQ,CAAA;AAAA,IAC1B,OAAA,EAAS;AAAA,GACX,CAAA;AAEA,EAAA,MAAM,SAASC,uBAAA,EAAO;AACtB,EAAA,MAAA,CAAO,GAAA,CAAIC,wBAAA,CAAQ,IAAA,EAAM,CAAA;AAEzB,EAAA,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC1C,IAAA,MAAM,EAAE,OAAA,EAAQ,GAAI,MAAM,QAAQ,GAAG,CAAA;AACrC,IAAA,GAAA,CAAI,IAAA,CAAK,MAAM,MAAA,CAAO,OAAO,CAAC,CAAA;AAAA,EAChC,CAAC,CAAA;AAED,EAAA,MAAA,CAAO,GAAA,CAAI,WAAA,EAAa,OAAO,GAAA,EAAK,GAAA,KAAQ;AAC1C,IAAA,MAAM,EAAE,WAAA,EAAa,OAAA,EAAQ,GAAI,MAAM,QAAQ,GAAG,CAAA;AAClD,IAAA,IAAI,CAAC,OAAA,EAAS;AACZ,MAAA,MAAM,IAAIC,sBAAA;AAAA,QACR;AAAA,OACF;AAAA,IACF;AACA,IAAA,MAAM,OAAA,GAAU,IAAI,IAAA,EAAM,OAAA;AAC1B,IAAA,MAAM,QAAA,GAAWC,kBAAS,OAAO,CAAA;AACjC,IAAA,IAAI,QAAA,CAAS,QAAQ,MAAM,IAAIC,kBAAW,QAAA,CAAS,IAAA,CAAK,IAAI,CAAC,CAAA;AAC7D,IAAA,MAAM,EAAA,GAAK,YAAY,SAAA,CAAU,aAAA;AACjC,IAAA,MAAM,KAAA,CAAM,KAAA,CAAM,OAAA,EAAgD,EAAE,CAAA;AACpE,IAAA,MAAA,CAAO,IAAA;AAAA,MACL,kCAAkC,EAAE,CAAA,EAAA,EAAK,IAAA,CAAK,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,KAClE;AACA,IAAA,GAAA,CAAI,IAAA,CAAK,MAAM,MAAA,CAAO,IAAI,CAAC,CAAA;AAAA,EAC7B,CAAC,CAAA;AAED,EAAA,OAAO,MAAA;AACT;;;;"}
@@ -0,0 +1,63 @@
1
+ 'use strict';
2
+
3
+ var pluginPermissionCommon = require('@backstage/plugin-permission-common');
4
+
5
+ const monitoringSettingsUpdatePermission = pluginPermissionCommon.createPermission({
6
+ name: "monitoring.settings.update",
7
+ attributes: { action: "update" }
8
+ });
9
+ const monitoringPermissions = [monitoringSettingsUpdatePermission];
10
+ const SETTINGS = {
11
+ "costs.currency": { type: "currency" },
12
+ "costs.monthlyInfrastructureCost": { type: "number", min: 0 },
13
+ "costs.split.cpu": { type: "number", min: 0, max: 1 },
14
+ "costs.split.memory": { type: "number", min: 0, max: 1 },
15
+ "costs.split.storage": { type: "number", min: 0, max: 1 },
16
+ "costs.split.network": { type: "number", min: 0, max: 1 },
17
+ "costs.split.backup": { type: "number", min: 0, max: 1 },
18
+ "costs.rates.cpuCoreMonth": { type: "number", min: 0 },
19
+ "costs.rates.memoryGbMonth": { type: "number", min: 0 },
20
+ "costs.rates.storageGbMonth": { type: "number", min: 0 },
21
+ "costs.rates.networkGb": { type: "number", min: 0 },
22
+ "costs.rates.backupGbMonth": { type: "number", min: 0 },
23
+ "costs.rates.guestMonth": { type: "number", min: 0 },
24
+ "costs.electricity.tariffPerKwh": { type: "number", min: 0 },
25
+ "costs.electricity.pue": { type: "number", min: 1, max: 3 },
26
+ "costs.electricity.co2KgPerKwh": { type: "number", min: 0, max: 2 }
27
+ };
28
+ function validate(changes) {
29
+ if (!changes || typeof changes !== "object" || Array.isArray(changes)) {
30
+ return ["expected an object of setting: value"];
31
+ }
32
+ const problems = [];
33
+ for (const [key, value] of Object.entries(
34
+ changes
35
+ )) {
36
+ const rule = SETTINGS[key];
37
+ if (!rule) {
38
+ problems.push(`${key}: not a setting`);
39
+ continue;
40
+ }
41
+ if (value === null) continue;
42
+ if (rule.type === "currency") {
43
+ if (typeof value !== "string" || !/^[A-Z]{3}$/.test(value)) {
44
+ problems.push(`${key}: a three-letter currency code, such as ZMW`);
45
+ }
46
+ continue;
47
+ }
48
+ if (typeof value !== "number" || !Number.isFinite(value)) {
49
+ problems.push(`${key}: a number`);
50
+ } else if (value < rule.min || rule.max !== void 0 && value > rule.max) {
51
+ problems.push(
52
+ rule.max === void 0 ? `${key}: ${rule.min} or more` : `${key}: between ${rule.min} and ${rule.max}`
53
+ );
54
+ }
55
+ }
56
+ return problems;
57
+ }
58
+
59
+ exports.SETTINGS = SETTINGS;
60
+ exports.monitoringPermissions = monitoringPermissions;
61
+ exports.monitoringSettingsUpdatePermission = monitoringSettingsUpdatePermission;
62
+ exports.validate = validate;
63
+ //# sourceMappingURL=settings.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"settings.cjs.js","sources":["../src/settings.ts"],"sourcesContent":["import { createPermission } from '@backstage/plugin-permission-common';\n\n/**\n * Change the Monitoring plugin's shared settings (Settings tab). Reading them is open to every\n * signed-in user. Grant it with a role on the RBAC Administration page, or to super users.\n */\nexport const monitoringSettingsUpdatePermission = createPermission({\n name: 'monitoring.settings.update',\n attributes: { action: 'update' },\n});\n\nexport const monitoringPermissions = [monitoringSettingsUpdatePermission];\n\ntype Rule =\n | { type: 'number'; min: number; max?: number }\n | { type: 'currency' };\n\n/**\n * The settings that can be changed, by their app-config key under `monitoring.`. A saved value\n * overrides app-config; removing it falls back to app-config again.\n */\nexport const SETTINGS: Record<string, Rule> = {\n 'costs.currency': { type: 'currency' },\n 'costs.monthlyInfrastructureCost': { type: 'number', min: 0 },\n 'costs.split.cpu': { type: 'number', min: 0, max: 1 },\n 'costs.split.memory': { type: 'number', min: 0, max: 1 },\n 'costs.split.storage': { type: 'number', min: 0, max: 1 },\n 'costs.split.network': { type: 'number', min: 0, max: 1 },\n 'costs.split.backup': { type: 'number', min: 0, max: 1 },\n 'costs.rates.cpuCoreMonth': { type: 'number', min: 0 },\n 'costs.rates.memoryGbMonth': { type: 'number', min: 0 },\n 'costs.rates.storageGbMonth': { type: 'number', min: 0 },\n 'costs.rates.networkGb': { type: 'number', min: 0 },\n 'costs.rates.backupGbMonth': { type: 'number', min: 0 },\n 'costs.rates.guestMonth': { type: 'number', min: 0 },\n 'costs.electricity.tariffPerKwh': { type: 'number', min: 0 },\n 'costs.electricity.pue': { type: 'number', min: 1, max: 3 },\n 'costs.electricity.co2KgPerKwh': { type: 'number', min: 0, max: 2 },\n};\n\nexport type SettingValue = number | string;\n\n/**\n * Checks a change: `{ key: value }` to save, `{ key: null }` to remove. Returns the problems, if any.\n */\nexport function validate(changes: unknown): string[] {\n if (!changes || typeof changes !== 'object' || Array.isArray(changes)) {\n return ['expected an object of setting: value'];\n }\n const problems: string[] = [];\n for (const [key, value] of Object.entries(\n changes as Record<string, unknown>,\n )) {\n const rule = SETTINGS[key];\n if (!rule) {\n problems.push(`${key}: not a setting`);\n continue;\n }\n if (value === null) continue;\n if (rule.type === 'currency') {\n if (typeof value !== 'string' || !/^[A-Z]{3}$/.test(value)) {\n problems.push(`${key}: a three-letter currency code, such as ZMW`);\n }\n continue;\n }\n if (typeof value !== 'number' || !Number.isFinite(value)) {\n problems.push(`${key}: a number`);\n } else if (\n value < rule.min ||\n (rule.max !== undefined && value > rule.max)\n ) {\n problems.push(\n rule.max === undefined\n ? `${key}: ${rule.min} or more`\n : `${key}: between ${rule.min} and ${rule.max}`,\n );\n }\n }\n return problems;\n}\n"],"names":["createPermission"],"mappings":"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"}
@@ -0,0 +1,49 @@
1
+ 'use strict';
2
+
3
+ const TABLE = "monitoring_settings";
4
+ async function createSettingsStore(database) {
5
+ const knex = await database.getClient();
6
+ if (!await knex.schema.hasTable(TABLE)) {
7
+ await knex.schema.createTable(TABLE, (t) => {
8
+ t.string("key").primary();
9
+ t.text("value").notNullable();
10
+ t.string("updated_by").notNullable();
11
+ t.timestamp("updated_at").notNullable();
12
+ });
13
+ }
14
+ return {
15
+ async all() {
16
+ const rows = await knex(TABLE).select("*");
17
+ return Object.fromEntries(
18
+ rows.map((r) => [
19
+ String(r.key),
20
+ {
21
+ value: JSON.parse(r.value),
22
+ updatedBy: String(r.updated_by),
23
+ updatedAt: new Date(r.updated_at).toISOString()
24
+ }
25
+ ])
26
+ );
27
+ },
28
+ async apply(changes, by) {
29
+ const now = /* @__PURE__ */ new Date();
30
+ await knex.transaction(async (tx) => {
31
+ for (const [key, value] of Object.entries(changes)) {
32
+ if (value === null) {
33
+ await tx(TABLE).where({ key }).delete();
34
+ } else {
35
+ await tx(TABLE).insert({
36
+ key,
37
+ value: JSON.stringify(value),
38
+ updated_by: by,
39
+ updated_at: now
40
+ }).onConflict("key").merge();
41
+ }
42
+ }
43
+ });
44
+ }
45
+ };
46
+ }
47
+
48
+ exports.createSettingsStore = createSettingsStore;
49
+ //# sourceMappingURL=store.cjs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.cjs.js","sources":["../src/store.ts"],"sourcesContent":["import { DatabaseService } from '@backstage/backend-plugin-api';\nimport { SettingValue } from './settings';\n\nconst TABLE = 'monitoring_settings';\n\nexport interface StoredSetting {\n value: SettingValue;\n updatedBy: string;\n updatedAt: string;\n}\n\nexport interface SettingsStore {\n all(): Promise<Record<string, StoredSetting>>;\n /** saves `{ key: value }`, removes `{ key: null }`, in one transaction */\n apply(\n changes: Record<string, SettingValue | null>,\n by: string,\n ): Promise<void>;\n}\n\n/** The shared settings, one row per setting (value as JSON), with who changed it and when. */\nexport async function createSettingsStore(\n database: DatabaseService,\n): Promise<SettingsStore> {\n const knex = await database.getClient();\n if (!(await knex.schema.hasTable(TABLE))) {\n await knex.schema.createTable(TABLE, t => {\n t.string('key').primary();\n t.text('value').notNullable();\n t.string('updated_by').notNullable();\n t.timestamp('updated_at').notNullable();\n });\n }\n return {\n async all() {\n const rows = await knex(TABLE).select('*');\n return Object.fromEntries(\n rows.map((r: any) => [\n String(r.key),\n {\n value: JSON.parse(r.value),\n updatedBy: String(r.updated_by),\n updatedAt: new Date(r.updated_at).toISOString(),\n },\n ]),\n );\n },\n async apply(changes, by) {\n const now = new Date();\n await knex.transaction(async tx => {\n for (const [key, value] of Object.entries(changes)) {\n if (value === null) {\n await tx(TABLE).where({ key }).delete();\n } else {\n await tx(TABLE)\n .insert({\n key,\n value: JSON.stringify(value),\n updated_by: by,\n updated_at: now,\n })\n .onConflict('key')\n .merge();\n }\n }\n });\n },\n };\n}\n"],"names":[],"mappings":";;AAGA,MAAM,KAAA,GAAQ,qBAAA;AAkBd,eAAsB,oBACpB,QAAA,EACwB;AACxB,EAAA,MAAM,IAAA,GAAO,MAAM,QAAA,CAAS,SAAA,EAAU;AACtC,EAAA,IAAI,CAAE,MAAM,IAAA,CAAK,MAAA,CAAO,QAAA,CAAS,KAAK,CAAA,EAAI;AACxC,IAAA,MAAM,IAAA,CAAK,MAAA,CAAO,WAAA,CAAY,KAAA,EAAO,CAAA,CAAA,KAAK;AACxC,MAAA,CAAA,CAAE,MAAA,CAAO,KAAK,CAAA,CAAE,OAAA,EAAQ;AACxB,MAAA,CAAA,CAAE,IAAA,CAAK,OAAO,CAAA,CAAE,WAAA,EAAY;AAC5B,MAAA,CAAA,CAAE,MAAA,CAAO,YAAY,CAAA,CAAE,WAAA,EAAY;AACnC,MAAA,CAAA,CAAE,SAAA,CAAU,YAAY,CAAA,CAAE,WAAA,EAAY;AAAA,IACxC,CAAC,CAAA;AAAA,EACH;AACA,EAAA,OAAO;AAAA,IACL,MAAM,GAAA,GAAM;AACV,MAAA,MAAM,OAAO,MAAM,IAAA,CAAK,KAAK,CAAA,CAAE,OAAO,GAAG,CAAA;AACzC,MAAA,OAAO,MAAA,CAAO,WAAA;AAAA,QACZ,IAAA,CAAK,GAAA,CAAI,CAAC,CAAA,KAAW;AAAA,UACnB,MAAA,CAAO,EAAE,GAAG,CAAA;AAAA,UACZ;AAAA,YACE,KAAA,EAAO,IAAA,CAAK,KAAA,CAAM,CAAA,CAAE,KAAK,CAAA;AAAA,YACzB,SAAA,EAAW,MAAA,CAAO,CAAA,CAAE,UAAU,CAAA;AAAA,YAC9B,WAAW,IAAI,IAAA,CAAK,CAAA,CAAE,UAAU,EAAE,WAAA;AAAY;AAChD,SACD;AAAA,OACH;AAAA,IACF,CAAA;AAAA,IACA,MAAM,KAAA,CAAM,OAAA,EAAS,EAAA,EAAI;AACvB,MAAA,MAAM,GAAA,uBAAU,IAAA,EAAK;AACrB,MAAA,MAAM,IAAA,CAAK,WAAA,CAAY,OAAM,EAAA,KAAM;AACjC,QAAA,KAAA,MAAW,CAAC,GAAA,EAAK,KAAK,KAAK,MAAA,CAAO,OAAA,CAAQ,OAAO,CAAA,EAAG;AAClD,UAAA,IAAI,UAAU,IAAA,EAAM;AAClB,YAAA,MAAM,EAAA,CAAG,KAAK,CAAA,CAAE,KAAA,CAAM,EAAE,GAAA,EAAK,EAAE,MAAA,EAAO;AAAA,UACxC,CAAA,MAAO;AACL,YAAA,MAAM,EAAA,CAAG,KAAK,CAAA,CACX,MAAA,CAAO;AAAA,cACN,GAAA;AAAA,cACA,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,KAAK,CAAA;AAAA,cAC3B,UAAA,EAAY,EAAA;AAAA,cACZ,UAAA,EAAY;AAAA,aACb,CAAA,CACA,UAAA,CAAW,KAAK,EAChB,KAAA,EAAM;AAAA,UACX;AAAA,QACF;AAAA,MACF,CAAC,CAAA;AAAA,IACH;AAAA,GACF;AACF;;;;"}
package/package.json CHANGED
@@ -1,6 +1,71 @@
1
1
  {
2
2
  "name": "@iswesolutions/plugin-monitoring-backend",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "Backend for @iswesolutions/plugin-monitoring: shared settings (cost rates, electricity tariff) for its Settings tab, editable with the monitoring.settings.update permission",
5
+ "main": "dist/index.cjs.js",
6
+ "types": "dist/index.d.ts",
7
+ "license": "Apache-2.0",
8
+ "publishConfig": {
9
+ "access": "public",
10
+ "main": "dist/index.cjs.js",
11
+ "types": "dist/index.d.ts"
12
+ },
13
+ "backstage": {
14
+ "role": "backend-plugin",
15
+ "pluginId": "monitoring",
16
+ "pluginPackages": [
17
+ "@iswesolutions/plugin-monitoring",
18
+ "@iswesolutions/plugin-monitoring-backend"
19
+ ],
20
+ "features": {
21
+ ".": "@backstage/BackendFeature"
22
+ }
23
+ },
24
+ "scripts": {
25
+ "start": "backstage-cli package start",
26
+ "build": "backstage-cli package build",
27
+ "lint": "backstage-cli package lint",
28
+ "test": "backstage-cli package test",
29
+ "clean": "backstage-cli package clean",
30
+ "prepack": "backstage-cli package prepack",
31
+ "postpack": "backstage-cli package postpack"
32
+ },
33
+ "dependencies": {
34
+ "@backstage/backend-plugin-api": "^1.9.1",
35
+ "@backstage/errors": "^1.3.1",
36
+ "@backstage/plugin-permission-common": "^0.9.9",
37
+ "express": "^4.17.1",
38
+ "express-promise-router": "^4.1.0"
39
+ },
40
+ "devDependencies": {
41
+ "@backstage/cli": "^0.36.2",
42
+ "@types/express": "^4.17.21",
43
+ "better-sqlite3": "^12.0.0",
44
+ "knex": "^3.1.0"
45
+ },
46
+ "files": [
47
+ "dist"
48
+ ],
49
+ "repository": {
50
+ "type": "git",
51
+ "url": "git+https://github.com/ISWE-Solutions/backstage-plugins.git",
52
+ "directory": "plugins/monitoring-backend"
53
+ },
54
+ "keywords": [
55
+ "backstage",
56
+ "backstage-plugin",
57
+ "monitoring",
58
+ "settings"
59
+ ],
60
+ "homepage": "https://github.com/ISWE-Solutions/backstage-plugins/tree/main/plugins/monitoring-backend#readme",
61
+ "bugs": {
62
+ "url": "https://github.com/ISWE-Solutions/backstage-plugins/issues"
63
+ },
64
+ "typesVersions": {
65
+ "*": {
66
+ "package.json": [
67
+ "package.json"
68
+ ]
69
+ }
70
+ }
71
+ }