@backstage/plugin-catalog-backend 1.6.0-next.3 → 1.7.0-next.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,97 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.7.0-next.0
4
+
5
+ ### Minor Changes
6
+
7
+ - f75bf76330: Implemented server side ordering in the entities endpoint
8
+
9
+ ### Patch Changes
10
+
11
+ - d136793ff0: Fixed an issue where internal references in the catalog would stick around for longer than expected, causing entities to not be deleted or orphaned as expected.
12
+ - Updated dependencies
13
+ - @backstage/catalog-model@1.1.5-next.0
14
+ - @backstage/plugin-scaffolder-common@1.2.4-next.0
15
+ - @backstage/catalog-client@1.3.0-next.0
16
+ - @backstage/backend-common@0.17.0
17
+ - @backstage/backend-plugin-api@0.2.0
18
+ - @backstage/config@1.0.5
19
+ - @backstage/errors@1.1.4
20
+ - @backstage/integration@1.4.1
21
+ - @backstage/types@1.0.2
22
+ - @backstage/plugin-catalog-common@1.0.10-next.0
23
+ - @backstage/plugin-catalog-node@1.3.1-next.0
24
+ - @backstage/plugin-permission-common@0.7.2
25
+ - @backstage/plugin-permission-node@0.7.2
26
+ - @backstage/plugin-search-common@1.2.0
27
+
28
+ ## 1.6.0
29
+
30
+ ### Minor Changes
31
+
32
+ - 16891a212c: Added new `POST /entities/by-refs` endpoint, which allows you to efficiently
33
+ batch-fetch entities by their entity ref. This can be useful e.g. in graphql
34
+ resolvers or similar contexts where you need to fetch many entities at the same
35
+ time.
36
+ - 273ba3a77f: Deprecated Prometheus metrics in favour of OpenTelemtry metrics.
37
+ - c395abb5b2: The catalog no longer stops after the first processor `validateEntityKind`
38
+ method returns `true` when validating entity kind shapes. Instead, it continues
39
+ through all registered processors that have this method, and requires that _at
40
+ least one_ of them returned true.
41
+
42
+ The old behavior of stopping early made it harder to extend existing core kinds
43
+ with additional fields, since the `BuiltinKindsEntityProcessor` is always
44
+ present at the top of the processing chain and ensures that your additional
45
+ validation code would never be run.
46
+
47
+ This is technically a breaking change, although it should not affect anybody
48
+ under normal circumstances, except if you had problematic validation code that
49
+ you were unaware that it was not being run. That code may now start to exhibit
50
+ those problems.
51
+
52
+ If you need to disable this new behavior, `CatalogBuilder` as used in your
53
+ `packages/backend/src/plugins/catalog.ts` file now has a
54
+ `useLegacySingleProcessorValidation()` method to go back to the old behavior.
55
+
56
+ ```diff
57
+ const builder = await CatalogBuilder.create(env);
58
+ +builder.useLegacySingleProcessorValidation();
59
+ ```
60
+
61
+ - 3072ebfdd7: The search table also holds the original entity value now and the facets endpoint fetches the filtered entity data from the search table.
62
+
63
+ ### Patch Changes
64
+
65
+ - ba13ff663c: Added a new `catalog.rules[].location` configuration that makes it possible to configure catalog rules to only apply to specific locations, either via exact match or a glob pattern.
66
+ - d8593ce0e6: Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package
67
+ - c507aee8a2: Ensured typescript type checks in migration files.
68
+ - 2a8e3cc0b5: Optimize `Stitcher` process to be more memory efficient
69
+ - 884d749b14: Refactored to use `coreServices` from `@backstage/backend-plugin-api`.
70
+ - eacc8e2b55: Make it possible for entity providers to supply only entity refs, instead of full entities, in `delta` mutation deletions.
71
+ - b05dcd5530: Move the `zod` dependency to a version that does not collide with other libraries
72
+ - 5b3e2afa45: Fixed deprecated use of `substr` into `substring`.
73
+ - 71147d5c16: Internal code reorganization.
74
+ - 93870e4df1: Track the last time the final entity changed with new timestamp "last updated at" data in final entities database, which gets updated with the time when final entity is updated.
75
+ - 20a5161f04: Adds MySQL support for the catalog-backend
76
+ - 3280711113: Updated dependency `msw` to `^0.49.0`.
77
+ - e982f77fe3: Registered shutdown hook in experimental catalog plugin.
78
+ - b3fac9c107: Ignore attempts at emitting the current entity as a child of itself.
79
+ - Updated dependencies
80
+ - @backstage/catalog-client@1.2.0
81
+ - @backstage/backend-common@0.17.0
82
+ - @backstage/plugin-catalog-node@1.3.0
83
+ - @backstage/plugin-permission-common@0.7.2
84
+ - @backstage/plugin-permission-node@0.7.2
85
+ - @backstage/errors@1.1.4
86
+ - @backstage/backend-plugin-api@0.2.0
87
+ - @backstage/integration@1.4.1
88
+ - @backstage/types@1.0.2
89
+ - @backstage/plugin-search-common@1.2.0
90
+ - @backstage/catalog-model@1.1.4
91
+ - @backstage/config@1.0.5
92
+ - @backstage/plugin-catalog-common@1.0.9
93
+ - @backstage/plugin-scaffolder-common@1.2.3
94
+
3
95
  ## 1.6.0-next.3
4
96
 
5
97
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "1.6.0-next.3",
3
+ "version": "1.7.0-next.0",
4
4
  "main": "../dist/index.cjs.js",
5
5
  "types": "../dist/index.alpha.d.ts"
6
6
  }
package/dist/index.cjs.js CHANGED
@@ -28,6 +28,7 @@ var backendCommon = require('@backstage/backend-common');
28
28
  var luxon = require('luxon');
29
29
  var promClient = require('prom-client');
30
30
  var stableStringify = require('fast-json-stable-stringify');
31
+ var api = require('@opentelemetry/api');
31
32
  var express = require('express');
32
33
  var Router = require('express-promise-router');
33
34
  var yn = require('yn');
@@ -1724,7 +1725,6 @@ class DefaultProcessingDatabase {
1724
1725
  async addUnprocessedEntities(txOpaque, options) {
1725
1726
  const tx = txOpaque;
1726
1727
  const stateReferences = new Array();
1727
- const conflictingStateReferences = new Array();
1728
1728
  for (const { entity, locationKey } of options.entities) {
1729
1729
  const entityRef = catalogModel.stringifyEntityRef(entity);
1730
1730
  const hash = generateStableHash$1(entity);
@@ -1758,10 +1758,9 @@ class DefaultProcessingDatabase {
1758
1758
  this.options.logger.warn(
1759
1759
  `Detected conflicting entityRef ${entityRef} already referenced by ${conflictingKey} and now also ${locationKey}`
1760
1760
  );
1761
- conflictingStateReferences.push(entityRef);
1762
1761
  }
1763
1762
  }
1764
- await tx("refresh_state_references").whereNotIn("target_entity_ref", conflictingStateReferences).andWhere({ source_entity_ref: options.sourceEntityRef }).delete();
1763
+ await tx("refresh_state_references").andWhere({ source_entity_ref: options.sourceEntityRef }).delete();
1765
1764
  await tx.batchInsert(
1766
1765
  "refresh_state_references",
1767
1766
  stateReferences.map((entityRef) => ({
@@ -2008,54 +2007,104 @@ class DefaultCatalogProcessingEngine {
2008
2007
  }
2009
2008
  }
2010
2009
  function progressTracker() {
2011
- const stitchedEntities = createCounterMetric({
2010
+ const promStitchedEntities = createCounterMetric({
2012
2011
  name: "catalog_stitched_entities_count",
2013
- help: "Amount of entities stitched"
2012
+ help: "Amount of entities stitched. DEPRECATED, use OpenTelemetry metrics instead"
2014
2013
  });
2015
- const processedEntities = createCounterMetric({
2014
+ const promProcessedEntities = createCounterMetric({
2016
2015
  name: "catalog_processed_entities_count",
2017
- help: "Amount of entities processed",
2016
+ help: "Amount of entities processed, DEPRECATED, use OpenTelemetry metrics instead",
2018
2017
  labelNames: ["result"]
2019
2018
  });
2020
- const processingDuration = createSummaryMetric({
2019
+ const promProcessingDuration = createSummaryMetric({
2021
2020
  name: "catalog_processing_duration_seconds",
2022
- help: "Time spent executing the full processing flow",
2021
+ help: "Time spent executing the full processing flow, DEPRECATED, use OpenTelemetry metrics instead",
2023
2022
  labelNames: ["result"]
2024
2023
  });
2025
- const processorsDuration = createSummaryMetric({
2024
+ const promProcessorsDuration = createSummaryMetric({
2026
2025
  name: "catalog_processors_duration_seconds",
2027
- help: "Time spent executing catalog processors",
2026
+ help: "Time spent executing catalog processors, DEPRECATED, use OpenTelemetry metrics instead",
2028
2027
  labelNames: ["result"]
2029
2028
  });
2030
- const processingQueueDelay = createSummaryMetric({
2029
+ const promProcessingQueueDelay = createSummaryMetric({
2031
2030
  name: "catalog_processing_queue_delay_seconds",
2032
- help: "The amount of delay between being scheduled for processing, and the start of actually being processed"
2031
+ help: "The amount of delay between being scheduled for processing, and the start of actually being processed, DEPRECATED, use OpenTelemetry metrics instead"
2033
2032
  });
2033
+ const meter = api.metrics.getMeter("default");
2034
+ const stitchedEntities = meter.createCounter(
2035
+ "catalog.stitched.entities.count",
2036
+ {
2037
+ description: "Amount of entities stitched"
2038
+ }
2039
+ );
2040
+ const processedEntities = meter.createCounter(
2041
+ "catalog.processed.entities.count",
2042
+ { description: "Amount of entities processed" }
2043
+ );
2044
+ const processingDuration = meter.createHistogram(
2045
+ "catalog.processing.duration",
2046
+ {
2047
+ description: "Time spent executing the full processing flow",
2048
+ unit: "seconds"
2049
+ }
2050
+ );
2051
+ const processorsDuration = meter.createHistogram(
2052
+ "catalog.processors.duration",
2053
+ {
2054
+ description: "Time spent executing catalog processors",
2055
+ unit: "seconds"
2056
+ }
2057
+ );
2058
+ const processingQueueDelay = meter.createHistogram(
2059
+ "catalog.processing.queue.delay",
2060
+ {
2061
+ description: "The amount of delay between being scheduled for processing, and the start of actually being processed",
2062
+ unit: "seconds"
2063
+ }
2064
+ );
2034
2065
  function processStart(item, logger) {
2066
+ const startTime = process.hrtime();
2067
+ const endOverallTimer = promProcessingDuration.startTimer();
2068
+ const endProcessorsTimer = promProcessorsDuration.startTimer();
2035
2069
  logger.debug(`Processing ${item.entityRef}`);
2036
2070
  if (item.nextUpdateAt) {
2037
- processingQueueDelay.observe(-item.nextUpdateAt.diffNow().as("seconds"));
2071
+ const seconds = -item.nextUpdateAt.diffNow().as("seconds");
2072
+ promProcessingQueueDelay.observe(seconds);
2073
+ processingQueueDelay.record(seconds);
2074
+ }
2075
+ function endTime() {
2076
+ const delta = process.hrtime(startTime);
2077
+ return delta[0] + delta[1] / 1e9;
2038
2078
  }
2039
- const endOverallTimer = processingDuration.startTimer();
2040
- const endProcessorsTimer = processorsDuration.startTimer();
2041
2079
  function markProcessorsCompleted(result) {
2042
2080
  endProcessorsTimer({ result: result.ok ? "ok" : "failed" });
2081
+ processorsDuration.record(endTime(), {
2082
+ result: result.ok ? "ok" : "failed"
2083
+ });
2043
2084
  }
2044
2085
  function markSuccessfulWithNoChanges() {
2045
2086
  endOverallTimer({ result: "unchanged" });
2046
- processedEntities.inc({ result: "unchanged" }, 1);
2087
+ promProcessedEntities.inc({ result: "unchanged" }, 1);
2088
+ processingDuration.record(endTime(), { result: "unchanged" });
2089
+ processedEntities.add(1, { result: "unchanged" });
2047
2090
  }
2048
2091
  function markSuccessfulWithErrors() {
2049
2092
  endOverallTimer({ result: "errors" });
2050
- processedEntities.inc({ result: "errors" }, 1);
2093
+ promProcessedEntities.inc({ result: "errors" }, 1);
2094
+ processingDuration.record(endTime(), { result: "errors" });
2095
+ processedEntities.add(1, { result: "errors" });
2051
2096
  }
2052
2097
  function markSuccessfulWithChanges(stitchedCount) {
2053
2098
  endOverallTimer({ result: "changed" });
2054
- stitchedEntities.inc(stitchedCount);
2055
- processedEntities.inc({ result: "changed" }, 1);
2099
+ promStitchedEntities.inc(stitchedCount);
2100
+ promProcessedEntities.inc({ result: "changed" }, 1);
2101
+ processingDuration.record(endTime(), { result: "changed" });
2102
+ stitchedEntities.add(stitchedCount);
2103
+ processedEntities.add(1, { result: "changed" });
2056
2104
  }
2057
2105
  function markFailed(error) {
2058
- processedEntities.inc({ result: "failed" }, 1);
2106
+ promProcessedEntities.inc({ result: "failed" }, 1);
2107
+ processedEntities.add(1, { result: "failed" });
2059
2108
  logger.warn(`Processing of ${item.entityRef} failed`, error);
2060
2109
  }
2061
2110
  return {
@@ -2198,7 +2247,7 @@ function stringifyPagination(input) {
2198
2247
  return base64;
2199
2248
  }
2200
2249
  function addCondition(queryBuilder, db, filter, negate = false, entityIdField = "entity_id") {
2201
- const matchQuery = db("search").select(entityIdField).where({ key: filter.key.toLowerCase() }).andWhere(function keyFilter() {
2250
+ const matchQuery = db("search").select("search.entity_id").where({ key: filter.key.toLowerCase() }).andWhere(function keyFilter() {
2202
2251
  if (filter.values) {
2203
2252
  if (filter.values.length === 1) {
2204
2253
  this.where({ value: filter.values[0].toLowerCase() });
@@ -2254,12 +2303,41 @@ class DefaultEntitiesCatalog {
2254
2303
  this.stitcher = stitcher;
2255
2304
  }
2256
2305
  async entities(request) {
2306
+ var _a, _b;
2257
2307
  const db = this.database;
2258
2308
  let entitiesQuery = db("final_entities").select("final_entities.*");
2309
+ (_a = request == null ? void 0 : request.order) == null ? void 0 : _a.forEach(({ field }, index) => {
2310
+ const alias = `order_${index}`;
2311
+ entitiesQuery = entitiesQuery.leftOuterJoin(
2312
+ { [alias]: "search" },
2313
+ function search(inner) {
2314
+ inner.on(`${alias}.entity_id`, "final_entities.entity_id").andOn(`${alias}.key`, db.raw("?", [field]));
2315
+ }
2316
+ );
2317
+ });
2318
+ entitiesQuery = entitiesQuery.whereNotNull("final_entities.final_entity");
2259
2319
  if (request == null ? void 0 : request.filter) {
2260
- entitiesQuery = parseFilter(request.filter, entitiesQuery, db);
2320
+ entitiesQuery = parseFilter(
2321
+ request.filter,
2322
+ entitiesQuery,
2323
+ db,
2324
+ false,
2325
+ "final_entities.entity_id"
2326
+ );
2261
2327
  }
2262
- entitiesQuery = entitiesQuery.whereNotNull("final_entities.final_entity").orderBy("entity_id", "asc");
2328
+ (_b = request == null ? void 0 : request.order) == null ? void 0 : _b.forEach(({ order }, index) => {
2329
+ if (db.client.config.client === "pg") {
2330
+ entitiesQuery = entitiesQuery.orderBy([
2331
+ { column: `order_${index}.value`, order, nulls: "last" }
2332
+ ]);
2333
+ } else {
2334
+ entitiesQuery = entitiesQuery.orderBy([
2335
+ { column: `order_${index}.value`, order: void 0, nulls: "last" },
2336
+ { column: `order_${index}.value`, order }
2337
+ ]);
2338
+ }
2339
+ });
2340
+ entitiesQuery = entitiesQuery.orderBy("final_entities.entity_id", "asc");
2263
2341
  const { limit, offset } = parsePagination(request == null ? void 0 : request.pagination);
2264
2342
  if (limit !== void 0) {
2265
2343
  entitiesQuery = entitiesQuery.limit(limit + 1);
@@ -2439,8 +2517,14 @@ class ProcessorOutputCollector {
2439
2517
  this.refreshKeys = new Array();
2440
2518
  this.done = false;
2441
2519
  }
2442
- get onEmit() {
2443
- return (i) => this.receive(i);
2520
+ generic() {
2521
+ return (i) => this.receive(this.logger, i);
2522
+ }
2523
+ forProcessor(processor) {
2524
+ const logger = this.logger.child({
2525
+ processor: processor.getProcessorName()
2526
+ });
2527
+ return (i) => this.receive(logger, i);
2444
2528
  }
2445
2529
  results() {
2446
2530
  this.done = true;
@@ -2451,9 +2535,9 @@ class ProcessorOutputCollector {
2451
2535
  deferredEntities: this.deferredEntities
2452
2536
  };
2453
2537
  }
2454
- receive(i) {
2538
+ receive(logger, i) {
2455
2539
  if (this.done) {
2456
- this.logger.warn(
2540
+ logger.warn(
2457
2541
  `Item of type "${i.type}" was emitted after processing had completed. Stack trace: ${new Error().stack}`
2458
2542
  );
2459
2543
  return;
@@ -2465,10 +2549,17 @@ class ProcessorOutputCollector {
2465
2549
  entity = validateEntityEnvelope(i.entity);
2466
2550
  } catch (e) {
2467
2551
  errors.assertError(e);
2468
- this.logger.debug(`Envelope validation failed at ${location}, ${e}`);
2552
+ logger.debug(`Envelope validation failed at ${location}, ${e}`);
2469
2553
  this.errors.push(e);
2470
2554
  return;
2471
2555
  }
2556
+ const entityRef = catalogModel.stringifyEntityRef(entity);
2557
+ if (entityRef === catalogModel.stringifyEntityRef(this.parentEntity)) {
2558
+ logger.warn(
2559
+ `Ignored emitted entity ${entityRef} whose ref was identical to the one being processed. This commonly indicates mistakenly emitting the input entity instead of returning it.`
2560
+ );
2561
+ return;
2562
+ }
2472
2563
  const annotations = entity.metadata.annotations || {};
2473
2564
  if (typeof annotations === "object" && !Array.isArray(annotations)) {
2474
2565
  const originLocation = getEntityOriginLocationRef(this.parentEntity);
@@ -2666,7 +2757,7 @@ class DefaultCatalogProcessingOrchestrator {
2666
2757
  res = await processor.preProcessEntity(
2667
2758
  res,
2668
2759
  context.location,
2669
- context.collector.onEmit,
2760
+ context.collector.forProcessor(processor),
2670
2761
  context.originLocation,
2671
2762
  context.cache.forProcessor(processor)
2672
2763
  );
@@ -2749,7 +2840,7 @@ class DefaultCatalogProcessingOrchestrator {
2749
2840
  }
2750
2841
  for (const maybeRelativeTarget of targets) {
2751
2842
  if (type === "file" && maybeRelativeTarget.endsWith(path__default["default"].sep)) {
2752
- context.collector.onEmit(
2843
+ context.collector.generic()(
2753
2844
  pluginCatalogNode.processingResult.inputError(
2754
2845
  context.location,
2755
2846
  `LocationEntityProcessor cannot handle ${type} type location with target ${context.location.target} that ends with a path separator`
@@ -2774,7 +2865,7 @@ class DefaultCatalogProcessingOrchestrator {
2774
2865
  presence
2775
2866
  },
2776
2867
  presence === "optional",
2777
- context.collector.onEmit,
2868
+ context.collector.forProcessor(processor),
2778
2869
  this.options.parser,
2779
2870
  context.cache.forProcessor(processor, target)
2780
2871
  );
@@ -2805,7 +2896,7 @@ class DefaultCatalogProcessingOrchestrator {
2805
2896
  res = await processor.postProcessEntity(
2806
2897
  res,
2807
2898
  context.location,
2808
- context.collector.onEmit,
2899
+ context.collector.forProcessor(processor),
2809
2900
  context.cache.forProcessor(processor)
2810
2901
  );
2811
2902
  } catch (e) {
@@ -3041,8 +3132,9 @@ class Stitcher {
3041
3132
  "final_entities"
3042
3133
  ).update({
3043
3134
  final_entity: JSON.stringify(entity),
3044
- hash
3045
- }).where("entity_id", entityId).where("stitch_ticket", ticket).onConflict("entity_id").merge(["final_entity", "hash"]);
3135
+ hash,
3136
+ last_updated_at: this.database.fn.now()
3137
+ }).where("entity_id", entityId).where("stitch_ticket", ticket).onConflict("entity_id").merge(["final_entity", "hash", "last_updated_at"]);
3046
3138
  if (amountOfRowsChanged === 0) {
3047
3139
  this.logger.debug(
3048
3140
  `Entity ${entityRef} is already processed, skipping write.`
@@ -3217,6 +3309,22 @@ function parseEntityFacetParams(params) {
3217
3309
  throw new errors.InputError("Missing facet parameter");
3218
3310
  }
3219
3311
 
3312
+ function parseEntityOrderParams(params) {
3313
+ var _a;
3314
+ return (_a = parseStringsParam(params.order, "order")) == null ? void 0 : _a.map((item) => {
3315
+ const match = item.match(/^(asc|desc):(.+)$/);
3316
+ if (!match) {
3317
+ throw new errors.InputError(
3318
+ `Invalid order parameter "${item}", expected "<asc or desc>:<field name>"`
3319
+ );
3320
+ }
3321
+ return {
3322
+ order: match[1],
3323
+ field: match[2]
3324
+ };
3325
+ });
3326
+ }
3327
+
3220
3328
  async function requireRequestBody(req) {
3221
3329
  const contentType = req.header("content-type");
3222
3330
  if (!contentType) {
@@ -3288,6 +3396,7 @@ async function createRouter(options) {
3288
3396
  const { entities, pageInfo } = await entitiesCatalog.entities({
3289
3397
  filter: parseEntityFilterParams(req.query),
3290
3398
  fields: parseEntityTransformParams(req.query),
3399
+ order: parseEntityOrderParams(req.query),
3291
3400
  pagination: parseEntityPaginationParams(req.query),
3292
3401
  authorizationToken: getBearerToken(req.header("authorization"))
3293
3402
  });
@@ -4024,6 +4133,7 @@ class DefaultProviderDatabase {
4024
4133
  logger: this.options.logger
4025
4134
  });
4026
4135
  }
4136
+ await tx("refresh_state_references").where("target_entity_ref", entityRef).andWhere({ source_key: options.sourceKey }).delete();
4027
4137
  if (ok) {
4028
4138
  await tx(
4029
4139
  "refresh_state_references"