@backstage/plugin-catalog-backend 3.4.0-next.2 → 3.4.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/CHANGELOG.md CHANGED
@@ -1,5 +1,60 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 3.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f1d29b4: Failures to connect catalog providers are now attributed to the module that provided the failing provider. This means that such failures will be reported as module startup failures rather than a failure to start the catalog plugin, and will therefore respect `onPluginModuleBootFailure` configuration instead.
8
+ - 34cc520: Implemented handling of events from the newly introduced alpha
9
+ `catalogScmEventsServiceRef` service, in the builtin entity providers. This
10
+ allows entities to get refreshed, and locations updated or removed, as a
11
+ response to incoming events. In its first iteration, only the GitHub module
12
+ implements such event handling however.
13
+
14
+ This is not yet enabled by default, but this fact may change in a future
15
+ release. To try it out, ensure that you have the latest catalog GitHub module
16
+ installed, and set the following in your app-config:
17
+
18
+ ```yaml
19
+ catalog:
20
+ scmEvents: true
21
+ ```
22
+
23
+ Or if you want to pick and choose from the various features:
24
+
25
+ ```yaml
26
+ catalog:
27
+ scmEvents:
28
+ # refresh (reprocess) upon events?
29
+ refresh: true
30
+ # automatically unregister locations based on events? (files deleted, repos archived, etc)
31
+ unregister: true
32
+ # automatically move locations based on events? (repo transferred, file renamed, etc)
33
+ move: true
34
+ ```
35
+
36
+ - b4e8249: Implemented the `POST /locations/by-query` endpoint which allows paginated, filtered location queries
37
+
38
+ ### Patch Changes
39
+
40
+ - cfd8103: Updated imports to use stable catalog extension points from `@backstage/plugin-catalog-node` instead of the deprecated alpha exports.
41
+ - 7455dae: Use node prefix on native imports
42
+ - 5e3ef57: Added `peerModules` metadata declaring recommended modules for cross-plugin integrations.
43
+ - 08a5813: Fixed O(n²) performance bottleneck in `buildEntitySearch` `traverse()` by replacing `Array.some()` linear scan with a `Set` for O(1) duplicate path key detection.
44
+ - 1e669cc: Migrate audit events reference docs to http://backstage.io/docs.
45
+ - 69d880e: Bump to latest zod to ensure it has the latest features
46
+ - Updated dependencies
47
+ - @backstage/integration@1.20.0
48
+ - @backstage/plugin-catalog-node@2.0.0
49
+ - @backstage/backend-openapi-utils@0.6.6
50
+ - @backstage/backend-plugin-api@1.7.0
51
+ - @backstage/catalog-client@1.13.0
52
+ - @backstage/filter-predicates@0.1.0
53
+ - @backstage/plugin-permission-common@0.9.6
54
+ - @backstage/plugin-permission-node@0.10.10
55
+ - @backstage/plugin-catalog-common@1.1.8
56
+ - @backstage/plugin-events-node@0.4.19
57
+
3
58
  ## 3.4.0-next.2
4
59
 
5
60
  ### Patch Changes
package/config.d.ts CHANGED
@@ -257,5 +257,48 @@ export interface Config {
257
257
  priority?: number;
258
258
  };
259
259
  };
260
+
261
+ /**
262
+ * Settings that control what to do when receiving messages from the SCM
263
+ * events service.
264
+ *
265
+ * @defaultValue false
266
+ * @remarks
267
+ *
268
+ * This is primarily meant to affect builtin providers in the catalog
269
+ * backend such as the location handler, but other providers and processors
270
+ * may also read this configuration.
271
+ *
272
+ * If set to false, disable all handling of SCM events.
273
+ *
274
+ * If set to true, enable all default handling of SCM events. Note that the
275
+ * set of default handling can change over time.
276
+ *
277
+ * You can also configure individual handlers one by one.
278
+ */
279
+ scmEvents?:
280
+ | boolean
281
+ | {
282
+ /**
283
+ * Trigger refreshes (reprocessing) of entities that are affected by an
284
+ * SCM event. This may include source control file content changes,
285
+ * repository status changes, etc.
286
+ *
287
+ * @defaultValue true
288
+ */
289
+ refresh?: boolean;
290
+ /**
291
+ * Unregister entities that are deleted as a result of an SCM event.
292
+ *
293
+ * @defaultValue true
294
+ */
295
+ unregister?: boolean;
296
+ /**
297
+ * Move entities that are moved as a result of an SCM event.
298
+ *
299
+ * @defaultValue true
300
+ */
301
+ move?: boolean;
302
+ };
260
303
  };
261
304
  }
@@ -5,12 +5,22 @@ var uuid = require('uuid');
5
5
  var util = require('../processing/util.cjs.js');
6
6
  var conversion = require('../util/conversion.cjs.js');
7
7
  var catalogModel = require('@backstage/catalog-model');
8
+ var lodash = require('lodash');
9
+ var parseGitUrl = require('git-url-parse');
10
+
11
+ function _interopDefaultCompat (e) { return e && typeof e === 'object' && 'default' in e ? e : { default: e }; }
12
+
13
+ var parseGitUrl__default = /*#__PURE__*/_interopDefaultCompat(parseGitUrl);
8
14
 
9
15
  class DefaultLocationStore {
10
16
  _connection;
11
17
  db;
12
- constructor(db) {
18
+ scmEvents;
19
+ scmEventHandlingConfig;
20
+ constructor(db, scmEvents, scmEventHandlingConfig) {
13
21
  this.db = db;
22
+ this.scmEvents = scmEvents;
23
+ this.scmEventHandlingConfig = scmEventHandlingConfig;
14
24
  }
15
25
  getProviderName() {
16
26
  return "DefaultLocationStore";
@@ -45,6 +55,36 @@ class DefaultLocationStore {
45
55
  async listLocations() {
46
56
  return await this.locations();
47
57
  }
58
+ async queryLocations(options) {
59
+ let itemsQuery = this.db("locations").whereNot(
60
+ "type",
61
+ "bootstrap"
62
+ );
63
+ if (options.query) {
64
+ itemsQuery = applyLocationFilterToQuery(
65
+ this.db.client.config.client,
66
+ itemsQuery,
67
+ options.query
68
+ );
69
+ }
70
+ const countQuery = itemsQuery.clone().count("*", { as: "count" });
71
+ itemsQuery = itemsQuery.orderBy("id", "asc");
72
+ if (options.afterId !== void 0) {
73
+ itemsQuery = itemsQuery.where("id", ">", options.afterId);
74
+ }
75
+ if (options.limit !== void 0) {
76
+ itemsQuery = itemsQuery.limit(options.limit);
77
+ }
78
+ const [items, [{ count }]] = await Promise.all([itemsQuery, countQuery]);
79
+ return {
80
+ items: items.map((item) => ({
81
+ id: item.id,
82
+ target: item.target,
83
+ type: item.type
84
+ })),
85
+ totalItems: Number(count)
86
+ };
87
+ }
48
88
  async getLocation(id) {
49
89
  const items = await this.db("locations").where({ id }).select();
50
90
  if (!items.length) {
@@ -112,6 +152,9 @@ class DefaultLocationStore {
112
152
  type: "full",
113
153
  entities
114
154
  });
155
+ if (this.scmEventHandlingConfig.unregister || this.scmEventHandlingConfig.move) {
156
+ this.scmEvents.subscribe({ onEvents: this.#onScmEvents.bind(this) });
157
+ }
115
158
  }
116
159
  async locations(dbOrTx = this.db) {
117
160
  const locations = await dbOrTx("locations").select();
@@ -121,6 +164,302 @@ class DefaultLocationStore {
121
164
  type: item.type
122
165
  }));
123
166
  }
167
+ // #region SCM event handling
168
+ async #onScmEvents(events) {
169
+ const exactLocationsToDelete = /* @__PURE__ */ new Set();
170
+ const locationPrefixesToDelete = /* @__PURE__ */ new Set();
171
+ const exactLocationsToCreate = /* @__PURE__ */ new Set();
172
+ const locationPrefixesToMove = /* @__PURE__ */ new Map();
173
+ for (const event of events) {
174
+ if (event.type === "location.deleted" && this.scmEventHandlingConfig.unregister) {
175
+ exactLocationsToDelete.add(event.url);
176
+ } else if (event.type === "location.moved" && this.scmEventHandlingConfig.move) {
177
+ exactLocationsToDelete.add(event.fromUrl);
178
+ exactLocationsToCreate.add(event.toUrl);
179
+ } else if (event.type === "repository.deleted" && this.scmEventHandlingConfig.unregister) {
180
+ locationPrefixesToDelete.add(event.url);
181
+ } else if (event.type === "repository.moved" && this.scmEventHandlingConfig.move) {
182
+ locationPrefixesToMove.set(event.fromUrl, event.toUrl);
183
+ }
184
+ }
185
+ if (exactLocationsToDelete.size > 0) {
186
+ await this.#deleteLocationsByExactUrl(exactLocationsToDelete);
187
+ }
188
+ if (locationPrefixesToDelete.size > 0) {
189
+ await this.#deleteLocationsByUrlPrefix(locationPrefixesToDelete);
190
+ }
191
+ if (exactLocationsToCreate.size > 0) {
192
+ await this.#createLocationsByExactUrl(exactLocationsToCreate);
193
+ }
194
+ if (locationPrefixesToMove.size > 0) {
195
+ await this.#moveLocationsByUrlPrefix(locationPrefixesToMove);
196
+ }
197
+ }
198
+ async #createLocationsByExactUrl(urls) {
199
+ let count = 0;
200
+ for (const batch of lodash.chunk(Array.from(urls), 100)) {
201
+ const existingUrls = await this.db("locations").where("type", "=", "url").whereIn("target", batch).select().then((rows) => new Set(rows.map((row) => row.target)));
202
+ const newLocations = batch.filter((url) => !existingUrls.has(url)).map((url) => ({ id: uuid.v4(), type: "url", target: url }));
203
+ if (newLocations.length) {
204
+ await this.db("locations").insert(newLocations);
205
+ await this.connection.applyMutation({
206
+ type: "delta",
207
+ added: newLocations.map((location) => {
208
+ const entity = conversion.locationSpecToLocationEntity({ location });
209
+ return { entity, locationKey: util.getEntityLocationRef(entity) };
210
+ }),
211
+ removed: []
212
+ });
213
+ count += newLocations.length;
214
+ }
215
+ }
216
+ return count;
217
+ }
218
+ async #deleteLocationsByExactUrl(urls) {
219
+ let count = 0;
220
+ for (const batch of lodash.chunk(Array.from(urls), 100)) {
221
+ const rows = await this.db("locations").where("type", "=", "url").whereIn("target", batch).select();
222
+ if (rows.length) {
223
+ await this.db("locations").whereIn(
224
+ "id",
225
+ rows.map((row) => row.id)
226
+ ).delete();
227
+ await this.connection.applyMutation({
228
+ type: "delta",
229
+ added: [],
230
+ removed: rows.map((row) => ({
231
+ entity: conversion.locationSpecToLocationEntity({ location: row })
232
+ }))
233
+ });
234
+ count += rows.length;
235
+ }
236
+ }
237
+ return count;
238
+ }
239
+ async #deleteLocationsByUrlPrefix(urls) {
240
+ const matches = await this.#findLocationsByPrefixOrExactMatch(urls);
241
+ if (matches.length) {
242
+ await this.#deleteLocations(matches.map((l) => l.row));
243
+ }
244
+ return matches.length;
245
+ }
246
+ async #moveLocationsByUrlPrefix(urlPrefixes) {
247
+ let count = 0;
248
+ for (const [fromPrefix, toPrefix] of urlPrefixes) {
249
+ if (fromPrefix === toPrefix) {
250
+ continue;
251
+ }
252
+ if (fromPrefix.match(/[?#]/) || toPrefix.match(/[?#]/)) {
253
+ continue;
254
+ }
255
+ const matches = await this.#findLocationsByPrefixOrExactMatch([
256
+ fromPrefix
257
+ ]);
258
+ if (matches.length) {
259
+ await this.#deleteLocations(matches.map((m) => m.row));
260
+ await this.#createLocationsByExactUrl(
261
+ matches.map((m) => {
262
+ const remainder = m.row.target.slice(fromPrefix.length).replace(/^\/+/, "");
263
+ if (!remainder) {
264
+ return toPrefix;
265
+ }
266
+ return `${toPrefix.replace(/\/+$/, "")}/${remainder}`;
267
+ })
268
+ );
269
+ count += matches.length;
270
+ }
271
+ }
272
+ return count;
273
+ }
274
+ async #deleteLocations(rows) {
275
+ for (const ids of lodash.chunk(
276
+ rows.map((l) => l.id),
277
+ 100
278
+ )) {
279
+ await this.db("locations").whereIn("id", ids).delete();
280
+ }
281
+ await this.connection.applyMutation({
282
+ type: "delta",
283
+ added: [],
284
+ removed: rows.map((l) => ({
285
+ entity: conversion.locationSpecToLocationEntity({ location: l })
286
+ }))
287
+ });
288
+ }
289
+ /**
290
+ * Given a "base" URL prefix, find all locations that are for paths at or
291
+ * below it.
292
+ *
293
+ * For example, given a base URL prefix of
294
+ * "https://github.com/backstage/backstage/blob/master/plugins", it will match
295
+ * locations inside the plugins directory, and nowhere else.
296
+ */
297
+ async #findLocationsByPrefixOrExactMatch(urls) {
298
+ const result = new Array();
299
+ for (const url of urls) {
300
+ let base;
301
+ try {
302
+ base = parseGitUrl__default.default(url);
303
+ } catch (error) {
304
+ throw new Error(`Invalid URL prefix, could not parse: ${url}`);
305
+ }
306
+ if (!base.owner || !base.name) {
307
+ throw new Error(
308
+ `Invalid URL prefix, missing owner or repository: ${url}`
309
+ );
310
+ }
311
+ const pathPrefix = base.filepath === "" || base.filepath.endsWith("/") ? base.filepath : `${base.filepath}/`;
312
+ const rows = await this.db("locations").where("type", "=", "url").where("target", "like", `%${base.owner}%`).where("target", "like", `%${base.name}%`).select();
313
+ result.push(
314
+ ...rows.flatMap((row) => {
315
+ try {
316
+ const candidate = parseGitUrl__default.default(row.target);
317
+ if (candidate.protocol === base.protocol && candidate.resource === base.resource && candidate.port === base.port && candidate.organization === base.organization && candidate.owner === base.owner && candidate.name === base.name && // If the base has no ref (for example didn't have the "/blob/master"
318
+ // part and therefore targeted an entire repository) then we match any
319
+ // ref below that
320
+ (!base.ref || candidate.ref === base.ref) && // Match both on exact equality and any subpath with a slash between
321
+ (candidate.filepath === base.filepath || candidate.filepath.startsWith(pathPrefix))) {
322
+ return [{ row, parsed: candidate }];
323
+ }
324
+ return [];
325
+ } catch {
326
+ return [];
327
+ }
328
+ })
329
+ );
330
+ }
331
+ return lodash.uniqBy(result, (entry) => entry.row.id);
332
+ }
333
+ // #endregion
334
+ }
335
+ function applyLocationFilterToQuery(clientType, inputQuery, query) {
336
+ let result = inputQuery;
337
+ if (!query || typeof query !== "object" || Array.isArray(query)) {
338
+ throw new errors.InputError("Invalid filter predicate, expected an object");
339
+ }
340
+ if ("$all" in query) {
341
+ if (query.$all.length === 0) {
342
+ return result.whereRaw("1 = 0");
343
+ }
344
+ return result.where((outer) => {
345
+ for (const subQuery of query.$all) {
346
+ outer.andWhere((inner) => {
347
+ applyLocationFilterToQuery(clientType, inner, subQuery);
348
+ });
349
+ }
350
+ });
351
+ }
352
+ if ("$any" in query) {
353
+ if (query.$any.length === 0) {
354
+ return result.whereRaw("1 = 0");
355
+ }
356
+ return result.where((outer) => {
357
+ for (const subQuery of query.$any) {
358
+ outer.orWhere((inner) => {
359
+ applyLocationFilterToQuery(clientType, inner, subQuery);
360
+ });
361
+ }
362
+ });
363
+ }
364
+ if ("$not" in query) {
365
+ return result.whereNot((inner) => {
366
+ applyLocationFilterToQuery(clientType, inner, query.$not);
367
+ });
368
+ }
369
+ const entries = Object.entries(query);
370
+ const keys = entries.map((e) => e[0]);
371
+ if (keys.some((k) => k.startsWith("$"))) {
372
+ throw new errors.InputError(
373
+ `Invalid filter predicate, unknown logic operator '${keys.join(", ")}'`
374
+ );
375
+ }
376
+ for (const [keyAnyCase, value] of entries) {
377
+ const key = keyAnyCase.toLocaleLowerCase("en-US");
378
+ if (!["id", "type", "target"].includes(key)) {
379
+ throw new errors.InputError(
380
+ `Invalid filter predicate, expected key to be 'id', 'type', or 'target', got '${keyAnyCase}'`
381
+ );
382
+ }
383
+ result = applyFilterValueToQuery(clientType, result, key, value);
384
+ }
385
+ return result;
386
+ }
387
+ function applyFilterValueToQuery(clientType, result, key, value) {
388
+ if (["string", "number", "boolean"].includes(typeof value)) {
389
+ if (clientType === "pg") {
390
+ return result.whereRaw(`UPPER(??::text) = UPPER(?::text)`, [key, value]);
391
+ }
392
+ if (clientType.includes("mysql")) {
393
+ return result.whereRaw(
394
+ `UPPER(CAST(?? AS CHAR)) = UPPER(CAST(? AS CHAR))`,
395
+ [key, value]
396
+ );
397
+ }
398
+ return result.whereRaw(`UPPER(??) = UPPER(?)`, [key, value]);
399
+ }
400
+ if (typeof value === "object") {
401
+ if (!value || Array.isArray(value)) {
402
+ throw new errors.InputError(
403
+ `Invalid filter predicate, got unknown matcher object '${JSON.stringify(
404
+ value
405
+ )}'`
406
+ );
407
+ }
408
+ if ("$exists" in value) {
409
+ return value.$exists ? result.whereNotNull(key) : result.whereNull(key);
410
+ }
411
+ if ("$in" in value) {
412
+ if (value.$in.length === 0) {
413
+ return result.whereRaw("1 = 0");
414
+ }
415
+ if (key === "id") {
416
+ return result.whereIn(key, value.$in);
417
+ }
418
+ if (clientType === "pg") {
419
+ const rhs2 = value.$in.map(() => "UPPER(?::text)").join(", ");
420
+ return result.whereRaw(`UPPER(??::text) IN (${rhs2})`, [
421
+ key,
422
+ ...value.$in
423
+ ]);
424
+ }
425
+ if (clientType.includes("mysql")) {
426
+ const rhs2 = value.$in.map(() => "UPPER(CAST(? AS CHAR))").join(", ");
427
+ return result.whereRaw(`UPPER(CAST(?? AS CHAR)) IN (${rhs2})`, [
428
+ key,
429
+ ...value.$in
430
+ ]);
431
+ }
432
+ const rhs = value.$in.map(() => "UPPER(?)").join(", ");
433
+ return result.whereRaw(`UPPER(??) IN (${rhs})`, [key, ...value.$in]);
434
+ }
435
+ if ("$hasPrefix" in value) {
436
+ const escaped = value.$hasPrefix.replace(/([\\%_])/g, "\\$1");
437
+ if (clientType === "pg") {
438
+ return result.whereRaw("?? ilike ? escape '\\'", [key, `${escaped}%`]);
439
+ }
440
+ if (clientType.includes("mysql")) {
441
+ return result.whereRaw("UPPER(??) like UPPER(?) escape '\\\\'", [
442
+ key,
443
+ `${escaped}%`
444
+ ]);
445
+ }
446
+ return result.whereRaw("UPPER(??) like UPPER(?) escape '\\'", [
447
+ key,
448
+ `${escaped}%`
449
+ ]);
450
+ }
451
+ if ("$contains" in value) {
452
+ return result.whereRaw("1 = 0");
453
+ }
454
+ throw new errors.InputError(
455
+ `Invalid filter predicate, got unknown matcher object '${JSON.stringify(
456
+ value
457
+ )}'`
458
+ );
459
+ }
460
+ throw new errors.InputError(
461
+ `Invalid filter predicate, expected value to be a primitive value or a matcher object, got '${typeof value}'`
462
+ );
124
463
  }
125
464
 
126
465
  exports.DefaultLocationStore = DefaultLocationStore;
@@ -1 +1 @@
1
- {"version":3,"file":"DefaultLocationStore.cjs.js","sources":["../../src/providers/DefaultLocationStore.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Location } from '@backstage/catalog-client';\nimport { ConflictError, NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport {\n DbLocationsRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../database/tables';\nimport { getEntityLocationRef } from '../processing/util';\nimport {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\nimport { locationSpecToLocationEntity } from '../util/conversion';\nimport { LocationInput, LocationStore } from '../service/types';\nimport {\n ANNOTATION_ORIGIN_LOCATION,\n CompoundEntityRef,\n parseLocationRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\n\nexport class DefaultLocationStore implements LocationStore, EntityProvider {\n private _connection: EntityProviderConnection | undefined;\n private readonly db: Knex;\n\n constructor(db: Knex) {\n this.db = db;\n }\n\n getProviderName(): string {\n return 'DefaultLocationStore';\n }\n\n async createLocation(input: LocationInput): Promise<Location> {\n const location = await this.db.transaction(async tx => {\n // Attempt to find a previous location matching the input\n const previousLocations = await this.locations(tx);\n // TODO: when location id's are a compilation of input target we can remove this full\n // lookup of locations first and just grab the by that instead.\n const previousLocation = previousLocations.some(\n l => input.type === l.type && input.target === l.target,\n );\n if (previousLocation) {\n throw new ConflictError(\n `Location ${input.type}:${input.target} already exists`,\n );\n }\n\n const inner: DbLocationsRow = {\n id: uuid(),\n type: input.type,\n target: input.target,\n };\n\n await tx<DbLocationsRow>('locations').insert(inner);\n\n return inner;\n });\n const entity = locationSpecToLocationEntity({ location });\n await this.connection.applyMutation({\n type: 'delta',\n added: [{ entity, locationKey: getEntityLocationRef(entity) }],\n removed: [],\n });\n\n return location;\n }\n\n async listLocations(): Promise<Location[]> {\n return await this.locations();\n }\n\n async getLocation(id: string): Promise<Location> {\n const items = await this.db<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!items.length) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n return items[0];\n }\n\n async deleteLocation(id: string): Promise<void> {\n if (!this.connection) {\n throw new Error('location store is not initialized');\n }\n\n const deleted = await this.db.transaction(async tx => {\n const [location] = await tx<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!location) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n\n await tx<DbLocationsRow>('locations').where({ id }).del();\n return location;\n });\n const entity = locationSpecToLocationEntity({ location: deleted });\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: [{ entity, locationKey: getEntityLocationRef(entity) }],\n });\n }\n\n async getLocationByEntity(entityRef: CompoundEntityRef): Promise<Location> {\n const entityRefString = stringifyEntityRef(entityRef);\n\n const [entityRow] = await this.db<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRefString })\n .select('entity_id')\n .limit(1);\n if (!entityRow) {\n throw new NotFoundError(`found no entity for ref ${entityRefString}`);\n }\n\n const [searchRow] = await this.db<DbSearchRow>('search')\n .where({\n entity_id: entityRow.entity_id,\n key: `metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`,\n })\n .select('original_value')\n .limit(1);\n if (!searchRow?.original_value) {\n throw new NotFoundError(\n `found no origin annotation for ref ${entityRefString}`,\n );\n }\n\n const { type, target } = parseLocationRef(searchRow.original_value);\n const [locationRow] = await this.db<DbLocationsRow>('locations')\n .where({ type, target })\n .select()\n .limit(1);\n\n if (!locationRow) {\n throw new NotFoundError(\n `Found no location with type ${type} and target ${target}`,\n );\n }\n\n return locationRow;\n }\n\n private get connection(): EntityProviderConnection {\n if (!this._connection) {\n throw new Error('location store is not initialized');\n }\n\n return this._connection;\n }\n\n async connect(connection: EntityProviderConnection): Promise<void> {\n this._connection = connection;\n\n const locations = await this.locations();\n\n const entities = locations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n });\n\n await this.connection.applyMutation({\n type: 'full',\n entities,\n });\n }\n\n private async locations(dbOrTx: Knex.Transaction | Knex = this.db) {\n const locations = await dbOrTx<DbLocationsRow>('locations').select();\n return (\n locations\n // TODO(blam): We should create a mutation to remove this location for everyone\n // eventually when it's all done and dusted\n .filter(({ type }) => type !== 'bootstrap')\n .map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n }))\n );\n }\n}\n"],"names":["ConflictError","uuid","locationSpecToLocationEntity","getEntityLocationRef","NotFoundError","stringifyEntityRef","ANNOTATION_ORIGIN_LOCATION","parseLocationRef"],"mappings":"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"}
1
+ {"version":3,"file":"DefaultLocationStore.cjs.js","sources":["../../src/providers/DefaultLocationStore.ts"],"sourcesContent":["/*\n * Copyright 2021 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 { Location } from '@backstage/catalog-client';\nimport { ConflictError, InputError, NotFoundError } from '@backstage/errors';\nimport { Knex } from 'knex';\nimport { v4 as uuid } from 'uuid';\nimport {\n DbLocationsRow,\n DbRefreshStateRow,\n DbSearchRow,\n} from '../database/tables';\nimport { getEntityLocationRef } from '../processing/util';\nimport {\n EntityProvider,\n EntityProviderConnection,\n} from '@backstage/plugin-catalog-node';\nimport { locationSpecToLocationEntity } from '../util/conversion';\nimport { LocationInput, LocationStore } from '../service/types';\nimport {\n ANNOTATION_ORIGIN_LOCATION,\n CompoundEntityRef,\n parseLocationRef,\n stringifyEntityRef,\n} from '@backstage/catalog-model';\nimport {\n CatalogScmEvent,\n CatalogScmEventsService,\n} from '@backstage/plugin-catalog-node/alpha';\nimport { chunk, uniqBy } from 'lodash';\nimport parseGitUrl, { type GitUrl } from 'git-url-parse';\nimport { ScmEventHandlingConfig } from '../util/readScmEventHandlingConfig';\nimport {\n FilterPredicate,\n FilterPredicateValue,\n} from '@backstage/filter-predicates';\n\nexport class DefaultLocationStore implements LocationStore, EntityProvider {\n private _connection: EntityProviderConnection | undefined;\n private readonly db: Knex;\n private readonly scmEvents: CatalogScmEventsService;\n private readonly scmEventHandlingConfig: ScmEventHandlingConfig;\n\n constructor(\n db: Knex,\n scmEvents: CatalogScmEventsService,\n scmEventHandlingConfig: ScmEventHandlingConfig,\n ) {\n this.db = db;\n this.scmEvents = scmEvents;\n this.scmEventHandlingConfig = scmEventHandlingConfig;\n }\n\n getProviderName(): string {\n return 'DefaultLocationStore';\n }\n\n async createLocation(input: LocationInput): Promise<Location> {\n const location = await this.db.transaction(async tx => {\n // Attempt to find a previous location matching the input\n const previousLocations = await this.locations(tx);\n // TODO: when location id's are a compilation of input target we can remove this full\n // lookup of locations first and just grab the by that instead.\n const previousLocation = previousLocations.some(\n l => input.type === l.type && input.target === l.target,\n );\n if (previousLocation) {\n throw new ConflictError(\n `Location ${input.type}:${input.target} already exists`,\n );\n }\n\n const inner: DbLocationsRow = {\n id: uuid(),\n type: input.type,\n target: input.target,\n };\n\n await tx<DbLocationsRow>('locations').insert(inner);\n\n return inner;\n });\n const entity = locationSpecToLocationEntity({ location });\n await this.connection.applyMutation({\n type: 'delta',\n added: [{ entity, locationKey: getEntityLocationRef(entity) }],\n removed: [],\n });\n\n return location;\n }\n\n async listLocations(): Promise<Location[]> {\n return await this.locations();\n }\n\n async queryLocations(options: {\n limit: number;\n afterId?: string;\n query?: FilterPredicate;\n }): Promise<{ items: Location[]; totalItems: number }> {\n let itemsQuery = this.db<DbLocationsRow>('locations').whereNot(\n 'type',\n 'bootstrap',\n );\n\n if (options.query) {\n itemsQuery = applyLocationFilterToQuery(\n this.db.client.config.client,\n itemsQuery,\n options.query,\n );\n }\n\n const countQuery = itemsQuery.clone().count('*', { as: 'count' });\n\n itemsQuery = itemsQuery.orderBy('id', 'asc');\n if (options.afterId !== undefined) {\n itemsQuery = itemsQuery.where('id', '>', options.afterId);\n }\n if (options.limit !== undefined) {\n itemsQuery = itemsQuery.limit(options.limit);\n }\n\n const [items, [{ count }]] = await Promise.all([itemsQuery, countQuery]);\n\n return {\n items: items.map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n })),\n totalItems: Number(count),\n };\n }\n\n async getLocation(id: string): Promise<Location> {\n const items = await this.db<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!items.length) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n return items[0];\n }\n\n async deleteLocation(id: string): Promise<void> {\n if (!this.connection) {\n throw new Error('location store is not initialized');\n }\n\n const deleted = await this.db.transaction(async tx => {\n const [location] = await tx<DbLocationsRow>('locations')\n .where({ id })\n .select();\n\n if (!location) {\n throw new NotFoundError(`Found no location with ID ${id}`);\n }\n\n await tx<DbLocationsRow>('locations').where({ id }).del();\n return location;\n });\n const entity = locationSpecToLocationEntity({ location: deleted });\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: [{ entity, locationKey: getEntityLocationRef(entity) }],\n });\n }\n\n async getLocationByEntity(entityRef: CompoundEntityRef): Promise<Location> {\n const entityRefString = stringifyEntityRef(entityRef);\n\n const [entityRow] = await this.db<DbRefreshStateRow>('refresh_state')\n .where({ entity_ref: entityRefString })\n .select('entity_id')\n .limit(1);\n if (!entityRow) {\n throw new NotFoundError(`found no entity for ref ${entityRefString}`);\n }\n\n const [searchRow] = await this.db<DbSearchRow>('search')\n .where({\n entity_id: entityRow.entity_id,\n key: `metadata.annotations.${ANNOTATION_ORIGIN_LOCATION}`,\n })\n .select('original_value')\n .limit(1);\n if (!searchRow?.original_value) {\n throw new NotFoundError(\n `found no origin annotation for ref ${entityRefString}`,\n );\n }\n\n const { type, target } = parseLocationRef(searchRow.original_value);\n const [locationRow] = await this.db<DbLocationsRow>('locations')\n .where({ type, target })\n .select()\n .limit(1);\n\n if (!locationRow) {\n throw new NotFoundError(\n `Found no location with type ${type} and target ${target}`,\n );\n }\n\n return locationRow;\n }\n\n private get connection(): EntityProviderConnection {\n if (!this._connection) {\n throw new Error('location store is not initialized');\n }\n\n return this._connection;\n }\n\n async connect(connection: EntityProviderConnection): Promise<void> {\n this._connection = connection;\n\n const locations = await this.locations();\n\n const entities = locations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n });\n\n await this.connection.applyMutation({\n type: 'full',\n entities,\n });\n\n if (\n this.scmEventHandlingConfig.unregister ||\n this.scmEventHandlingConfig.move\n ) {\n this.scmEvents.subscribe({ onEvents: this.#onScmEvents.bind(this) });\n }\n }\n\n private async locations(dbOrTx: Knex.Transaction | Knex = this.db) {\n const locations = await dbOrTx<DbLocationsRow>('locations').select();\n return (\n locations\n // TODO(blam): We should create a mutation to remove this location for everyone\n // eventually when it's all done and dusted\n .filter(({ type }) => type !== 'bootstrap')\n .map(item => ({\n id: item.id,\n target: item.target,\n type: item.type,\n }))\n );\n }\n\n // #region SCM event handling\n\n async #onScmEvents(events: CatalogScmEvent[]): Promise<void> {\n const exactLocationsToDelete = new Set<string>();\n const locationPrefixesToDelete = new Set<string>();\n const exactLocationsToCreate = new Set<string>();\n const locationPrefixesToMove = new Map<string, string>();\n\n for (const event of events) {\n if (\n event.type === 'location.deleted' &&\n this.scmEventHandlingConfig.unregister\n ) {\n exactLocationsToDelete.add(event.url);\n } else if (\n event.type === 'location.moved' &&\n this.scmEventHandlingConfig.move\n ) {\n // Since Location entities are named after their target URL, these\n // unfortunately have to be translated into deletion and creation\n exactLocationsToDelete.add(event.fromUrl);\n exactLocationsToCreate.add(event.toUrl);\n } else if (\n event.type === 'repository.deleted' &&\n this.scmEventHandlingConfig.unregister\n ) {\n locationPrefixesToDelete.add(event.url);\n } else if (\n event.type === 'repository.moved' &&\n this.scmEventHandlingConfig.move\n ) {\n // These also have to be handled with deletions and creations\n locationPrefixesToMove.set(event.fromUrl, event.toUrl);\n }\n }\n\n if (exactLocationsToDelete.size > 0) {\n await this.#deleteLocationsByExactUrl(exactLocationsToDelete);\n }\n if (locationPrefixesToDelete.size > 0) {\n await this.#deleteLocationsByUrlPrefix(locationPrefixesToDelete);\n }\n if (exactLocationsToCreate.size > 0) {\n await this.#createLocationsByExactUrl(exactLocationsToCreate);\n }\n if (locationPrefixesToMove.size > 0) {\n await this.#moveLocationsByUrlPrefix(locationPrefixesToMove);\n }\n }\n\n async #createLocationsByExactUrl(urls: Iterable<string>): Promise<number> {\n let count = 0;\n\n for (const batch of chunk(Array.from(urls), 100)) {\n const existingUrls = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n .whereIn('target', batch)\n .select()\n .then(rows => new Set(rows.map(row => row.target)));\n\n const newLocations = batch\n .filter(url => !existingUrls.has(url))\n .map(url => ({ id: uuid(), type: 'url', target: url }));\n\n if (newLocations.length) {\n await this.db<DbLocationsRow>('locations').insert(newLocations);\n\n await this.connection.applyMutation({\n type: 'delta',\n added: newLocations.map(location => {\n const entity = locationSpecToLocationEntity({ location });\n return { entity, locationKey: getEntityLocationRef(entity) };\n }),\n removed: [],\n });\n\n count += newLocations.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocationsByExactUrl(urls: Iterable<string>): Promise<number> {\n let count = 0;\n\n for (const batch of chunk(Array.from(urls), 100)) {\n const rows = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n .whereIn('target', batch)\n .select();\n\n if (rows.length) {\n await this.db<DbLocationsRow>('locations')\n .whereIn(\n 'id',\n rows.map(row => row.id),\n )\n .delete();\n\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: rows.map(row => ({\n entity: locationSpecToLocationEntity({ location: row }),\n })),\n });\n\n count += rows.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocationsByUrlPrefix(urls: Iterable<string>): Promise<number> {\n const matches = await this.#findLocationsByPrefixOrExactMatch(urls);\n if (matches.length) {\n await this.#deleteLocations(matches.map(l => l.row));\n }\n\n return matches.length;\n }\n\n async #moveLocationsByUrlPrefix(\n urlPrefixes: Map<string, string>,\n ): Promise<number> {\n let count = 0;\n\n for (const [fromPrefix, toPrefix] of urlPrefixes) {\n if (fromPrefix === toPrefix) {\n continue;\n }\n\n if (fromPrefix.match(/[?#]/) || toPrefix.match(/[?#]/)) {\n // TODO(freben): We can't yet support complex URL locations where e.g.\n // the path can be anywhere in the URL including in the query or hash\n // part. The code below currently assumes that we can use simple\n // substring operations.\n continue;\n }\n\n const matches = await this.#findLocationsByPrefixOrExactMatch([\n fromPrefix,\n ]);\n if (matches.length) {\n await this.#deleteLocations(matches.map(m => m.row));\n\n await this.#createLocationsByExactUrl(\n matches.map(m => {\n const remainder = m.row.target\n .slice(fromPrefix.length)\n .replace(/^\\/+/, '');\n if (!remainder) {\n return toPrefix;\n }\n return `${toPrefix.replace(/\\/+$/, '')}/${remainder}`;\n }),\n );\n\n count += matches.length;\n }\n }\n\n return count;\n }\n\n async #deleteLocations(rows: DbLocationsRow[]): Promise<void> {\n // Delete the location table entries (in chunks so as not to overload the\n // knex query builder)\n for (const ids of chunk(\n rows.map(l => l.id),\n 100,\n )) {\n await this.db<DbLocationsRow>('locations').whereIn('id', ids).delete();\n }\n\n // Delete the corresponding Location kind entities (this is efficiently\n // chunked internally in the catalog)\n await this.connection.applyMutation({\n type: 'delta',\n added: [],\n removed: rows.map(l => ({\n entity: locationSpecToLocationEntity({ location: l }),\n })),\n });\n }\n\n /**\n * Given a \"base\" URL prefix, find all locations that are for paths at or\n * below it.\n *\n * For example, given a base URL prefix of\n * \"https://github.com/backstage/backstage/blob/master/plugins\", it will match\n * locations inside the plugins directory, and nowhere else.\n */\n async #findLocationsByPrefixOrExactMatch(\n urls: Iterable<string>,\n ): Promise<Array<{ row: DbLocationsRow; parsed: GitUrl }>> {\n const result = new Array<{ row: DbLocationsRow; parsed: GitUrl }>();\n\n for (const url of urls) {\n let base: GitUrl;\n try {\n base = parseGitUrl(url);\n } catch (error) {\n throw new Error(`Invalid URL prefix, could not parse: ${url}`);\n }\n\n if (!base.owner || !base.name) {\n throw new Error(\n `Invalid URL prefix, missing owner or repository: ${url}`,\n );\n }\n\n const pathPrefix =\n base.filepath === '' || base.filepath.endsWith('/')\n ? base.filepath\n : `${base.filepath}/`;\n\n const rows = await this.db<DbLocationsRow>('locations')\n .where('type', '=', 'url')\n // Initial rough pruning to not have to go through them all\n .where('target', 'like', `%${base.owner}%`)\n .where('target', 'like', `%${base.name}%`)\n .select();\n\n result.push(\n ...rows.flatMap(row => {\n try {\n // We do this pretty explicit set of checks because we want to support\n // providers that have a URL format where the path isn't necessarily at\n // the end of the URL string (e.g. in the query part). Some of these may\n // be empty strings etc, but that's fine as long as they parse to the\n // same thing as above.\n const candidate = parseGitUrl(row.target);\n\n if (\n candidate.protocol === base.protocol &&\n candidate.resource === base.resource &&\n candidate.port === base.port &&\n candidate.organization === base.organization &&\n candidate.owner === base.owner &&\n candidate.name === base.name &&\n // If the base has no ref (for example didn't have the \"/blob/master\"\n // part and therefore targeted an entire repository) then we match any\n // ref below that\n (!base.ref || candidate.ref === base.ref) &&\n // Match both on exact equality and any subpath with a slash between\n (candidate.filepath === base.filepath ||\n candidate.filepath.startsWith(pathPrefix))\n ) {\n return [{ row, parsed: candidate }];\n }\n return [];\n } catch {\n return [];\n }\n }),\n );\n }\n\n return uniqBy(result, entry => entry.row.id);\n }\n\n // #endregion\n}\n\n/**\n * Recursively builds up the SQL expression corresponding to the given filter\n * predicate.\n *\n * @remarks\n *\n * Design note: The code prefers to let the SQL engine achieve case\n * insensitivity. We could attempt to use `.toUpperCase` etc on the client\n * side, but that would only work for the values being passed in, not the column\n * side of the expression. If we let the database perform UPPER on both, we know\n * that they will always be locale consistent etc as well.\n *\n * This does come at a runtime cost. However, the data set is typically rather\n * small in the grand scheme of things, and we can add the proper indices in the\n * future if needed. At this point I considered it not worth the effort.\n */\nfunction applyLocationFilterToQuery(\n clientType: string,\n inputQuery: Knex.QueryBuilder,\n query: FilterPredicate,\n): Knex.QueryBuilder {\n let result = inputQuery;\n\n if (!query || typeof query !== 'object' || Array.isArray(query)) {\n throw new InputError('Invalid filter predicate, expected an object');\n }\n\n if ('$all' in query) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (query.$all.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n return result.where(outer => {\n for (const subQuery of query.$all) {\n outer.andWhere(inner => {\n applyLocationFilterToQuery(clientType, inner, subQuery);\n });\n }\n });\n }\n\n if ('$any' in query) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (query.$any.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n return result.where(outer => {\n for (const subQuery of query.$any) {\n outer.orWhere(inner => {\n applyLocationFilterToQuery(clientType, inner, subQuery);\n });\n }\n });\n }\n\n if ('$not' in query) {\n return result.whereNot(inner => {\n applyLocationFilterToQuery(clientType, inner, query.$not);\n });\n }\n\n const entries = Object.entries(query);\n const keys = entries.map(e => e[0]);\n if (keys.some(k => k.startsWith('$'))) {\n throw new InputError(\n `Invalid filter predicate, unknown logic operator '${keys.join(', ')}'`,\n );\n }\n\n for (const [keyAnyCase, value] of entries) {\n const key = keyAnyCase.toLocaleLowerCase('en-US');\n if (!['id', 'type', 'target'].includes(key)) {\n throw new InputError(\n `Invalid filter predicate, expected key to be 'id', 'type', or 'target', got '${keyAnyCase}'`,\n );\n }\n\n result = applyFilterValueToQuery(clientType, result, key, value);\n }\n\n return result;\n}\n\nfunction applyFilterValueToQuery(\n clientType: string,\n result: Knex.QueryBuilder,\n key: string,\n value: FilterPredicateValue,\n): Knex.QueryBuilder {\n // Is it a primitive value?\n if (['string', 'number', 'boolean'].includes(typeof value)) {\n if (clientType === 'pg') {\n return result.whereRaw(`UPPER(??::text) = UPPER(?::text)`, [key, value]);\n }\n\n if (clientType.includes('mysql')) {\n return result.whereRaw(\n `UPPER(CAST(?? AS CHAR)) = UPPER(CAST(? AS CHAR))`,\n [key, value],\n );\n }\n\n return result.whereRaw(`UPPER(??) = UPPER(?)`, [key, value]);\n }\n\n // Is it a matcher object?\n if (typeof value === 'object') {\n if (!value || Array.isArray(value)) {\n throw new InputError(\n `Invalid filter predicate, got unknown matcher object '${JSON.stringify(\n value,\n )}'`,\n );\n }\n\n // Technically existence checks do not make much sense in the context of\n // this table at the time of writing (values are always present), but\n // there's nothing gained by prohibiting it.\n if ('$exists' in value) {\n return value.$exists ? result.whereNotNull(key) : result.whereNull(key);\n }\n\n if ('$in' in value) {\n // Explicitly handle the empty case to avoid malformed SQL\n if (value.$in.length === 0) {\n return result.whereRaw('1 = 0');\n }\n\n // The id is matched with plain equality; it's of UUID type and case\n // insensitivity does not apply.\n if (key === 'id') {\n return result.whereIn(key, value.$in);\n }\n\n if (clientType === 'pg') {\n const rhs = value.$in.map(() => 'UPPER(?::text)').join(', ');\n return result.whereRaw(`UPPER(??::text) IN (${rhs})`, [\n key,\n ...value.$in,\n ]);\n }\n\n if (clientType.includes('mysql')) {\n const rhs = value.$in.map(() => 'UPPER(CAST(? AS CHAR))').join(', ');\n return result.whereRaw(`UPPER(CAST(?? AS CHAR)) IN (${rhs})`, [\n key,\n ...value.$in,\n ]);\n }\n\n const rhs = value.$in.map(() => 'UPPER(?)').join(', ');\n return result.whereRaw(`UPPER(??) IN (${rhs})`, [key, ...value.$in]);\n }\n\n if ('$hasPrefix' in value) {\n const escaped = value.$hasPrefix.replace(/([\\\\%_])/g, '\\\\$1');\n\n if (clientType === 'pg') {\n return result.whereRaw(\"?? ilike ? escape '\\\\'\", [key, `${escaped}%`]);\n }\n\n if (clientType.includes('mysql')) {\n return result.whereRaw(\"UPPER(??) like UPPER(?) escape '\\\\\\\\'\", [\n key,\n `${escaped}%`,\n ]);\n }\n\n return result.whereRaw(\"UPPER(??) like UPPER(?) escape '\\\\'\", [\n key,\n `${escaped}%`,\n ]);\n }\n\n // There are no array shaped values for location queries, so we just always\n // fail here\n if ('$contains' in value) {\n return result.whereRaw('1 = 0');\n }\n\n throw new InputError(\n `Invalid filter predicate, got unknown matcher object '${JSON.stringify(\n value,\n )}'`,\n );\n }\n\n throw new InputError(\n `Invalid filter predicate, expected value to be a primitive value or a matcher object, got '${typeof value}'`,\n );\n}\n"],"names":["ConflictError","uuid","locationSpecToLocationEntity","getEntityLocationRef","NotFoundError","stringifyEntityRef","ANNOTATION_ORIGIN_LOCATION","parseLocationRef","chunk","parseGitUrl","uniqBy","InputError","rhs"],"mappings":"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"}