@backstage/plugin-user-settings-backend 0.3.10 → 0.4.0-next.1

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/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # @backstage/plugin-user-settings-backend
2
2
 
3
+ ## 0.4.0-next.1
4
+
5
+ ### Minor Changes
6
+
7
+ - 104ca74: User-settings will now use DataLoader to batch consecutive calls into one API call to improve performance
8
+
9
+ ### Patch Changes
10
+
11
+ - 8148621: Moved `@backstage/backend-defaults` from `dependencies` to `devDependencies`.
12
+ - Updated dependencies
13
+ - @backstage/plugin-user-settings-common@0.1.0-next.0
14
+ - @backstage/backend-plugin-api@1.7.0-next.1
15
+ - @backstage/plugin-auth-node@0.6.13-next.1
16
+
17
+ ## 0.3.11-next.0
18
+
19
+ ### Patch Changes
20
+
21
+ - Updated dependencies
22
+ - @backstage/backend-plugin-api@1.7.0-next.0
23
+ - @backstage/backend-defaults@0.15.1-next.0
24
+ - @backstage/plugin-auth-node@0.6.12-next.0
25
+ - @backstage/errors@1.2.7
26
+ - @backstage/types@1.2.2
27
+ - @backstage/plugin-signals-node@0.1.28-next.0
28
+ - @backstage/plugin-user-settings-common@0.0.1
29
+
3
30
  ## 0.3.10
4
31
 
5
32
  ### Patch Changes
@@ -2,11 +2,17 @@
2
2
 
3
3
  var backendPluginApi = require('@backstage/backend-plugin-api');
4
4
  var errors = require('@backstage/errors');
5
+ var pLimit = require('p-limit');
6
+
7
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
+
9
+ var pLimit__default = /*#__PURE__*/_interopDefaultCompat(pLimit);
5
10
 
6
11
  const migrationsDir = backendPluginApi.resolvePackagePath(
7
12
  "@backstage/plugin-user-settings-backend",
8
13
  "migrations"
9
14
  );
15
+ const dbLimit = pLimit__default.default(10);
10
16
  class DatabaseUserSettingsStore {
11
17
  static async create(options) {
12
18
  const { database } = options;
@@ -39,6 +45,53 @@ class DatabaseUserSettingsStore {
39
45
  value: JSON.parse(rows[0].value)
40
46
  };
41
47
  }
48
+ async multiget(options) {
49
+ if (options.items.length === 0) {
50
+ return [];
51
+ }
52
+ const bucketMap = /* @__PURE__ */ new Map();
53
+ for (const item of options.items) {
54
+ let keys = bucketMap.get(item.bucket);
55
+ if (!keys) {
56
+ keys = /* @__PURE__ */ new Set();
57
+ bucketMap.set(item.bucket, keys);
58
+ }
59
+ keys.add(item.key);
60
+ }
61
+ const chunkKeys = (keys, size) => {
62
+ const chunks = [];
63
+ for (let i = 0; i < keys.length; i += size) {
64
+ chunks.push(keys.slice(i, i + size));
65
+ }
66
+ return chunks;
67
+ };
68
+ const resultsMap = /* @__PURE__ */ new Map();
69
+ await Promise.all(
70
+ Array.from(bucketMap.entries()).map(
71
+ ([bucket, keySet]) => dbLimit(async () => {
72
+ const keyMap = /* @__PURE__ */ new Map();
73
+ resultsMap.set(bucket, keyMap);
74
+ const keyChunks = chunkKeys(Array.from(keySet), 100);
75
+ for (const keys of keyChunks) {
76
+ const rows = await this.db("user_settings").where({
77
+ user_entity_ref: options.userEntityRef,
78
+ bucket
79
+ }).whereIn("key", keys).select(["bucket", "key", "value"]);
80
+ for (const row of rows) {
81
+ keyMap.set(row.key, JSON.parse(row.value));
82
+ }
83
+ }
84
+ })
85
+ )
86
+ );
87
+ return options.items.map(({ bucket, key }) => {
88
+ const value = resultsMap.get(bucket)?.get(key);
89
+ if (typeof value === "undefined") {
90
+ return null;
91
+ }
92
+ return { value };
93
+ });
94
+ }
42
95
  async set(options) {
43
96
  await this.db("user_settings").insert({
44
97
  user_entity_ref: options.userEntityRef,
@@ -1 +1 @@
1
- {"version":3,"file":"DatabaseUserSettingsStore.cjs.js","sources":["../../src/database/DatabaseUserSettingsStore.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n resolvePackagePath,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport { NotFoundError } from '@backstage/errors';\nimport { JsonValue } from '@backstage/types';\nimport { Knex } from 'knex';\nimport { UserSettingsStore, type UserSetting } from './UserSettingsStore';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-user-settings-backend',\n 'migrations',\n);\n\n/**\n * @public\n */\nexport type RawDbUserSettingsRow = {\n user_entity_ref: string;\n bucket: string;\n key: string;\n value: string;\n};\n\n/**\n * Store to manage the user settings.\n *\n * @public\n */\nexport class DatabaseUserSettingsStore implements UserSettingsStore {\n static async create(options: {\n database: DatabaseService;\n }): Promise<DatabaseUserSettingsStore> {\n const { database } = options;\n const client = await database.getClient();\n\n if (!database.migrations?.skip) {\n await client.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n return new DatabaseUserSettingsStore(client);\n }\n\n private readonly db: Knex;\n\n private constructor(db: Knex) {\n this.db = db;\n }\n\n async get(options: {\n userEntityRef: string;\n bucket: string;\n key: string;\n }): Promise<UserSetting> {\n const rows = await this.db<RawDbUserSettingsRow>('user_settings')\n .where({\n user_entity_ref: options.userEntityRef,\n bucket: options.bucket,\n key: options.key,\n })\n .select(['bucket', 'key', 'value']);\n\n if (!rows.length) {\n throw new NotFoundError(\n `Unable to find '${options.key}' in bucket '${options.bucket}'`,\n );\n }\n\n return {\n bucket: rows[0].bucket,\n key: rows[0].key,\n value: JSON.parse(rows[0].value),\n };\n }\n\n async set(options: {\n userEntityRef: string;\n bucket: string;\n key: string;\n value: JsonValue;\n }): Promise<void> {\n await this.db<RawDbUserSettingsRow>('user_settings')\n .insert({\n user_entity_ref: options.userEntityRef,\n bucket: options.bucket,\n key: options.key,\n value: JSON.stringify(options.value),\n })\n .onConflict(['user_entity_ref', 'bucket', 'key'])\n .merge(['value']);\n }\n\n async delete(options: {\n userEntityRef: string;\n bucket: string;\n key: string;\n }): Promise<void> {\n await this.db<RawDbUserSettingsRow>('user_settings')\n .where({\n user_entity_ref: options.userEntityRef,\n bucket: options.bucket,\n key: options.key,\n })\n .delete();\n }\n}\n"],"names":["resolvePackagePath","NotFoundError"],"mappings":";;;;;AAyBA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,EACpB,yCAAA;AAAA,EACA;AACF,CAAA;AAiBO,MAAM,yBAAA,CAAuD;AAAA,EAClE,aAAa,OAAO,OAAA,EAEmB;AACrC,IAAA,MAAM,EAAE,UAAS,GAAI,OAAA;AACrB,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AAExC,IAAA,IAAI,CAAC,QAAA,CAAS,UAAA,EAAY,IAAA,EAAM;AAC9B,MAAA,MAAM,MAAA,CAAO,QAAQ,MAAA,CAAO;AAAA,QAC1B,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,IAAI,0BAA0B,MAAM,CAAA;AAAA,EAC7C;AAAA,EAEiB,EAAA;AAAA,EAET,YAAY,EAAA,EAAU;AAC5B,IAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AAAA,EACZ;AAAA,EAEA,MAAM,IAAI,OAAA,EAIe;AACvB,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,EAC7D,KAAA,CAAM;AAAA,MACL,iBAAiB,OAAA,CAAQ,aAAA;AAAA,MACzB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAK,OAAA,CAAQ;AAAA,KACd,CAAA,CACA,MAAA,CAAO,CAAC,QAAA,EAAU,KAAA,EAAO,OAAO,CAAC,CAAA;AAEpC,IAAA,IAAI,CAAC,KAAK,MAAA,EAAQ;AAChB,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR,CAAA,gBAAA,EAAmB,OAAA,CAAQ,GAAG,CAAA,aAAA,EAAgB,QAAQ,MAAM,CAAA,CAAA;AAAA,OAC9D;AAAA,IACF;AAEA,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,IAAA,CAAK,CAAC,CAAA,CAAE,MAAA;AAAA,MAChB,GAAA,EAAK,IAAA,CAAK,CAAC,CAAA,CAAE,GAAA;AAAA,MACb,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,EAAE,KAAK;AAAA,KACjC;AAAA,EACF;AAAA,EAEA,MAAM,IAAI,OAAA,EAKQ;AAChB,IAAA,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,CAAA,CAChD,MAAA,CAAO;AAAA,MACN,iBAAiB,OAAA,CAAQ,aAAA;AAAA,MACzB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAK,OAAA,CAAQ,GAAA;AAAA,MACb,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,KAAK;AAAA,KACpC,CAAA,CACA,UAAA,CAAW,CAAC,iBAAA,EAAmB,QAAA,EAAU,KAAK,CAAC,CAAA,CAC/C,KAAA,CAAM,CAAC,OAAO,CAAC,CAAA;AAAA,EACpB;AAAA,EAEA,MAAM,OAAO,OAAA,EAIK;AAChB,IAAA,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,CAAA,CAChD,KAAA,CAAM;AAAA,MACL,iBAAiB,OAAA,CAAQ,aAAA;AAAA,MACzB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAK,OAAA,CAAQ;AAAA,KACd,EACA,MAAA,EAAO;AAAA,EACZ;AACF;;;;"}
1
+ {"version":3,"file":"DatabaseUserSettingsStore.cjs.js","sources":["../../src/database/DatabaseUserSettingsStore.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport {\n resolvePackagePath,\n DatabaseService,\n} from '@backstage/backend-plugin-api';\nimport { NotFoundError } from '@backstage/errors';\nimport { JsonValue } from '@backstage/types';\nimport { Knex } from 'knex';\nimport pLimit from 'p-limit';\nimport { UserSettingsStore, type UserSetting } from './UserSettingsStore';\n\nconst migrationsDir = resolvePackagePath(\n '@backstage/plugin-user-settings-backend',\n 'migrations',\n);\n\nconst dbLimit = pLimit(10);\n\n/**\n * @public\n */\nexport type RawDbUserSettingsRow = {\n user_entity_ref: string;\n bucket: string;\n key: string;\n value: string;\n};\n\n/**\n * Store to manage the user settings.\n *\n * @public\n */\nexport class DatabaseUserSettingsStore implements UserSettingsStore {\n static async create(options: {\n database: DatabaseService;\n }): Promise<DatabaseUserSettingsStore> {\n const { database } = options;\n const client = await database.getClient();\n\n if (!database.migrations?.skip) {\n await client.migrate.latest({\n directory: migrationsDir,\n });\n }\n\n return new DatabaseUserSettingsStore(client);\n }\n\n private readonly db: Knex;\n\n private constructor(db: Knex) {\n this.db = db;\n }\n\n async get(options: {\n userEntityRef: string;\n bucket: string;\n key: string;\n }): Promise<UserSetting> {\n const rows = await this.db<RawDbUserSettingsRow>('user_settings')\n .where({\n user_entity_ref: options.userEntityRef,\n bucket: options.bucket,\n key: options.key,\n })\n .select(['bucket', 'key', 'value']);\n\n if (!rows.length) {\n throw new NotFoundError(\n `Unable to find '${options.key}' in bucket '${options.bucket}'`,\n );\n }\n\n return {\n bucket: rows[0].bucket,\n key: rows[0].key,\n value: JSON.parse(rows[0].value),\n };\n }\n\n async multiget(options: {\n userEntityRef: string;\n items: Array<{ bucket: string; key: string }>;\n }): Promise<({ value: JsonValue } | null)[]> {\n if (options.items.length === 0) {\n return [];\n }\n\n // Split the items into a map of bucket -> keys\n const bucketMap = new Map<string, Set<string>>();\n for (const item of options.items) {\n let keys = bucketMap.get(item.bucket);\n if (!keys) {\n keys = new Set();\n bucketMap.set(item.bucket, keys);\n }\n keys.add(item.key);\n }\n\n // Chunks the keys per bucket to avoid hitting SQL parameter limits\n const chunkKeys = (keys: Array<string>, size: number): string[][] => {\n const chunks = [];\n for (let i = 0; i < keys.length; i += size) {\n chunks.push(keys.slice(i, i + size));\n }\n return chunks;\n };\n\n // Store the database content into a map of bucket -> {key -> value}\n const resultsMap = new Map<string, Map<string, JsonValue>>();\n\n await Promise.all(\n Array.from(bucketMap.entries()).map(([bucket, keySet]) =>\n dbLimit(async (): Promise<void> => {\n const keyMap = new Map<string, JsonValue>();\n resultsMap.set(bucket, keyMap);\n\n const keyChunks = chunkKeys(Array.from(keySet), 100);\n\n for (const keys of keyChunks) {\n const rows = await this.db<RawDbUserSettingsRow>('user_settings')\n .where({\n user_entity_ref: options.userEntityRef,\n bucket,\n })\n .whereIn('key', keys)\n .select(['bucket', 'key', 'value']);\n\n for (const row of rows) {\n keyMap.set(row.key, JSON.parse(row.value));\n }\n }\n }),\n ),\n );\n\n // For each exact bucket/key requested, return either the value or null if\n // not found\n return options.items.map(({ bucket, key }) => {\n const value = resultsMap.get(bucket)?.get(key);\n\n if (typeof value === 'undefined') {\n return null;\n }\n\n return { value };\n });\n }\n\n async set(options: {\n userEntityRef: string;\n bucket: string;\n key: string;\n value: JsonValue;\n }): Promise<void> {\n await this.db<RawDbUserSettingsRow>('user_settings')\n .insert({\n user_entity_ref: options.userEntityRef,\n bucket: options.bucket,\n key: options.key,\n value: JSON.stringify(options.value),\n })\n .onConflict(['user_entity_ref', 'bucket', 'key'])\n .merge(['value']);\n }\n\n async delete(options: {\n userEntityRef: string;\n bucket: string;\n key: string;\n }): Promise<void> {\n await this.db<RawDbUserSettingsRow>('user_settings')\n .where({\n user_entity_ref: options.userEntityRef,\n bucket: options.bucket,\n key: options.key,\n })\n .delete();\n }\n}\n"],"names":["resolvePackagePath","pLimit","NotFoundError"],"mappings":";;;;;;;;;;AA0BA,MAAM,aAAA,GAAgBA,mCAAA;AAAA,EACpB,yCAAA;AAAA,EACA;AACF,CAAA;AAEA,MAAM,OAAA,GAAUC,wBAAO,EAAE,CAAA;AAiBlB,MAAM,yBAAA,CAAuD;AAAA,EAClE,aAAa,OAAO,OAAA,EAEmB;AACrC,IAAA,MAAM,EAAE,UAAS,GAAI,OAAA;AACrB,IAAA,MAAM,MAAA,GAAS,MAAM,QAAA,CAAS,SAAA,EAAU;AAExC,IAAA,IAAI,CAAC,QAAA,CAAS,UAAA,EAAY,IAAA,EAAM;AAC9B,MAAA,MAAM,MAAA,CAAO,QAAQ,MAAA,CAAO;AAAA,QAC1B,SAAA,EAAW;AAAA,OACZ,CAAA;AAAA,IACH;AAEA,IAAA,OAAO,IAAI,0BAA0B,MAAM,CAAA;AAAA,EAC7C;AAAA,EAEiB,EAAA;AAAA,EAET,YAAY,EAAA,EAAU;AAC5B,IAAA,IAAA,CAAK,EAAA,GAAK,EAAA;AAAA,EACZ;AAAA,EAEA,MAAM,IAAI,OAAA,EAIe;AACvB,IAAA,MAAM,OAAO,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,EAC7D,KAAA,CAAM;AAAA,MACL,iBAAiB,OAAA,CAAQ,aAAA;AAAA,MACzB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAK,OAAA,CAAQ;AAAA,KACd,CAAA,CACA,MAAA,CAAO,CAAC,QAAA,EAAU,KAAA,EAAO,OAAO,CAAC,CAAA;AAEpC,IAAA,IAAI,CAAC,KAAK,MAAA,EAAQ;AAChB,MAAA,MAAM,IAAIC,oBAAA;AAAA,QACR,CAAA,gBAAA,EAAmB,OAAA,CAAQ,GAAG,CAAA,aAAA,EAAgB,QAAQ,MAAM,CAAA,CAAA;AAAA,OAC9D;AAAA,IACF;AAEA,IAAA,OAAO;AAAA,MACL,MAAA,EAAQ,IAAA,CAAK,CAAC,CAAA,CAAE,MAAA;AAAA,MAChB,GAAA,EAAK,IAAA,CAAK,CAAC,CAAA,CAAE,GAAA;AAAA,MACb,OAAO,IAAA,CAAK,KAAA,CAAM,IAAA,CAAK,CAAC,EAAE,KAAK;AAAA,KACjC;AAAA,EACF;AAAA,EAEA,MAAM,SAAS,OAAA,EAG8B;AAC3C,IAAA,IAAI,OAAA,CAAQ,KAAA,CAAM,MAAA,KAAW,CAAA,EAAG;AAC9B,MAAA,OAAO,EAAC;AAAA,IACV;AAGA,IAAA,MAAM,SAAA,uBAAgB,GAAA,EAAyB;AAC/C,IAAA,KAAA,MAAW,IAAA,IAAQ,QAAQ,KAAA,EAAO;AAChC,MAAA,IAAI,IAAA,GAAO,SAAA,CAAU,GAAA,CAAI,IAAA,CAAK,MAAM,CAAA;AACpC,MAAA,IAAI,CAAC,IAAA,EAAM;AACT,QAAA,IAAA,uBAAW,GAAA,EAAI;AACf,QAAA,SAAA,CAAU,GAAA,CAAI,IAAA,CAAK,MAAA,EAAQ,IAAI,CAAA;AAAA,MACjC;AACA,MAAA,IAAA,CAAK,GAAA,CAAI,KAAK,GAAG,CAAA;AAAA,IACnB;AAGA,IAAA,MAAM,SAAA,GAAY,CAAC,IAAA,EAAqB,IAAA,KAA6B;AACnE,MAAA,MAAM,SAAS,EAAC;AAChB,MAAA,KAAA,IAAS,IAAI,CAAA,EAAG,CAAA,GAAI,IAAA,CAAK,MAAA,EAAQ,KAAK,IAAA,EAAM;AAC1C,QAAA,MAAA,CAAO,KAAK,IAAA,CAAK,KAAA,CAAM,CAAA,EAAG,CAAA,GAAI,IAAI,CAAC,CAAA;AAAA,MACrC;AACA,MAAA,OAAO,MAAA;AAAA,IACT,CAAA;AAGA,IAAA,MAAM,UAAA,uBAAiB,GAAA,EAAoC;AAE3D,IAAA,MAAM,OAAA,CAAQ,GAAA;AAAA,MACZ,KAAA,CAAM,IAAA,CAAK,SAAA,CAAU,OAAA,EAAS,CAAA,CAAE,GAAA;AAAA,QAAI,CAAC,CAAC,MAAA,EAAQ,MAAM,CAAA,KAClD,QAAQ,YAA2B;AACjC,UAAA,MAAM,MAAA,uBAAa,GAAA,EAAuB;AAC1C,UAAA,UAAA,CAAW,GAAA,CAAI,QAAQ,MAAM,CAAA;AAE7B,UAAA,MAAM,YAAY,SAAA,CAAU,KAAA,CAAM,IAAA,CAAK,MAAM,GAAG,GAAG,CAAA;AAEnD,UAAA,KAAA,MAAW,QAAQ,SAAA,EAAW;AAC5B,YAAA,MAAM,OAAO,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,EAC7D,KAAA,CAAM;AAAA,cACL,iBAAiB,OAAA,CAAQ,aAAA;AAAA,cACzB;AAAA,aACD,CAAA,CACA,OAAA,CAAQ,KAAA,EAAO,IAAI,CAAA,CACnB,MAAA,CAAO,CAAC,QAAA,EAAU,KAAA,EAAO,OAAO,CAAC,CAAA;AAEpC,YAAA,KAAA,MAAW,OAAO,IAAA,EAAM;AACtB,cAAA,MAAA,CAAO,IAAI,GAAA,CAAI,GAAA,EAAK,KAAK,KAAA,CAAM,GAAA,CAAI,KAAK,CAAC,CAAA;AAAA,YAC3C;AAAA,UACF;AAAA,QACF,CAAC;AAAA;AACH,KACF;AAIA,IAAA,OAAO,QAAQ,KAAA,CAAM,GAAA,CAAI,CAAC,EAAE,MAAA,EAAQ,KAAI,KAAM;AAC5C,MAAA,MAAM,QAAQ,UAAA,CAAW,GAAA,CAAI,MAAM,CAAA,EAAG,IAAI,GAAG,CAAA;AAE7C,MAAA,IAAI,OAAO,UAAU,WAAA,EAAa;AAChC,QAAA,OAAO,IAAA;AAAA,MACT;AAEA,MAAA,OAAO,EAAE,KAAA,EAAM;AAAA,IACjB,CAAC,CAAA;AAAA,EACH;AAAA,EAEA,MAAM,IAAI,OAAA,EAKQ;AAChB,IAAA,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,CAAA,CAChD,MAAA,CAAO;AAAA,MACN,iBAAiB,OAAA,CAAQ,aAAA;AAAA,MACzB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAK,OAAA,CAAQ,GAAA;AAAA,MACb,KAAA,EAAO,IAAA,CAAK,SAAA,CAAU,OAAA,CAAQ,KAAK;AAAA,KACpC,CAAA,CACA,UAAA,CAAW,CAAC,iBAAA,EAAmB,QAAA,EAAU,KAAK,CAAC,CAAA,CAC/C,KAAA,CAAM,CAAC,OAAO,CAAC,CAAA;AAAA,EACpB;AAAA,EAEA,MAAM,OAAO,OAAA,EAIK;AAChB,IAAA,MAAM,IAAA,CAAK,EAAA,CAAyB,eAAe,CAAA,CAChD,KAAA,CAAM;AAAA,MACL,iBAAiB,OAAA,CAAQ,aAAA;AAAA,MACzB,QAAQ,OAAA,CAAQ,MAAA;AAAA,MAChB,KAAK,OAAA,CAAQ;AAAA,KACd,EACA,MAAA,EAAO;AAAA,EACZ;AACF;;;;"}
@@ -3,6 +3,7 @@
3
3
  var errors = require('@backstage/errors');
4
4
  var express = require('express');
5
5
  var Router = require('express-promise-router');
6
+ var zod = require('zod');
6
7
 
7
8
  function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
8
9
 
@@ -12,12 +13,24 @@ var Router__default = /*#__PURE__*/_interopDefaultCompat(Router);
12
13
  async function createRouter(options) {
13
14
  const router = Router__default.default();
14
15
  router.use(express__default.default.json());
16
+ const multiGetRequestSchema = zod.z.object({
17
+ items: zod.z.array(zod.z.object({ bucket: zod.z.string(), key: zod.z.string() }))
18
+ });
15
19
  const getUserEntityRef = async (req) => {
16
20
  const credentials = await options.httpAuth.credentials(req, {
17
21
  allow: ["user"]
18
22
  });
19
23
  return credentials.principal.userEntityRef;
20
24
  };
25
+ router.post("/multiget", async (req, res) => {
26
+ const userEntityRef = await getUserEntityRef(req);
27
+ const bucketsAndKeys = multiGetRequestSchema.parse(req.body).items;
28
+ const items = await options.userSettingsStore.multiget({
29
+ userEntityRef,
30
+ items: bucketsAndKeys
31
+ });
32
+ res.json({ items });
33
+ });
21
34
  router.get("/buckets/:bucket/keys/:key(*)", async (req, res) => {
22
35
  const userEntityRef = await getUserEntityRef(req);
23
36
  const { bucket, key } = req.params;
@@ -1 +1 @@
1
- {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InputError } from '@backstage/errors';\nimport express, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport { UserSettingsStore } from '../database/UserSettingsStore';\nimport { SignalsService } from '@backstage/plugin-signals-node';\nimport { UserSettingsSignal } from '@backstage/plugin-user-settings-common';\nimport { HttpAuthService } from '@backstage/backend-plugin-api';\n\nexport async function createRouter(options: {\n httpAuth: HttpAuthService;\n userSettingsStore: UserSettingsStore;\n signals: SignalsService;\n}): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n /**\n * Helper method to extract the userEntityRef from the request.\n */\n const getUserEntityRef = async (req: Request): Promise<string> => {\n const credentials = await options.httpAuth.credentials(req, {\n allow: ['user'],\n });\n return credentials.principal.userEntityRef;\n };\n\n // get a single value\n router.get('/buckets/:bucket/keys/:key(*)', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n const { bucket, key } = req.params;\n\n const setting = await options.userSettingsStore.get({\n userEntityRef,\n bucket,\n key,\n });\n\n res.json(setting);\n });\n\n // set a single value\n router.put('/buckets/:bucket/keys/:key(*)', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n const { bucket, key } = req.params;\n const { value } = req.body;\n\n if (value === undefined) {\n throw new InputError('Missing required field \"value\"');\n }\n\n await options.userSettingsStore.set({\n userEntityRef,\n bucket,\n key,\n value,\n });\n const setting = await options.userSettingsStore.get({\n userEntityRef,\n bucket,\n key,\n });\n\n if (options.signals) {\n await options.signals.publish<UserSettingsSignal>({\n recipients: { type: 'user', entityRef: userEntityRef },\n channel: `user-settings`,\n message: { type: 'key-changed', key },\n });\n }\n\n res.json(setting);\n });\n\n // delete a single value\n router.delete('/buckets/:bucket/keys/:key(*)', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n const { bucket, key } = req.params;\n\n await options.userSettingsStore.delete({ userEntityRef, bucket, key });\n if (options.signals) {\n await options.signals.publish<UserSettingsSignal>({\n recipients: { type: 'user', entityRef: userEntityRef },\n channel: 'user-settings',\n message: { type: 'key-deleted', key },\n });\n }\n\n res.status(204).end();\n });\n\n return router;\n}\n"],"names":["Router","express","InputError"],"mappings":"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"}
1
+ {"version":3,"file":"router.cjs.js","sources":["../../src/service/router.ts"],"sourcesContent":["/*\n * Copyright 2022 The Backstage Authors\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\nimport { InputError } from '@backstage/errors';\nimport express, { Request } from 'express';\nimport Router from 'express-promise-router';\nimport { z } from 'zod';\nimport { UserSettingsStore } from '../database/UserSettingsStore';\nimport { SignalsService } from '@backstage/plugin-signals-node';\nimport {\n MultiGetResponse,\n UserSettingsSignal,\n} from '@backstage/plugin-user-settings-common';\nimport { HttpAuthService } from '@backstage/backend-plugin-api';\n\nexport async function createRouter(options: {\n httpAuth: HttpAuthService;\n userSettingsStore: UserSettingsStore;\n signals: SignalsService;\n}): Promise<express.Router> {\n const router = Router();\n router.use(express.json());\n\n const multiGetRequestSchema = z.object({\n items: z.array(z.object({ bucket: z.string(), key: z.string() })),\n });\n\n /**\n * Helper method to extract the userEntityRef from the request.\n */\n const getUserEntityRef = async (req: Request): Promise<string> => {\n const credentials = await options.httpAuth.credentials(req, {\n allow: ['user'],\n });\n return credentials.principal.userEntityRef;\n };\n\n // get multiple values\n router.post('/multiget', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n\n const bucketsAndKeys = multiGetRequestSchema.parse(req.body).items;\n\n const items = await options.userSettingsStore.multiget({\n userEntityRef,\n items: bucketsAndKeys,\n });\n\n res.json({ items } satisfies MultiGetResponse);\n });\n\n // get a single value\n router.get('/buckets/:bucket/keys/:key(*)', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n const { bucket, key } = req.params;\n\n const setting = await options.userSettingsStore.get({\n userEntityRef,\n bucket,\n key,\n });\n\n res.json(setting);\n });\n\n // set a single value\n router.put('/buckets/:bucket/keys/:key(*)', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n const { bucket, key } = req.params;\n const { value } = req.body;\n\n if (value === undefined) {\n throw new InputError('Missing required field \"value\"');\n }\n\n await options.userSettingsStore.set({\n userEntityRef,\n bucket,\n key,\n value,\n });\n const setting = await options.userSettingsStore.get({\n userEntityRef,\n bucket,\n key,\n });\n\n if (options.signals) {\n await options.signals.publish<UserSettingsSignal>({\n recipients: { type: 'user', entityRef: userEntityRef },\n channel: `user-settings`,\n message: { type: 'key-changed', key },\n });\n }\n\n res.json(setting);\n });\n\n // delete a single value\n router.delete('/buckets/:bucket/keys/:key(*)', async (req, res) => {\n const userEntityRef = await getUserEntityRef(req);\n const { bucket, key } = req.params;\n\n await options.userSettingsStore.delete({ userEntityRef, bucket, key });\n if (options.signals) {\n await options.signals.publish<UserSettingsSignal>({\n recipients: { type: 'user', entityRef: userEntityRef },\n channel: 'user-settings',\n message: { type: 'key-deleted', key },\n });\n }\n\n res.status(204).end();\n });\n\n return router;\n}\n"],"names":["Router","express","z","InputError"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-user-settings-backend",
3
- "version": "0.3.10",
3
+ "version": "0.4.0-next.1",
4
4
  "description": "The Backstage backend plugin to manage user settings",
5
5
  "backstage": {
6
6
  "role": "backend-plugin",
@@ -56,21 +56,23 @@
56
56
  "test": "backstage-cli package test"
57
57
  },
58
58
  "dependencies": {
59
- "@backstage/backend-defaults": "^0.15.0",
60
- "@backstage/backend-plugin-api": "^1.6.1",
61
- "@backstage/errors": "^1.2.7",
62
- "@backstage/plugin-auth-node": "^0.6.11",
63
- "@backstage/plugin-signals-node": "^0.1.27",
64
- "@backstage/plugin-user-settings-common": "^0.0.1",
65
- "@backstage/types": "^1.2.2",
59
+ "@backstage/backend-plugin-api": "1.7.0-next.1",
60
+ "@backstage/errors": "1.2.7",
61
+ "@backstage/plugin-auth-node": "0.6.13-next.1",
62
+ "@backstage/plugin-signals-node": "0.1.28-next.0",
63
+ "@backstage/plugin-user-settings-common": "0.1.0-next.0",
64
+ "@backstage/types": "1.2.2",
65
+ "already": "^2.2.1",
66
66
  "express": "^4.22.0",
67
67
  "express-promise-router": "^4.1.0",
68
- "knex": "^3.0.0"
68
+ "knex": "^3.0.0",
69
+ "p-limit": "^3.1.0",
70
+ "zod": "^3.25.76"
69
71
  },
70
72
  "devDependencies": {
71
- "@backstage/backend-defaults": "^0.15.0",
72
- "@backstage/backend-test-utils": "^1.10.3",
73
- "@backstage/cli": "^0.35.2",
73
+ "@backstage/backend-defaults": "0.15.2-next.1",
74
+ "@backstage/backend-test-utils": "1.11.0-next.1",
75
+ "@backstage/cli": "0.35.4-next.2",
74
76
  "@types/express": "^4.17.6",
75
77
  "@types/supertest": "^2.0.8",
76
78
  "supertest": "^7.0.0"