@backstage/plugin-catalog-backend 1.5.1 → 1.6.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,83 @@
1
1
  # @backstage/plugin-catalog-backend
2
2
 
3
+ ## 1.6.0-next.1
4
+
5
+ ### Minor Changes
6
+
7
+ - c395abb5b2: The catalog no longer stops after the first processor `validateEntityKind`
8
+ method returns `true` when validating entity kind shapes. Instead, it continues
9
+ through all registered processors that have this method, and requires that _at
10
+ least one_ of them returned true.
11
+
12
+ The old behavior of stopping early made it harder to extend existing core kinds
13
+ with additional fields, since the `BuiltinKindsEntityProcessor` is always
14
+ present at the top of the processing chain and ensures that your additional
15
+ validation code would never be run.
16
+
17
+ This is technically a breaking change, although it should not affect anybody
18
+ under normal circumstances, except if you had problematic validation code that
19
+ you were unaware that it was not being run. That code may now start to exhibit
20
+ those problems.
21
+
22
+ If you need to disable this new behavior, `CatalogBuilder` as used in your
23
+ `packages/backend/src/plugins/catalog.ts` file now has a
24
+ `useLegacySingleProcessorValidation()` method to go back to the old behavior.
25
+
26
+ ```diff
27
+ const builder = await CatalogBuilder.create(env);
28
+ +builder.useLegacySingleProcessorValidation();
29
+ ```
30
+
31
+ ### Patch Changes
32
+
33
+ - 2a8e3cc0b5: Optimize `Stitcher` process to be more memory efficient
34
+ - 5b3e2afa45: Fixed deprecated use of `substr` into `substring`.
35
+ - Updated dependencies
36
+ - @backstage/backend-common@0.17.0-next.1
37
+ - @backstage/types@1.0.2-next.1
38
+ - @backstage/backend-plugin-api@0.1.5-next.1
39
+ - @backstage/plugin-catalog-node@1.2.2-next.1
40
+ - @backstage/plugin-permission-node@0.7.2-next.1
41
+ - @backstage/config@1.0.5-next.1
42
+ - @backstage/integration@1.4.1-next.1
43
+ - @backstage/catalog-client@1.2.0-next.1
44
+ - @backstage/catalog-model@1.1.4-next.1
45
+ - @backstage/errors@1.1.4-next.1
46
+ - @backstage/plugin-catalog-common@1.0.9-next.1
47
+ - @backstage/plugin-permission-common@0.7.2-next.1
48
+ - @backstage/plugin-scaffolder-common@1.2.3-next.1
49
+ - @backstage/plugin-search-common@1.1.2-next.1
50
+
51
+ ## 1.6.0-next.0
52
+
53
+ ### Minor Changes
54
+
55
+ - 16891a212c: Added new `POST /entities/by-refs` endpoint, which allows you to efficiently
56
+ batch-fetch entities by their entity ref. This can be useful e.g. in graphql
57
+ resolvers or similar contexts where you need to fetch many entities at the same
58
+ time.
59
+
60
+ ### Patch Changes
61
+
62
+ - d8593ce0e6: Do not use deprecated `LocationSpec` from the `@backstage/plugin-catalog-node` package
63
+ - 3280711113: Updated dependency `msw` to `^0.49.0`.
64
+ - e982f77fe3: Registered shutdown hook in experimental catalog plugin.
65
+ - Updated dependencies
66
+ - @backstage/catalog-client@1.2.0-next.0
67
+ - @backstage/backend-common@0.16.1-next.0
68
+ - @backstage/integration@1.4.1-next.0
69
+ - @backstage/plugin-permission-common@0.7.2-next.0
70
+ - @backstage/plugin-permission-node@0.7.2-next.0
71
+ - @backstage/types@1.0.2-next.0
72
+ - @backstage/backend-plugin-api@0.1.5-next.0
73
+ - @backstage/plugin-catalog-node@1.2.2-next.0
74
+ - @backstage/catalog-model@1.1.4-next.0
75
+ - @backstage/config@1.0.5-next.0
76
+ - @backstage/errors@1.1.4-next.0
77
+ - @backstage/plugin-catalog-common@1.0.9-next.0
78
+ - @backstage/plugin-scaffolder-common@1.2.3-next.0
79
+ - @backstage/plugin-search-common@1.1.2-next.0
80
+
3
81
  ## 1.5.1
4
82
 
5
83
  ### Patch Changes
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@backstage/plugin-catalog-backend",
3
- "version": "1.5.1",
3
+ "version": "1.6.0-next.1",
4
4
  "main": "../dist/index.cjs.js",
5
5
  "types": "../dist/index.alpha.d.ts"
6
6
  }
@@ -38,7 +38,7 @@ import { EntityRelationSpec } from '@backstage/plugin-catalog-node';
38
38
  import { GetEntitiesRequest } from '@backstage/catalog-client';
39
39
  import { JsonValue } from '@backstage/types';
40
40
  import { LocationEntityV1alpha1 } from '@backstage/catalog-model';
41
- import { LocationSpec } from '@backstage/plugin-catalog-node';
41
+ import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common';
42
42
  import { Logger } from 'winston';
43
43
  import { Permission } from '@backstage/plugin-permission-common';
44
44
  import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
@@ -114,7 +114,7 @@ export declare class AnnotateLocationEntityProcessor implements CatalogProcessor
114
114
  integrations: ScmIntegrationRegistry;
115
115
  });
116
116
  getProcessorName(): string;
117
- preProcessEntity(entity: Entity, location: LocationSpec, _: CatalogProcessorEmit, originLocation: LocationSpec): Promise<Entity>;
117
+ preProcessEntity(entity: Entity, location: LocationSpec_2, _: CatalogProcessorEmit, originLocation: LocationSpec_2): Promise<Entity>;
118
118
  }
119
119
 
120
120
  /** @public */
@@ -125,7 +125,7 @@ export declare class AnnotateScmSlugEntityProcessor implements CatalogProcessor
125
125
  });
126
126
  getProcessorName(): string;
127
127
  static fromConfig(config: Config): AnnotateScmSlugEntityProcessor;
128
- preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
128
+ preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
129
129
  }
130
130
 
131
131
  /** @public */
@@ -133,7 +133,7 @@ export declare class BuiltinKindsEntityProcessor implements CatalogProcessor {
133
133
  private readonly validators;
134
134
  getProcessorName(): string;
135
135
  validateEntityKind(entity: Entity): Promise<boolean>;
136
- postProcessEntity(entity: Entity, _location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
136
+ postProcessEntity(entity: Entity, _location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
137
137
  }
138
138
 
139
139
  /**
@@ -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
  */
@@ -427,7 +433,7 @@ export declare class CodeOwnersProcessor implements CatalogProcessor {
427
433
  reader: UrlReader;
428
434
  });
429
435
  getProcessorName(): string;
430
- preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
436
+ preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
431
437
  }
432
438
 
433
439
  /**
@@ -586,7 +592,7 @@ export { EntityRelationSpec }
586
592
  /** @public */
587
593
  export declare class FileReaderProcessor implements CatalogProcessor {
588
594
  getProcessorName(): string;
589
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
595
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
590
596
  }
591
597
 
592
598
  /** @public */
@@ -605,7 +611,7 @@ export declare class LocationEntityProcessor implements CatalogProcessor {
605
611
  private readonly options;
606
612
  constructor(options: LocationEntityProcessorOptions);
607
613
  getProcessorName(): string;
608
- postProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
614
+ postProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
609
615
  }
610
616
 
611
617
  /** @public */
@@ -613,16 +619,28 @@ export declare type LocationEntityProcessorOptions = {
613
619
  integrations: ScmIntegrationRegistry;
614
620
  };
615
621
 
616
- export { LocationSpec }
622
+ /**
623
+ * Holds the entity location information.
624
+ *
625
+ * @remarks
626
+ *
627
+ * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch.
628
+ * This flag is then set to indicate that the file can be not present.
629
+ * default value: 'required'.
630
+ *
631
+ * @public
632
+ * @deprecated use the same type from `@backstage/plugin-catalog-common` instead
633
+ */
634
+ export declare type LocationSpec = LocationSpec_2;
617
635
 
618
636
  /** @public */
619
637
  export declare function locationSpecToLocationEntity(opts: {
620
- location: LocationSpec;
638
+ location: LocationSpec_2;
621
639
  parentEntity?: Entity;
622
640
  }): LocationEntityV1alpha1;
623
641
 
624
642
  /** @public */
625
- export declare function parseEntityYaml(data: Buffer, location: LocationSpec): Iterable<CatalogProcessorResult>;
643
+ export declare function parseEntityYaml(data: Buffer, location: LocationSpec_2): Iterable<CatalogProcessorResult>;
626
644
 
627
645
  /**
628
646
  * These permission rules can be used to conditionally filter catalog entities
@@ -663,7 +681,7 @@ export declare class PlaceholderProcessor implements CatalogProcessor {
663
681
  private readonly options;
664
682
  constructor(options: PlaceholderProcessorOptions);
665
683
  getProcessorName(): string;
666
- preProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
684
+ preProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
667
685
  }
668
686
 
669
687
  /** @public */
@@ -719,7 +737,7 @@ export declare class UrlReaderProcessor implements CatalogProcessor {
719
737
  logger: Logger;
720
738
  });
721
739
  getProcessorName(): string;
722
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
740
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
723
741
  private doRead;
724
742
  }
725
743
 
@@ -38,7 +38,7 @@ import { EntityRelationSpec } from '@backstage/plugin-catalog-node';
38
38
  import { GetEntitiesRequest } from '@backstage/catalog-client';
39
39
  import { JsonValue } from '@backstage/types';
40
40
  import { LocationEntityV1alpha1 } from '@backstage/catalog-model';
41
- import { LocationSpec } from '@backstage/plugin-catalog-node';
41
+ import { LocationSpec as LocationSpec_2 } from '@backstage/plugin-catalog-common';
42
42
  import { Logger } from 'winston';
43
43
  import { Permission } from '@backstage/plugin-permission-common';
44
44
  import { PermissionAuthorizer } from '@backstage/plugin-permission-common';
@@ -114,7 +114,7 @@ export declare class AnnotateLocationEntityProcessor implements CatalogProcessor
114
114
  integrations: ScmIntegrationRegistry;
115
115
  });
116
116
  getProcessorName(): string;
117
- preProcessEntity(entity: Entity, location: LocationSpec, _: CatalogProcessorEmit, originLocation: LocationSpec): Promise<Entity>;
117
+ preProcessEntity(entity: Entity, location: LocationSpec_2, _: CatalogProcessorEmit, originLocation: LocationSpec_2): Promise<Entity>;
118
118
  }
119
119
 
120
120
  /** @public */
@@ -125,7 +125,7 @@ export declare class AnnotateScmSlugEntityProcessor implements CatalogProcessor
125
125
  });
126
126
  getProcessorName(): string;
127
127
  static fromConfig(config: Config): AnnotateScmSlugEntityProcessor;
128
- preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
128
+ preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
129
129
  }
130
130
 
131
131
  /** @public */
@@ -133,7 +133,7 @@ export declare class BuiltinKindsEntityProcessor implements CatalogProcessor {
133
133
  private readonly validators;
134
134
  getProcessorName(): string;
135
135
  validateEntityKind(entity: Entity): Promise<boolean>;
136
- postProcessEntity(entity: Entity, _location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
136
+ postProcessEntity(entity: Entity, _location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
137
137
  }
138
138
 
139
139
  /**
@@ -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
  */
@@ -380,7 +386,7 @@ export declare class CodeOwnersProcessor implements CatalogProcessor {
380
386
  reader: UrlReader;
381
387
  });
382
388
  getProcessorName(): string;
383
- preProcessEntity(entity: Entity, location: LocationSpec): Promise<Entity>;
389
+ preProcessEntity(entity: Entity, location: LocationSpec_2): Promise<Entity>;
384
390
  }
385
391
 
386
392
  /* Excluded from this release type: createCatalogConditionalDecision */
@@ -501,7 +507,7 @@ export { EntityRelationSpec }
501
507
  /** @public */
502
508
  export declare class FileReaderProcessor implements CatalogProcessor {
503
509
  getProcessorName(): string;
504
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
510
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser): Promise<boolean>;
505
511
  }
506
512
 
507
513
  /** @public */
@@ -520,7 +526,7 @@ export declare class LocationEntityProcessor implements CatalogProcessor {
520
526
  private readonly options;
521
527
  constructor(options: LocationEntityProcessorOptions);
522
528
  getProcessorName(): string;
523
- postProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
529
+ postProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
524
530
  }
525
531
 
526
532
  /** @public */
@@ -528,16 +534,28 @@ export declare type LocationEntityProcessorOptions = {
528
534
  integrations: ScmIntegrationRegistry;
529
535
  };
530
536
 
531
- export { LocationSpec }
537
+ /**
538
+ * Holds the entity location information.
539
+ *
540
+ * @remarks
541
+ *
542
+ * `presence` flag: when using repo importer plugin, location is being created before the component yaml file is merged to the main branch.
543
+ * This flag is then set to indicate that the file can be not present.
544
+ * default value: 'required'.
545
+ *
546
+ * @public
547
+ * @deprecated use the same type from `@backstage/plugin-catalog-common` instead
548
+ */
549
+ export declare type LocationSpec = LocationSpec_2;
532
550
 
533
551
  /** @public */
534
552
  export declare function locationSpecToLocationEntity(opts: {
535
- location: LocationSpec;
553
+ location: LocationSpec_2;
536
554
  parentEntity?: Entity;
537
555
  }): LocationEntityV1alpha1;
538
556
 
539
557
  /** @public */
540
- export declare function parseEntityYaml(data: Buffer, location: LocationSpec): Iterable<CatalogProcessorResult>;
558
+ export declare function parseEntityYaml(data: Buffer, location: LocationSpec_2): Iterable<CatalogProcessorResult>;
541
559
 
542
560
  /* Excluded from this release type: permissionRules */
543
561
 
@@ -550,7 +568,7 @@ export declare class PlaceholderProcessor implements CatalogProcessor {
550
568
  private readonly options;
551
569
  constructor(options: PlaceholderProcessorOptions);
552
570
  getProcessorName(): string;
553
- preProcessEntity(entity: Entity, location: LocationSpec, emit: CatalogProcessorEmit): Promise<Entity>;
571
+ preProcessEntity(entity: Entity, location: LocationSpec_2, emit: CatalogProcessorEmit): Promise<Entity>;
554
572
  }
555
573
 
556
574
  /** @public */
@@ -606,7 +624,7 @@ export declare class UrlReaderProcessor implements CatalogProcessor {
606
624
  logger: Logger;
607
625
  });
608
626
  getProcessorName(): string;
609
- readLocation(location: LocationSpec, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
627
+ readLocation(location: LocationSpec_2, optional: boolean, emit: CatalogProcessorEmit, parser: CatalogProcessorParser, cache: CatalogProcessorCache): Promise<boolean>;
610
628
  private doRead;
611
629
  }
612
630
 
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) {
@@ -2525,6 +2525,31 @@ class DefaultEntitiesCatalog {
2525
2525
  pageInfo
2526
2526
  };
2527
2527
  }
2528
+ async entitiesBatch(request) {
2529
+ const lookup = /* @__PURE__ */ new Map();
2530
+ for (const chunk of lodash__default["default"].chunk(request.entityRefs, 200)) {
2531
+ let query = this.database("final_entities").innerJoin("refresh_state", {
2532
+ "refresh_state.entity_id": "final_entities.entity_id"
2533
+ }).select({
2534
+ entityRef: "refresh_state.entity_ref",
2535
+ entity: "final_entities.final_entity"
2536
+ }).whereIn("refresh_state.entity_ref", chunk);
2537
+ if (request == null ? void 0 : request.filter) {
2538
+ query = parseFilter(request.filter, query, this.database);
2539
+ }
2540
+ for (const row of await query) {
2541
+ lookup.set(row.entityRef, row.entity ? JSON.parse(row.entity) : null);
2542
+ }
2543
+ }
2544
+ let items = request.entityRefs.map((ref) => {
2545
+ var _a;
2546
+ return (_a = lookup.get(ref)) != null ? _a : null;
2547
+ });
2548
+ if (request.fields) {
2549
+ items = items.map((e) => e && request.fields(e));
2550
+ }
2551
+ return { items };
2552
+ }
2528
2553
  async removeEntityByUid(uid) {
2529
2554
  await this.database("refresh_state").update({
2530
2555
  result_hash: "child-was-deleted",
@@ -2905,13 +2930,16 @@ class DefaultCatalogProcessingOrchestrator {
2905
2930
  e
2906
2931
  );
2907
2932
  }
2908
- let foundKind = false;
2933
+ let valid = false;
2909
2934
  for (const processor of this.options.processors) {
2910
2935
  if (processor.validateEntityKind) {
2911
2936
  try {
2912
- foundKind = await processor.validateEntityKind(entity);
2913
- if (foundKind) {
2914
- break;
2937
+ const thisValid = await processor.validateEntityKind(entity);
2938
+ if (thisValid) {
2939
+ valid = true;
2940
+ if (this.options.legacySingleProcessorValidation) {
2941
+ break;
2942
+ }
2915
2943
  }
2916
2944
  } catch (e) {
2917
2945
  throw new errors.InputError(
@@ -2921,7 +2949,7 @@ class DefaultCatalogProcessingOrchestrator {
2921
2949
  }
2922
2950
  }
2923
2951
  }
2924
- if (!foundKind) {
2952
+ if (!valid) {
2925
2953
  throw new errors.InputError(
2926
2954
  `No processor recognized the entity ${context.entityRef} as valid, possibly caused by a foreign kind or apiVersion`
2927
2955
  );
@@ -3134,22 +3162,24 @@ class Stitcher {
3134
3162
  hash: "",
3135
3163
  stitch_ticket: ticket
3136
3164
  }).onConflict("entity_id").merge(["stitch_ticket"]);
3137
- const result = await this.database.with("incoming_references", function incomingReferences(builder) {
3138
- return builder.from("refresh_state_references").where({ target_entity_ref: entityRef }).count({ count: "*" });
3139
- }).select({
3140
- entityId: "refresh_state.entity_id",
3141
- processedEntity: "refresh_state.processed_entity",
3142
- errors: "refresh_state.errors",
3143
- incomingReferenceCount: "incoming_references.count",
3144
- previousHash: "final_entities.hash",
3145
- relationType: "relations.type",
3146
- relationTarget: "relations.target_entity_ref"
3147
- }).from("refresh_state").where({ "refresh_state.entity_ref": entityRef }).crossJoin(this.database.raw("incoming_references")).leftOuterJoin("final_entities", {
3148
- "final_entities.entity_id": "refresh_state.entity_id"
3149
- }).leftOuterJoin("relations", {
3150
- "relations.source_entity_ref": "refresh_state.entity_ref"
3151
- }).orderBy("relationType", "asc").orderBy("relationTarget", "asc");
3152
- if (!result.length) {
3165
+ const [processedResult, relationsResult] = await Promise.all([
3166
+ this.database.with("incoming_references", function incomingReferences(builder) {
3167
+ return builder.from("refresh_state_references").where({ target_entity_ref: entityRef }).count({ count: "*" });
3168
+ }).select({
3169
+ entityId: "refresh_state.entity_id",
3170
+ processedEntity: "refresh_state.processed_entity",
3171
+ errors: "refresh_state.errors",
3172
+ incomingReferenceCount: "incoming_references.count",
3173
+ previousHash: "final_entities.hash"
3174
+ }).from("refresh_state").where({ "refresh_state.entity_ref": entityRef }).crossJoin(this.database.raw("incoming_references")).leftOuterJoin("final_entities", {
3175
+ "final_entities.entity_id": "refresh_state.entity_id"
3176
+ }),
3177
+ this.database.distinct({
3178
+ relationType: "type",
3179
+ relationTarget: "target_entity_ref"
3180
+ }).from("relations").where({ source_entity_ref: entityRef }).orderBy("relationType", "asc").orderBy("relationTarget", "asc")
3181
+ ]);
3182
+ if (!processedResult.length) {
3153
3183
  this.logger.error(
3154
3184
  `Unable to stitch ${entityRef}, item does not exist in refresh state table`
3155
3185
  );
@@ -3161,7 +3191,7 @@ class Stitcher {
3161
3191
  errors,
3162
3192
  incomingReferenceCount,
3163
3193
  previousHash
3164
- } = result[0];
3194
+ } = processedResult[0];
3165
3195
  if (!processedEntity) {
3166
3196
  this.logger.debug(
3167
3197
  `Unable to stitch ${entityRef}, the entity has not yet been processed`
@@ -3189,11 +3219,7 @@ class Stitcher {
3189
3219
  }));
3190
3220
  }
3191
3221
  }
3192
- const uniqueRelationRows = lodash.uniqBy(
3193
- result,
3194
- (r) => `${r.relationType}:${r.relationTarget}`
3195
- );
3196
- entity.relations = uniqueRelationRows.filter((row) => row.relationType).map((row) => ({
3222
+ entity.relations = relationsResult.filter((row) => row.relationType).map((row) => ({
3197
3223
  type: row.relationType,
3198
3224
  targetRef: row.relationTarget
3199
3225
  }));
@@ -3230,6 +3256,19 @@ class Stitcher {
3230
3256
  }
3231
3257
  }
3232
3258
 
3259
+ const schema = zod.z.object({
3260
+ entityRefs: zod.z.array(zod.z.string())
3261
+ });
3262
+ function entitiesBatchRequest(req) {
3263
+ try {
3264
+ return schema.parse(req.body);
3265
+ } catch (error) {
3266
+ throw new errors.InputError(
3267
+ `Malformed request body (did you remember to specify an application/json content type?), ${error.message}`
3268
+ );
3269
+ }
3270
+ }
3271
+
3233
3272
  function basicEntityFilter(items) {
3234
3273
  const filtersByKey = {};
3235
3274
  for (const [key, value] of Object.entries(items)) {
@@ -3292,8 +3331,8 @@ function parseEntityFilterString(filterString) {
3292
3331
  const filtersByKey = {};
3293
3332
  for (const statement of statements) {
3294
3333
  const equalsIndex = statement.indexOf("=");
3295
- const key = equalsIndex === -1 ? statement : statement.substr(0, equalsIndex).trim();
3296
- const value = equalsIndex === -1 ? void 0 : statement.substr(equalsIndex + 1).trim();
3334
+ const key = equalsIndex === -1 ? statement : statement.substring(0, equalsIndex).trim();
3335
+ const value = equalsIndex === -1 ? void 0 : statement.substring(equalsIndex + 1).trim();
3297
3336
  if (!key) {
3298
3337
  throw new errors.InputError(
3299
3338
  `Invalid filter, '${statement}' is not a valid statement (expected a string on the form a=b or a= or a)`
@@ -3369,6 +3408,17 @@ function parseEntityTransformParams(params) {
3369
3408
  };
3370
3409
  }
3371
3410
 
3411
+ function parseEntityFacetParams(params) {
3412
+ const facetStrings = parseStringsParam(params.facet, "facet");
3413
+ if (facetStrings) {
3414
+ const filtered = facetStrings.filter(Boolean);
3415
+ if (filtered.length) {
3416
+ return filtered;
3417
+ }
3418
+ }
3419
+ throw new errors.InputError("Missing facet parameter");
3420
+ }
3421
+
3372
3422
  async function requireRequestBody(req) {
3373
3423
  const contentType = req.header("content-type");
3374
3424
  if (!contentType) {
@@ -3405,17 +3455,6 @@ function disallowReadonlyMode(readonly) {
3405
3455
  }
3406
3456
  }
3407
3457
 
3408
- function parseEntityFacetParams(params) {
3409
- const facetStrings = parseStringsParam(params.facet, "facet");
3410
- if (facetStrings) {
3411
- const filtered = facetStrings.filter(Boolean);
3412
- if (filtered.length) {
3413
- return filtered;
3414
- }
3415
- }
3416
- throw new errors.InputError("Missing facet parameter");
3417
- }
3418
-
3419
3458
  async function createRouter(options) {
3420
3459
  const {
3421
3460
  entitiesCatalog,
@@ -3503,7 +3542,16 @@ async function createRouter(options) {
3503
3542
  });
3504
3543
  res.status(200).json(response);
3505
3544
  }
3506
- ).get("/entity-facets", async (req, res) => {
3545
+ ).post("/entities/by-refs", async (req, res) => {
3546
+ const request = entitiesBatchRequest(req);
3547
+ const token = getBearerToken(req.header("authorization"));
3548
+ const response = await entitiesCatalog.entitiesBatch({
3549
+ entityRefs: request.entityRefs,
3550
+ fields: parseEntityTransformParams(req.query),
3551
+ authorizationToken: token
3552
+ });
3553
+ res.status(200).json(response);
3554
+ }).get("/entity-facets", async (req, res) => {
3507
3555
  const response = await entitiesCatalog.facets({
3508
3556
  filter: parseEntityFilterParams(req.query),
3509
3557
  facets: parseEntityFacetParams(req.query),
@@ -3829,6 +3877,27 @@ class AuthorizedEntitiesCatalog {
3829
3877
  }
3830
3878
  return this.entitiesCatalog.entities(request);
3831
3879
  }
3880
+ async entitiesBatch(request) {
3881
+ const authorizeDecision = (await this.permissionApi.authorizeConditional(
3882
+ [{ permission: pluginCatalogCommon.catalogEntityReadPermission }],
3883
+ { token: request == null ? void 0 : request.authorizationToken }
3884
+ ))[0];
3885
+ if (authorizeDecision.result === pluginPermissionCommon.AuthorizeResult.DENY) {
3886
+ return {
3887
+ items: new Array(request.entityRefs.length).fill(null)
3888
+ };
3889
+ }
3890
+ if (authorizeDecision.result === pluginPermissionCommon.AuthorizeResult.CONDITIONAL) {
3891
+ const permissionFilter = this.transformConditions(
3892
+ authorizeDecision.conditions
3893
+ );
3894
+ return this.entitiesCatalog.entitiesBatch({
3895
+ ...request,
3896
+ filter: (request == null ? void 0 : request.filter) ? { allOf: [permissionFilter, request.filter] } : permissionFilter
3897
+ });
3898
+ }
3899
+ return this.entitiesCatalog.entitiesBatch(request);
3900
+ }
3832
3901
  async removeEntityByUid(uid, options) {
3833
3902
  const authorizeResponse = (await this.permissionApi.authorizeConditional(
3834
3903
  [{ permission: pluginCatalogCommon.catalogEntityDeletePermission }],
@@ -3989,6 +4058,7 @@ class CatalogBuilder {
3989
4058
  maxSeconds: 150
3990
4059
  });
3991
4060
  this.locationAnalyzer = void 0;
4061
+ this.legacySingleProcessorValidation = false;
3992
4062
  this.env = env;
3993
4063
  this.entityPolicies = [];
3994
4064
  this.entityPoliciesReplace = false;
@@ -4070,11 +4140,16 @@ class CatalogBuilder {
4070
4140
  }
4071
4141
  addPermissionRules(...permissionRules) {
4072
4142
  this.permissionRules.push(...permissionRules.flat());
4143
+ return this;
4073
4144
  }
4074
4145
  setAllowedLocationTypes(allowedLocationTypes) {
4075
4146
  this.allowedLocationType = allowedLocationTypes;
4076
4147
  return this;
4077
4148
  }
4149
+ useLegacySingleProcessorValidation() {
4150
+ this.legacySingleProcessorValidation = true;
4151
+ return this;
4152
+ }
4078
4153
  async build() {
4079
4154
  var _a, _b;
4080
4155
  const { config, database, logger, permissions } = this.env;
@@ -4099,7 +4174,8 @@ class CatalogBuilder {
4099
4174
  rulesEnforcer,
4100
4175
  logger,
4101
4176
  parser,
4102
- policy
4177
+ policy,
4178
+ legacySingleProcessorValidation: this.legacySingleProcessorValidation
4103
4179
  });
4104
4180
  const stitcher = new Stitcher(dbClient, logger);
4105
4181
  const unauthorizedEntitiesCatalog = new DefaultEntitiesCatalog(
@@ -4373,7 +4449,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4373
4449
  reader: backendPluginApi.urlReaderServiceRef,
4374
4450
  permissions: backendPluginApi.permissionsServiceRef,
4375
4451
  database: backendPluginApi.databaseServiceRef,
4376
- httpRouter: backendPluginApi.httpRouterServiceRef
4452
+ httpRouter: backendPluginApi.httpRouterServiceRef,
4453
+ lifecycle: backendPluginApi.lifecycleServiceRef
4377
4454
  },
4378
4455
  async init({
4379
4456
  logger,
@@ -4381,7 +4458,8 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4381
4458
  reader,
4382
4459
  database,
4383
4460
  permissions,
4384
- httpRouter
4461
+ httpRouter,
4462
+ lifecycle
4385
4463
  }) {
4386
4464
  const winstonLogger = backendPluginApi.loggerToWinstonLogger(logger);
4387
4465
  const builder = await CatalogBuilder.create({
@@ -4395,6 +4473,11 @@ const catalogPlugin = backendPluginApi.createBackendPlugin({
4395
4473
  builder.addEntityProvider(...processingExtensions.entityProviders);
4396
4474
  const { processingEngine, router } = await builder.build();
4397
4475
  await processingEngine.start();
4476
+ lifecycle.addShutdownHook({
4477
+ fn: async () => {
4478
+ await processingEngine.stop();
4479
+ }
4480
+ });
4398
4481
  httpRouter.use(router);
4399
4482
  }
4400
4483
  });