@backstage/plugin-catalog-backend 1.6.0-next.0 → 1.6.0-next.2

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,81 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.6.0-next.2
4
+
5
+ ### Minor Changes
6
+
7
+ - 3072ebfdd7: The search table also holds the original entity value now and the facets endpoint fetches the filtered entity data from the search table.
8
+
9
+ ### Patch Changes
10
+
11
+ - c507aee8a2: Ensured typescript type checks in migration files.
12
+ - 884d749b14: Refactored to use `coreServices` from `@backstage/backend-plugin-api`.
13
+ - eacc8e2b55: Make it possible for entity providers to supply only entity refs, instead of full entities, in `delta` mutation deletions.
14
+ - 20a5161f04: Adds MySQL support for the catalog-backend
15
+ - Updated dependencies
16
+ - @backstage/plugin-catalog-node@1.3.0-next.2
17
+ - @backstage/backend-common@0.17.0-next.2
18
+ - @backstage/backend-plugin-api@0.2.0-next.2
19
+ - @backstage/plugin-search-common@1.2.0-next.2
20
+ - @backstage/plugin-permission-node@0.7.2-next.2
21
+ - @backstage/catalog-client@1.2.0-next.1
22
+ - @backstage/catalog-model@1.1.4-next.1
23
+ - @backstage/config@1.0.5-next.1
24
+ - @backstage/errors@1.1.4-next.1
25
+ - @backstage/integration@1.4.1-next.1
26
+ - @backstage/types@1.0.2-next.1
27
+ - @backstage/plugin-catalog-common@1.0.9-next.2
28
+ - @backstage/plugin-permission-common@0.7.2-next.1
29
+ - @backstage/plugin-scaffolder-common@1.2.3-next.1
30
+
31
+ ## 1.6.0-next.1
32
+
33
+ ### Minor Changes
34
+
35
+ - c395abb5b2: The catalog no longer stops after the first processor `validateEntityKind`
36
+ method returns `true` when validating entity kind shapes. Instead, it continues
37
+ through all registered processors that have this method, and requires that _at
38
+ least one_ of them returned true.
39
+
40
+ The old behavior of stopping early made it harder to extend existing core kinds
41
+ with additional fields, since the `BuiltinKindsEntityProcessor` is always
42
+ present at the top of the processing chain and ensures that your additional
43
+ validation code would never be run.
44
+
45
+ This is technically a breaking change, although it should not affect anybody
46
+ under normal circumstances, except if you had problematic validation code that
47
+ you were unaware that it was not being run. That code may now start to exhibit
48
+ those problems.
49
+
50
+ If you need to disable this new behavior, `CatalogBuilder` as used in your
51
+ `packages/backend/src/plugins/catalog.ts` file now has a
52
+ `useLegacySingleProcessorValidation()` method to go back to the old behavior.
53
+
54
+ ```diff
55
+ const builder = await CatalogBuilder.create(env);
56
+ +builder.useLegacySingleProcessorValidation();
57
+ ```
58
+
59
+ ### Patch Changes
60
+
61
+ - 2a8e3cc0b5: Optimize `Stitcher` process to be more memory efficient
62
+ - 5b3e2afa45: Fixed deprecated use of `substr` into `substring`.
63
+ - Updated dependencies
64
+ - @backstage/backend-common@0.17.0-next.1
65
+ - @backstage/types@1.0.2-next.1
66
+ - @backstage/backend-plugin-api@0.1.5-next.1
67
+ - @backstage/plugin-catalog-node@1.2.2-next.1
68
+ - @backstage/plugin-permission-node@0.7.2-next.1
69
+ - @backstage/config@1.0.5-next.1
70
+ - @backstage/integration@1.4.1-next.1
71
+ - @backstage/catalog-client@1.2.0-next.1
72
+ - @backstage/catalog-model@1.1.4-next.1
73
+ - @backstage/errors@1.1.4-next.1
74
+ - @backstage/plugin-catalog-common@1.0.9-next.1
75
+ - @backstage/plugin-permission-common@0.7.2-next.1
76
+ - @backstage/plugin-scaffolder-common@1.2.3-next.1
77
+ - @backstage/plugin-search-common@1.1.2-next.1
78
+
3
79
  ## 1.6.0-next.0
4
80
 
5
81
  ### Minor Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "1.6.0-next.0",
3
+ "version": "1.6.0-next.2",
4
4
  "main": "../dist/index.cjs.js",
5
5
  "types": "../dist/index.alpha.d.ts"
6
6
  }
@@ -177,6 +177,7 @@ export declare class CatalogBuilder {
177
177
  private locationAnalyzer;
178
178
  private readonly permissionRules;
179
179
  private allowedLocationType;
180
+ private legacySingleProcessorValidation;
180
181
  /**
181
182
  * Creates a catalog builder.
182
183
  */
@@ -306,13 +307,18 @@ export declare class CatalogBuilder {
306
307
  * @param permissionRules - Additional permission rules
307
308
  * @alpha
308
309
  */
309
- addPermissionRules(...permissionRules: Array<CatalogPermissionRule | Array<CatalogPermissionRule>>): void;
310
+ addPermissionRules(...permissionRules: Array<CatalogPermissionRule | Array<CatalogPermissionRule>>): this;
310
311
  /**
311
312
  * Sets up the allowed location types from being registered via the location service.
312
313
  *
313
314
  * @param allowedLocationTypes - the allowed location types
314
315
  */
315
316
  setAllowedLocationTypes(allowedLocationTypes: string[]): CatalogBuilder;
317
+ /**
318
+ * Enables the legacy behaviour of canceling validation early whenever only a
319
+ * single processor declares an entity kind to be valid.
320
+ */
321
+ useLegacySingleProcessorValidation(): this;
316
322
  /**
317
323
  * Wires up and returns all of the component parts of the catalog
318
324
  */
@@ -177,6 +177,7 @@ export declare class CatalogBuilder {
177
177
  private locationAnalyzer;
178
178
  private readonly permissionRules;
179
179
  private allowedLocationType;
180
+ private legacySingleProcessorValidation;
180
181
  /**
181
182
  * Creates a catalog builder.
182
183
  */
@@ -305,6 +306,11 @@ export declare class CatalogBuilder {
305
306
  * @param allowedLocationTypes - the allowed location types
306
307
  */
307
308
  setAllowedLocationTypes(allowedLocationTypes: string[]): CatalogBuilder;
309
+ /**
310
+ * Enables the legacy behaviour of canceling validation early whenever only a
311
+ * single processor declares an entity kind to be valid.
312
+ */
313
+ useLegacySingleProcessorValidation(): this;
308
314
  /**
309
315
  * Wires up and returns all of the component parts of the catalog
310
316
  */
package/dist/index.cjs.js CHANGED
@@ -601,7 +601,7 @@ class PlaceholderProcessor {
601
601
  } else if (keys.length !== 1) {
602
602
  return [data, false];
603
603
  }
604
- const resolverKey = keys[0].substr(1);
604
+ const resolverKey = keys[0].substring(1);
605
605
  const resolverValue = data[keys[0]];
606
606
  const resolver = this.options.resolvers[resolverKey];
607
607
  if (!resolver) {
@@ -1517,6 +1517,7 @@ class DefaultProcessingDatabase {
1517
1517
  refreshKeys,
1518
1518
  locationKey
1519
1519
  } = options;
1520
+ const configClient = tx.client.config.client;
1520
1521
  const refreshResult = await tx("refresh_state").update({
1521
1522
  processed_entity: JSON.stringify(processedEntity),
1522
1523
  result_hash: resultHash,
@@ -1539,7 +1540,7 @@ class DefaultProcessingDatabase {
1539
1540
  sourceEntityRef
1540
1541
  });
1541
1542
  let previousRelationRows;
1542
- if (tx.client.config.client.includes("sqlite3") || tx.client.config.client.includes("mysql")) {
1543
+ if (configClient.includes("sqlite3") || configClient.includes("mysql")) {
1543
1544
  previousRelationRows = await tx("relations").select("*").where({ originating_entity_id: id });
1544
1545
  await tx("relations").where({ originating_entity_id: id }).delete();
1545
1546
  } else {
@@ -1868,16 +1869,20 @@ class DefaultProcessingDatabase {
1868
1869
  next_update_at: tx.fn.now(),
1869
1870
  last_discovery_at: tx.fn.now()
1870
1871
  });
1871
- if (!tx.client.config.client.includes("sqlite3")) {
1872
+ if (tx.client.config.client.includes("pg")) {
1872
1873
  query = query.onConflict("entity_ref").ignore();
1873
1874
  }
1874
1875
  const result = await query;
1875
1876
  return result.rowCount === 1 || result.length === 1;
1876
1877
  } catch (error) {
1877
- if (errors.isError(error) && error.message.includes("UNIQUE constraint failed")) {
1878
+ if (!backendCommon.isDatabaseConflictError(error)) {
1879
+ throw error;
1880
+ } else {
1881
+ this.options.logger.debug(
1882
+ `Unable to insert a new refresh state row, ${error}`
1883
+ );
1878
1884
  return false;
1879
1885
  }
1880
- throw error;
1881
1886
  }
1882
1887
  }
1883
1888
  async checkLocationKeyConflict(tx, entityRef, locationKey) {
@@ -1906,7 +1911,7 @@ class DefaultProcessingDatabase {
1906
1911
  deferred: e,
1907
1912
  hash: generateStableHash$1(e.entity)
1908
1913
  })),
1909
- toRemove: options.removed.map((e) => catalogModel.stringifyEntityRef(e.entity))
1914
+ toRemove: options.removed.map((e) => e.entityRef)
1910
1915
  };
1911
1916
  }
1912
1917
  const oldRefs = await tx(
@@ -2425,8 +2430,8 @@ function stringifyPagination(input) {
2425
2430
  const base64 = Buffer.from(json, "utf8").toString("base64");
2426
2431
  return base64;
2427
2432
  }
2428
- function addCondition(queryBuilder, db, filter, negate = false) {
2429
- const matchQuery = db("search").select("entity_id").where({ key: filter.key.toLowerCase() }).andWhere(function keyFilter() {
2433
+ function addCondition(queryBuilder, db, filter, negate = false, entityIdField = "entity_id") {
2434
+ const matchQuery = db("search").select(entityIdField).where({ key: filter.key.toLowerCase() }).andWhere(function keyFilter() {
2430
2435
  if (filter.values) {
2431
2436
  if (filter.values.length === 1) {
2432
2437
  this.where({ value: filter.values[0].toLowerCase() });
@@ -2439,7 +2444,7 @@ function addCondition(queryBuilder, db, filter, negate = false) {
2439
2444
  }
2440
2445
  }
2441
2446
  });
2442
- queryBuilder.andWhere("entity_id", negate ? "not in" : "in", matchQuery);
2447
+ queryBuilder.andWhere(entityIdField, negate ? "not in" : "in", matchQuery);
2443
2448
  }
2444
2449
  function isEntitiesSearchFilter(filter) {
2445
2450
  return filter.hasOwnProperty("key");
@@ -2450,24 +2455,28 @@ function isOrEntityFilter(filter) {
2450
2455
  function isNegationEntityFilter(filter) {
2451
2456
  return filter.hasOwnProperty("not");
2452
2457
  }
2453
- function parseFilter(filter, query, db, negate = false) {
2458
+ function parseFilter(filter, query, db, negate = false, entityIdField = "entity_id") {
2454
2459
  if (isEntitiesSearchFilter(filter)) {
2455
2460
  return query.andWhere(function filterFunction() {
2456
- addCondition(this, db, filter, negate);
2461
+ addCondition(this, db, filter, negate, entityIdField);
2457
2462
  });
2458
2463
  }
2459
2464
  if (isNegationEntityFilter(filter)) {
2460
- return parseFilter(filter.not, query, db, !negate);
2465
+ return parseFilter(filter.not, query, db, !negate, entityIdField);
2461
2466
  }
2462
2467
  return query[negate ? "andWhereNot" : "andWhere"](function filterFunction() {
2463
2468
  var _a, _b;
2464
2469
  if (isOrEntityFilter(filter)) {
2465
2470
  for (const subFilter of (_a = filter.anyOf) != null ? _a : []) {
2466
- this.orWhere((subQuery) => parseFilter(subFilter, subQuery, db));
2471
+ this.orWhere(
2472
+ (subQuery) => parseFilter(subFilter, subQuery, db, false, entityIdField)
2473
+ );
2467
2474
  }
2468
2475
  } else {
2469
2476
  for (const subFilter of (_b = filter.allOf) != null ? _b : []) {
2470
- this.andWhere((subQuery) => parseFilter(subFilter, subQuery, db));
2477
+ this.andWhere(
2478
+ (subQuery) => parseFilter(subFilter, subQuery, db, false, entityIdField)
2479
+ );
2471
2480
  }
2472
2481
  }
2473
2482
  });
@@ -2551,14 +2560,36 @@ class DefaultEntitiesCatalog {
2551
2560
  return { items };
2552
2561
  }
2553
2562
  async removeEntityByUid(uid) {
2554
- await this.database("refresh_state").update({
2555
- result_hash: "child-was-deleted",
2556
- next_update_at: this.database.fn.now()
2557
- }).whereIn("entity_ref", function parents(builder) {
2558
- return builder.from("refresh_state").innerJoin("refresh_state_references", {
2559
- "refresh_state_references.target_entity_ref": "refresh_state.entity_ref"
2560
- }).where("refresh_state.entity_id", "=", uid).select("refresh_state_references.source_entity_ref");
2561
- });
2563
+ const dbConfig = this.database.client.config;
2564
+ if (dbConfig.client.includes("mysql")) {
2565
+ const results = await this.database("refresh_state").select("entity_id").whereIn("entity_ref", function parents(builder) {
2566
+ return builder.from("refresh_state").innerJoin(
2567
+ "refresh_state_references",
2568
+ {
2569
+ "refresh_state_references.target_entity_ref": "refresh_state.entity_ref"
2570
+ }
2571
+ ).where("refresh_state.entity_id", "=", uid).select("refresh_state_references.source_entity_ref");
2572
+ });
2573
+ await this.database("refresh_state").update({
2574
+ result_hash: "child-was-deleted",
2575
+ next_update_at: this.database.fn.now()
2576
+ }).whereIn(
2577
+ "entity_id",
2578
+ results.map((key) => key.entity_id)
2579
+ );
2580
+ } else {
2581
+ await this.database("refresh_state").update({
2582
+ result_hash: "child-was-deleted",
2583
+ next_update_at: this.database.fn.now()
2584
+ }).whereIn("entity_ref", function parents(builder) {
2585
+ return builder.from("refresh_state").innerJoin(
2586
+ "refresh_state_references",
2587
+ {
2588
+ "refresh_state_references.target_entity_ref": "refresh_state.entity_ref"
2589
+ }
2590
+ ).where("refresh_state.entity_id", "=", uid).select("refresh_state_references.source_entity_ref");
2591
+ });
2592
+ }
2562
2593
  const relationPeers = await this.database.from("relations").innerJoin("refresh_state", {
2563
2594
  "refresh_state.entity_ref": "relations.target_entity_ref"
2564
2595
  }).where("relations.originating_entity_id", "=", uid).andWhere("refresh_state.entity_id", "!=", uid).select({ ref: "relations.target_entity_ref" }).union(
@@ -2614,32 +2645,17 @@ class DefaultEntitiesCatalog {
2614
2645
  };
2615
2646
  }
2616
2647
  async facets(request) {
2617
- const { entities } = await this.entities({
2618
- filter: request.filter,
2619
- authorizationToken: request.authorizationToken
2620
- });
2621
2648
  const facets = {};
2649
+ const db = this.database;
2622
2650
  for (const facet of request.facets) {
2623
- const values = entities.map((entity) => {
2624
- var _a, _b;
2625
- if (facet.startsWith("metadata.annotations.")) {
2626
- return (_a = entity.metadata.annotations) == null ? void 0 : _a[facet.substring("metadata.annotations.".length)];
2627
- } else if (facet.startsWith("metadata.labels.")) {
2628
- return (_b = entity.metadata.labels) == null ? void 0 : _b[facet.substring("metadata.labels.".length)];
2629
- }
2630
- return lodash__default["default"].get(entity, facet);
2631
- }).flatMap((field) => {
2632
- if (typeof field === "string") {
2633
- return [field];
2634
- } else if (Array.isArray(field)) {
2635
- return field.filter((i) => typeof i === "string");
2636
- }
2637
- return [];
2638
- }).sort();
2639
- const counts = lodash__default["default"].countBy(values, lodash__default["default"].identity);
2640
- facets[facet] = Object.entries(counts).map(([value, count]) => ({
2641
- value,
2642
- count
2651
+ const dbQuery = db("search").join("final_entities", "search.entity_id", "final_entities.entity_id").where("search.key", facet.toLocaleLowerCase("en-US")).count("search.entity_id as count").select({ value: "search.original_value" }).groupBy("search.original_value");
2652
+ if (request == null ? void 0 : request.filter) {
2653
+ parseFilter(request.filter, dbQuery, db, false, "search.entity_id");
2654
+ }
2655
+ const result = await dbQuery;
2656
+ facets[facet] = result.map((data) => ({
2657
+ value: String(data.value),
2658
+ count: Number(data.count)
2643
2659
  }));
2644
2660
  }
2645
2661
  return { facets };
@@ -2930,13 +2946,16 @@ class DefaultCatalogProcessingOrchestrator {
2930
2946
  e
2931
2947
  );
2932
2948
  }
2933
- let foundKind = false;
2949
+ let valid = false;
2934
2950
  for (const processor of this.options.processors) {
2935
2951
  if (processor.validateEntityKind) {
2936
2952
  try {
2937
- foundKind = await processor.validateEntityKind(entity);
2938
- if (foundKind) {
2939
- break;
2953
+ const thisValid = await processor.validateEntityKind(entity);
2954
+ if (thisValid) {
2955
+ valid = true;
2956
+ if (this.options.legacySingleProcessorValidation) {
2957
+ break;
2958
+ }
2940
2959
  }
2941
2960
  } catch (e) {
2942
2961
  throw new errors.InputError(
@@ -2946,7 +2965,7 @@ class DefaultCatalogProcessingOrchestrator {
2946
2965
  }
2947
2966
  }
2948
2967
  }
2949
- if (!foundKind) {
2968
+ if (!valid) {
2950
2969
  throw new errors.InputError(
2951
2970
  `No processor recognized the entity ${context.entityRef} as valid, possibly caused by a foreign kind or apiVersion`
2952
2971
  );
@@ -3079,15 +3098,30 @@ function mapToRows(input, entityId) {
3079
3098
  for (const { key: rawKey, value: rawValue } of input) {
3080
3099
  const key = rawKey.toLocaleLowerCase("en-US");
3081
3100
  if (rawValue === void 0 || rawValue === null) {
3082
- result.push({ entity_id: entityId, key, value: null });
3101
+ result.push({
3102
+ entity_id: entityId,
3103
+ key,
3104
+ original_value: null,
3105
+ value: null
3106
+ });
3083
3107
  } else {
3084
3108
  const value = String(rawValue).toLocaleLowerCase("en-US");
3085
3109
  if (key.length <= MAX_KEY_LENGTH) {
3086
- result.push({
3087
- entity_id: entityId,
3088
- key,
3089
- value: value.length <= MAX_VALUE_LENGTH ? value : null
3090
- });
3110
+ if (value.length <= MAX_VALUE_LENGTH) {
3111
+ result.push({
3112
+ entity_id: entityId,
3113
+ key,
3114
+ original_value: String(rawValue),
3115
+ value
3116
+ });
3117
+ } else {
3118
+ result.push({
3119
+ entity_id: entityId,
3120
+ key,
3121
+ original_value: null,
3122
+ value: null
3123
+ });
3124
+ }
3091
3125
  }
3092
3126
  }
3093
3127
  }
@@ -3159,22 +3193,24 @@ class Stitcher {
3159
3193
  hash: "",
3160
3194
  stitch_ticket: ticket
3161
3195
  }).onConflict("entity_id").merge(["stitch_ticket"]);
3162
- const result = await this.database.with("incoming_references", function incomingReferences(builder) {
3163
- return builder.from("refresh_state_references").where({ target_entity_ref: entityRef }).count({ count: "*" });
3164
- }).select({
3165
- entityId: "refresh_state.entity_id",
3166
- processedEntity: "refresh_state.processed_entity",
3167
- errors: "refresh_state.errors",
3168
- incomingReferenceCount: "incoming_references.count",
3169
- previousHash: "final_entities.hash",
3170
- relationType: "relations.type",
3171
- relationTarget: "relations.target_entity_ref"
3172
- }).from("refresh_state").where({ "refresh_state.entity_ref": entityRef }).crossJoin(this.database.raw("incoming_references")).leftOuterJoin("final_entities", {
3173
- "final_entities.entity_id": "refresh_state.entity_id"
3174
- }).leftOuterJoin("relations", {
3175
- "relations.source_entity_ref": "refresh_state.entity_ref"
3176
- }).orderBy("relationType", "asc").orderBy("relationTarget", "asc");
3177
- if (!result.length) {
3196
+ const [processedResult, relationsResult] = await Promise.all([
3197
+ this.database.with("incoming_references", function incomingReferences(builder) {
3198
+ return builder.from("refresh_state_references").where({ target_entity_ref: entityRef }).count({ count: "*" });
3199
+ }).select({
3200
+ entityId: "refresh_state.entity_id",
3201
+ processedEntity: "refresh_state.processed_entity",
3202
+ errors: "refresh_state.errors",
3203
+ incomingReferenceCount: "incoming_references.count",
3204
+ previousHash: "final_entities.hash"
3205
+ }).from("refresh_state").where({ "refresh_state.entity_ref": entityRef }).crossJoin(this.database.raw("incoming_references")).leftOuterJoin("final_entities", {
3206
+ "final_entities.entity_id": "refresh_state.entity_id"
3207
+ }),
3208
+ this.database.distinct({
3209
+ relationType: "type",
3210
+ relationTarget: "target_entity_ref"
3211
+ }).from("relations").where({ source_entity_ref: entityRef }).orderBy("relationType", "asc").orderBy("relationTarget", "asc")
3212
+ ]);
3213
+ if (!processedResult.length) {
3178
3214
  this.logger.error(
3179
3215
  `Unable to stitch ${entityRef}, item does not exist in refresh state table`
3180
3216
  );
@@ -3186,7 +3222,7 @@ class Stitcher {
3186
3222
  errors,
3187
3223
  incomingReferenceCount,
3188
3224
  previousHash
3189
- } = result[0];
3225
+ } = processedResult[0];
3190
3226
  if (!processedEntity) {
3191
3227
  this.logger.debug(
3192
3228
  `Unable to stitch ${entityRef}, the entity has not yet been processed`
@@ -3214,11 +3250,7 @@ class Stitcher {
3214
3250
  }));
3215
3251
  }
3216
3252
  }
3217
- const uniqueRelationRows = lodash.uniqBy(
3218
- result,
3219
- (r) => `${r.relationType}:${r.relationTarget}`
3220
- );
3221
- entity.relations = uniqueRelationRows.filter((row) => row.relationType).map((row) => ({
3253
+ entity.relations = relationsResult.filter((row) => row.relationType).map((row) => ({
3222
3254
  type: row.relationType,
3223
3255
  targetRef: row.relationTarget
3224
3256
  }));
@@ -3330,8 +3362,8 @@ function parseEntityFilterString(filterString) {
3330
3362
  const filtersByKey = {};
3331
3363
  for (const statement of statements) {
3332
3364
  const equalsIndex = statement.indexOf("=");
3333
- const key = equalsIndex === -1 ? statement : statement.substr(0, equalsIndex).trim();
3334
- const value = equalsIndex === -1 ? void 0 : statement.substr(equalsIndex + 1).trim();
3365
+ const key = equalsIndex === -1 ? statement : statement.substring(0, equalsIndex).trim();
3366
+ const value = equalsIndex === -1 ? void 0 : statement.substring(equalsIndex + 1).trim();
3335
3367
  if (!key) {
3336
3368
  throw new errors.InputError(
3337
3369
  `Invalid filter, '${statement}' is not a valid statement (expected a string on the form a=b or a= or a)`
@@ -3807,13 +3839,20 @@ class Connection {
3807
3839
  });
3808
3840
  } else if (mutation.type === "delta") {
3809
3841
  this.check(mutation.added.map((e) => e.entity));
3810
- this.check(mutation.removed.map((e) => e.entity));
3842
+ this.check(
3843
+ mutation.removed.map((e) => "entity" in e ? e.entity : void 0).filter((e) => Boolean(e))
3844
+ );
3811
3845
  await db.transaction(async (tx) => {
3812
3846
  await db.replaceUnprocessedEntities(tx, {
3813
3847
  sourceKey: this.config.id,
3814
3848
  type: "delta",
3815
3849
  added: mutation.added,
3816
- removed: mutation.removed
3850
+ removed: mutation.removed.map(
3851
+ (r) => "entityRef" in r ? r : {
3852
+ entityRef: catalogModel.stringifyEntityRef(r.entity),
3853
+ locationKey: r.locationKey
3854
+ }
3855
+ )
3817
3856
  });
3818
3857
  });
3819
3858
  }
@@ -4057,6 +4096,7 @@ class CatalogBuilder {
4057
4096
  maxSeconds: 150
4058
4097
  });
4059
4098
  this.locationAnalyzer = void 0;
4099
+ this.legacySingleProcessorValidation = false;
4060
4100
  this.env = env;
4061
4101
  this.entityPolicies = [];
4062
4102
  this.entityPoliciesReplace = false;
@@ -4138,11 +4178,16 @@ class CatalogBuilder {
4138
4178
  }
4139
4179
  addPermissionRules(...permissionRules) {
4140
4180
  this.permissionRules.push(...permissionRules.flat());
4181
+ return this;
4141
4182
  }
4142
4183
  setAllowedLocationTypes(allowedLocationTypes) {
4143
4184
  this.allowedLocationType = allowedLocationTypes;
4144
4185
  return this;
4145
4186
  }
4187
+ useLegacySingleProcessorValidation() {
4188
+ this.legacySingleProcessorValidation = true;
4189
+ return this;
4190
+ }
4146
4191
  async build() {
4147
4192
  var _a, _b;
4148
4193
  const { config, database, logger, permissions } = this.env;
@@ -4167,7 +4212,8 @@ class CatalogBuilder {
4167
4212
  rulesEnforcer,
4168
4213
  logger,
4169
4214
  parser,
4170
- policy
4215
+ policy,
4216
+ legacySingleProcessorValidation: this.legacySingleProcessorValidation
4171
4217
  });
4172
4218
  const stitcher = new Stitcher(dbClient, logger);
4173
4219
  const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog(
@@ -4436,13 +4482,13 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4436
4482
  );
4437
4483
  env.registerInit({
4438
4484
  deps: {
4439
- logger: backendPluginApi.loggerServiceRef,
4440
- config: backendPluginApi.configServiceRef,
4441
- reader: backendPluginApi.urlReaderServiceRef,
4442
- permissions: backendPluginApi.permissionsServiceRef,
4443
- database: backendPluginApi.databaseServiceRef,
4444
- httpRouter: backendPluginApi.httpRouterServiceRef,
4445
- lifecycle: backendPluginApi.lifecycleServiceRef
4485
+ logger: backendPluginApi.coreServices.logger,
4486
+ config: backendPluginApi.coreServices.config,
4487
+ reader: backendPluginApi.coreServices.urlReader,
4488
+ permissions: backendPluginApi.coreServices.permissions,
4489
+ database: backendPluginApi.coreServices.database,
4490
+ httpRouter: backendPluginApi.coreServices.httpRouter,
4491
+ lifecycle: backendPluginApi.coreServices.lifecycle
4446
4492
  },
4447
4493
  async init({
4448
4494
  logger,